diff options
82 files changed, 1852 insertions, 380 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java b/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java index a0d9133b93da..7fc630c964e5 100644 --- a/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java +++ b/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java @@ -136,10 +136,9 @@ public class UserWakeupStore { * Remove wakeup scheduled for the user with given userId if present. */ public void removeUserWakeup(int userId) { - synchronized (mUserWakeupLock) { - mUserStarts.delete(userId); + if (deleteWakeupFromUserStarts(userId)) { + updateUserListFile(); } - updateUserListFile(); } /** @@ -186,7 +185,7 @@ public class UserWakeupStore { * Return scheduled start time for user or -1 if user does not have alarm set. */ @VisibleForTesting - long getWakeupTimeForUserForTest(int userId) { + long getWakeupTimeForUser(int userId) { synchronized (mUserWakeupLock) { return mUserStarts.get(userId, -1); } @@ -197,8 +196,11 @@ public class UserWakeupStore { */ public void onUserStarting(int userId) { synchronized (mUserWakeupLock) { - mStartingUsers.put(userId, getWakeupTimeForUserForTest(userId)); - mUserStarts.delete(userId); + final long wakeup = getWakeupTimeForUser(userId); + if (wakeup >= 0) { + mStartingUsers.put(userId, wakeup); + mUserStarts.delete(userId); + } } } @@ -206,21 +208,48 @@ public class UserWakeupStore { * Remove userId from starting user list once start is complete. */ public void onUserStarted(int userId) { - synchronized (mUserWakeupLock) { - mStartingUsers.delete(userId); + if (deleteWakeupFromStartingUsers(userId)) { + updateUserListFile(); } - updateUserListFile(); } /** * Remove userId from the store when the user is removed. */ public void onUserRemoved(int userId) { + if (deleteWakeupFromUserStarts(userId) || deleteWakeupFromStartingUsers(userId)) { + updateUserListFile(); + } + } + + /** + * Remove wakeup for a given userId from mUserStarts. + * @return true if an entry is found and the list of wakeups changes. + */ + private boolean deleteWakeupFromUserStarts(int userId) { + int index; synchronized (mUserWakeupLock) { - mUserStarts.delete(userId); - mStartingUsers.delete(userId); + index = mUserStarts.indexOfKey(userId); + if (index >= 0) { + mUserStarts.removeAt(index); + } } - updateUserListFile(); + return index >= 0; + } + + /** + * Remove wakeup for a given userId from mStartingUsers. + * @return true if an entry is found and the list of wakeups changes. + */ + private boolean deleteWakeupFromStartingUsers(int userId) { + int index; + synchronized (mUserWakeupLock) { + index = mStartingUsers.indexOfKey(userId); + if (index >= 0) { + mStartingUsers.removeAt(index); + } + } + return index >= 0; } /** diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 5a3ff83b6861..628611d8d2c0 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1770,14 +1770,16 @@ package android.hardware.input { } public final class InputManager { - method public void addUniqueIdAssociation(@NonNull String, @NonNull String); + method @RequiresPermission("android.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY") public void addUniqueIdAssociation(@NonNull String, @NonNull String); + method @FlaggedApi("com.android.input.flags.device_associations") @RequiresPermission("android.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY") public void addUniqueIdAssociationByDescriptor(@NonNull String, @NonNull String); method @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public void clearAllModifierKeyRemappings(); method @NonNull public java.util.List<java.lang.String> getKeyboardLayoutDescriptors(); method @NonNull public String getKeyboardLayoutTypeForLayoutDescriptor(@NonNull String); method @NonNull @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public java.util.Map<java.lang.Integer,java.lang.Integer> getModifierKeyRemapping(); method public int getMousePointerSpeed(); method @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public void remapModifierKey(int, int); - method public void removeUniqueIdAssociation(@NonNull String); + method @RequiresPermission("android.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY") public void removeUniqueIdAssociation(@NonNull String); + method @FlaggedApi("com.android.input.flags.device_associations") @RequiresPermission("android.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY") public void removeUniqueIdAssociationByDescriptor(@NonNull String); field public static final long BLOCK_UNTRUSTED_TOUCHES = 158002302L; // 0x96aec7eL } diff --git a/core/api/test-lint-baseline.txt b/core/api/test-lint-baseline.txt index 685ea63cb432..c4fe06170414 100644 --- a/core/api/test-lint-baseline.txt +++ b/core/api/test-lint-baseline.txt @@ -977,8 +977,12 @@ RequiresPermission: android.hardware.hdmi.HdmiControlManager#setHdmiCecVersion(i Method 'setHdmiCecVersion' documentation mentions permissions already declared by @RequiresPermission RequiresPermission: android.hardware.input.InputManager#addUniqueIdAssociation(String, String): Method 'addUniqueIdAssociation' documentation mentions permissions without declaring @RequiresPermission +RequiresPermission: android.hardware.input.InputManager#addUniqueIdAssociationByDescriptor(String, String): + Method 'addUniqueIdAssociationByDescriptor' documentation mentions permissions already declared by @RequiresPermission RequiresPermission: android.hardware.input.InputManager#removeUniqueIdAssociation(String): Method 'removeUniqueIdAssociation' documentation mentions permissions without declaring @RequiresPermission +RequiresPermission: android.hardware.input.InputManager#removeUniqueIdAssociationByDescriptor(String): + Method 'removeUniqueIdAssociationByDescriptor' documentation mentions permissions already declared by @RequiresPermission RequiresPermission: android.hardware.location.GeofenceHardware#addGeofence(int, int, android.hardware.location.GeofenceHardwareRequest, android.hardware.location.GeofenceHardwareCallback): Method 'addGeofence' documentation mentions permissions without declaring @RequiresPermission RequiresPermission: android.hardware.location.GeofenceHardware#getMonitoringTypes(): diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index b3312a829298..8e766c95b2e8 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -2577,8 +2577,6 @@ public class AppOpsManager { OP_RECEIVE_SANDBOX_TRIGGER_AUDIO, OP_MEDIA_ROUTING_CONTROL, OP_READ_SYSTEM_GRAMMATICAL_GENDER, - OP_ARCHIVE_ICON_OVERLAY, - OP_UNARCHIVAL_CONFIRMATION, }; static final AppOpInfo[] sAppOpInfos = new AppOpInfo[]{ diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 714454b01d93..b19dc8f6e470 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -5709,6 +5709,7 @@ public class Notification implements Parcelable TemplateBindResult result) { p.headerless(resId == getBaseLayoutResource() || resId == getHeadsUpBaseLayoutResource() + || resId == getCompactHeadsUpBaseLayoutResource() || resId == getMessagingLayoutResource() || resId == R.layout.notification_template_material_media); RemoteViews contentView = new BuilderRemoteViews(mContext.getApplicationInfo(), resId); @@ -6594,6 +6595,36 @@ public class Notification implements Parcelable } /** + * Construct a RemoteViews for the final compact heads-up notification layout. + * @hide + */ + public RemoteViews createCompactHeadsUpContentView() { + // TODO(b/336225281): re-evaluate custom view usage. + if (useExistingRemoteView(mN.headsUpContentView)) { + return fullyCustomViewRequiresDecoration(false /* fromStyle */) + ? minimallyDecoratedHeadsUpContentView(mN.headsUpContentView) + : mN.headsUpContentView; + } else if (mStyle != null) { + final RemoteViews styleView = mStyle.makeCompactHeadsUpContentView(); + if (styleView != null) { + return styleView; + } + } + + final StandardTemplateParams p = mParams.reset() + .viewType(StandardTemplateParams.VIEW_TYPE_HEADS_UP) + .fillTextsFrom(this); + // Notification text is shown as secondary header text + // for the minimal hun when it is provided. + // Time(when and chronometer) is not shown for the minimal hun. + p.headerTextSecondary(p.mText).text(null).hideTime(true); + + return applyStandardTemplate( + getCompactHeadsUpBaseLayoutResource(), p, + null /* result */); + } + + /** * Construct a RemoteViews representing the heads up notification layout. * * @deprecated For performance and system health reasons, this API is no longer required to @@ -7269,6 +7300,10 @@ public class Notification implements Parcelable return R.layout.notification_template_material_heads_up_base; } + private int getCompactHeadsUpBaseLayoutResource() { + return R.layout.notification_template_material_compact_heads_up_base; + } + private int getBigBaseLayoutResource() { return R.layout.notification_template_material_big_base; } @@ -7795,6 +7830,16 @@ public class Notification implements Parcelable } /** + * Construct a Style-specific RemoteViews for the final compact HUN layout. + * return null to use the standard compact heads up view. + * @hide + */ + @Nullable + public RemoteViews makeCompactHeadsUpContentView() { + return null; + } + + /** * Apply any style-specific extras to this notification before shipping it out. * @hide */ @@ -9106,6 +9151,16 @@ public class Notification implements Parcelable /** * @hide */ + @Nullable + @Override + public RemoteViews makeCompactHeadsUpContentView() { + // TODO(b/336229954): Apply minimal HUN treatment to Messaging Notifications. + return makeHeadsUpContentView(false); + } + + /** + * @hide + */ @Override public void reduceImageSizes(Context context) { super.reduceImageSizes(context); @@ -10275,6 +10330,16 @@ public class Notification implements Parcelable /** * @hide */ + @Nullable + @Override + public RemoteViews makeCompactHeadsUpContentView() { + // TODO(b/336228700): Apply minimal HUN treatment for Call Style. + return makeHeadsUpContentView(false); + } + + /** + * @hide + */ public RemoteViews makeBigContentView() { return makeCallLayout(StandardTemplateParams.VIEW_TYPE_BIG); } diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig index e694ccca2b05..e3c367f8adce 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -138,4 +138,11 @@ flag { namespace: "systemui" description: "Cleans up spans and unnecessary new lines from standard notification templates" bug: "313439845" -}
\ No newline at end of file +} + +flag { + name: "compact_heads_up_notification" + namespace: "systemui" + description: "[Minimal HUN] Enables the compact heads up notification feature" + bug: "270709257" +} diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index c57a3a69cb1b..cd1913bb26d9 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -61,6 +61,13 @@ flag { } flag { + name: "new_multiuser_settings_ux" + namespace: "multiuser" + description: "Update multiuser settings UI" + bug: "298008926" +} + +flag { name: "enable_biometrics_to_unlock_private_space" is_exported: true namespace: "profile_experiences" diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl index 1c37aa25af5f..243ae142d9b6 100644 --- a/core/java/android/hardware/input/IInputManager.aidl +++ b/core/java/android/hardware/input/IInputManager.aidl @@ -165,9 +165,16 @@ interface IInputManager { // static association for the cleared input port will be restored. void removePortAssociation(in String inputPort); - // Add a runtime association between the input device and display. + // Add a runtime association between the input device and display, using device's descriptor. + void addUniqueIdAssociationByDescriptor(in String inputDeviceDescriptor, + in String displayUniqueId); + // Remove the runtime association between the input device and display, using device's + // descriptor. + void removeUniqueIdAssociationByDescriptor(in String inputDeviceDescriptor); + + // Add a runtime association between the input device and display, using device's port. void addUniqueIdAssociation(in String inputPort, in String displayUniqueId); - // Remove the runtime association between the input device and display. + // Remove the runtime association between the input device and display, using device's port. void removeUniqueIdAssociation(in String inputPort); InputSensorInfo[] getSensorList(int deviceId); diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index f94915876a2f..dd4ea31af17d 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -17,6 +17,7 @@ package android.hardware.input; import static com.android.input.flags.Flags.FLAG_INPUT_DEVICE_VIEW_BEHAVIOR_API; +import static com.android.input.flags.Flags.FLAG_DEVICE_ASSOCIATIONS; import static com.android.hardware.input.Flags.keyboardLayoutPreviewFlag; import android.Manifest; @@ -1054,13 +1055,14 @@ public final class InputManager { /** * Add a runtime association between the input port and the display port. This overrides any * static associations. - * @param inputPort The port of the input device. - * @param displayPort The physical port of the associated display. + * @param inputPort the port of the input device + * @param displayPort the physical port of the associated display * <p> * Requires {@link android.Manifest.permission#ASSOCIATE_INPUT_DEVICE_TO_DISPLAY}. * </p> * @hide */ + @RequiresPermission(android.Manifest.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY) public void addPortAssociation(@NonNull String inputPort, int displayPort) { try { mIm.addPortAssociation(inputPort, displayPort); @@ -1072,12 +1074,13 @@ public final class InputManager { /** * Remove the runtime association between the input port and the display port. Any existing * static association for the cleared input port will be restored. - * @param inputPort The port of the input device to be cleared. + * @param inputPort the port of the input device to be cleared * <p> * Requires {@link android.Manifest.permission#ASSOCIATE_INPUT_DEVICE_TO_DISPLAY}. * </p> * @hide */ + @RequiresPermission(android.Manifest.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY) public void removePortAssociation(@NonNull String inputPort) { try { mIm.removePortAssociation(inputPort); @@ -1089,14 +1092,16 @@ public final class InputManager { /** * Add a runtime association between the input port and display, by unique id. Input ports are * expected to be unique. - * @param inputPort The port of the input device. - * @param displayUniqueId The unique id of the associated display. + * @param inputPort the port of the input device + * @param displayUniqueId the unique id of the associated display * <p> * Requires {@link android.Manifest.permission#ASSOCIATE_INPUT_DEVICE_TO_DISPLAY}. * </p> * @hide */ + @RequiresPermission(android.Manifest.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY) @TestApi + // TODO(b/324075859): Rename to addUniqueIdAssociationByPort public void addUniqueIdAssociation(@NonNull String inputPort, @NonNull String displayUniqueId) { mGlobal.addUniqueIdAssociation(inputPort, displayUniqueId); @@ -1104,18 +1109,60 @@ public final class InputManager { /** * Removes a runtime association between the input device and display. - * @param inputPort The port of the input device. + * @param inputPort the port of the input device * <p> * Requires {@link android.Manifest.permission#ASSOCIATE_INPUT_DEVICE_TO_DISPLAY}. * </p> * @hide */ + @RequiresPermission(android.Manifest.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY) @TestApi + // TODO(b/324075859): Rename to removeUniqueIdAssociationByPort public void removeUniqueIdAssociation(@NonNull String inputPort) { mGlobal.removeUniqueIdAssociation(inputPort); } /** + * Add a runtime association between the input device name and display, by descriptor. Input + * device descriptors are expected to be unique per physical device, though one physical + * device can have multiple virtual input devices that possess the same descriptor. + * E.g. a keyboard with built in trackpad will be 2 different input devices with the same + * descriptor. + * @param inputDeviceDescriptor the descriptor of the input device + * @param displayUniqueId the unique id of the associated display + * <p> + * Requires {@link android.Manifest.permissions.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY}. + * </p> + * @hide + */ + @FlaggedApi(FLAG_DEVICE_ASSOCIATIONS) + @RequiresPermission(android.Manifest.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY) + @TestApi + public void addUniqueIdAssociationByDescriptor(@NonNull String inputDeviceDescriptor, + @NonNull String displayUniqueId) { + mGlobal.addUniqueIdAssociationByDescriptor(inputDeviceDescriptor, displayUniqueId); + } + + /** + * Removes a runtime association between the input device and display. + } + + /** + * Removes a runtime association between the input device and display. + * @param inputDeviceDescriptor the descriptor of the input device + * <p> + * Requires {@link android.Manifest.permissions.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY}. + * </p> + * @hide + */ + @FlaggedApi(FLAG_DEVICE_ASSOCIATIONS) + @RequiresPermission(android.Manifest.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY) + @TestApi + public void removeUniqueIdAssociationByDescriptor(@NonNull String inputDeviceDescriptor) { + mGlobal.removeUniqueIdAssociationByDescriptor(inputDeviceDescriptor); + } + + /** * Reports the version of the Universal Stylus Initiative (USI) protocol supported by the given * display, if any. * diff --git a/core/java/android/hardware/input/InputManagerGlobal.java b/core/java/android/hardware/input/InputManagerGlobal.java index 7b29666d9a96..a9c97b1a0e51 100644 --- a/core/java/android/hardware/input/InputManagerGlobal.java +++ b/core/java/android/hardware/input/InputManagerGlobal.java @@ -1489,6 +1489,29 @@ public final class InputManagerGlobal { } /** + * @see InputManager#addUniqueIdAssociationByDescriptor(String, String) + */ + public void addUniqueIdAssociationByDescriptor(@NonNull String inputDeviceDescriptor, + @NonNull String displayUniqueId) { + try { + mIm.addUniqueIdAssociationByDescriptor(inputDeviceDescriptor, displayUniqueId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @see InputManager#removeUniqueIdAssociationByDescriptor(String) + */ + public void removeUniqueIdAssociationByDescriptor(@NonNull String inputDeviceDescriptor) { + try { + mIm.removeUniqueIdAssociationByDescriptor(inputDeviceDescriptor); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * @see InputManager#getInputDeviceBluetoothAddress(int) */ @RequiresPermission(Manifest.permission.BLUETOOTH) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 16649e852c4a..095ab70788c9 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -12629,6 +12629,15 @@ public final class Settings { * @hide */ public static final String V_TO_U_RESTORE_DENYLIST = "v_to_u_restore_denylist"; + + /** + * Integer property that determines which charging optimization mode is applied. + * [0-10] inclusive representing different modes, where 0 is the default indicating + * no optimization mode is applied. + * + * @hide + */ + public static final String CHARGE_OPTIMIZATION_MODE = "charge_optimization_mode"; } /** diff --git a/core/java/android/view/ImeBackAnimationController.java b/core/java/android/view/ImeBackAnimationController.java index ed049b5ec114..e14ddd6ed75b 100644 --- a/core/java/android/view/ImeBackAnimationController.java +++ b/core/java/android/view/ImeBackAnimationController.java @@ -70,13 +70,13 @@ public class ImeBackAnimationController implements OnBackAnimationCallback { @Override public void onBackStarted(@NonNull BackEvent backEvent) { - if (isAdjustResize()) { + if (!isBackAnimationAllowed()) { // There is no good solution for a predictive back animation if the app uses // adjustResize, since we can't relayout the whole app for every frame. We also don't // want to reveal any black areas behind the IME. Therefore let's not play any animation // in that case for now. Log.d(TAG, "onBackStarted -> not playing predictive back animation due to softinput" - + " mode adjustResize"); + + " mode adjustResize AND no animation callback registered"); return; } if (isHideAnimationInProgress()) { @@ -128,13 +128,13 @@ public class ImeBackAnimationController implements OnBackAnimationCallback { @Override public void onBackCancelled() { - if (isAdjustResize()) return; + if (!isBackAnimationAllowed()) return; startPostCommitAnim(/*hideIme*/ false); } @Override public void onBackInvoked() { - if (isAdjustResize()) { + if (!isBackAnimationAllowed()) { mInsetsController.hide(ime()); return; } @@ -252,9 +252,12 @@ public class ImeBackAnimationController implements OnBackAnimationCallback { } } - private boolean isAdjustResize() { + private boolean isBackAnimationAllowed() { + // back animation is allowed in all cases except when softInputMode is adjust_resize AND + // there is no app-registered WindowInsetsAnimationCallback. return (mViewRoot.mWindowAttributes.softInputMode & SOFT_INPUT_MASK_ADJUST) - == SOFT_INPUT_ADJUST_RESIZE; + != SOFT_INPUT_ADJUST_RESIZE + || (mViewRoot.mView != null && mViewRoot.mView.hasWindowInsetsAnimationCallback()); } private boolean isAdjustPan() { diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto index f5bbbb4dc0a1..9abb5c888841 100644 --- a/core/proto/android/providers/settings/secure.proto +++ b/core/proto/android/providers/settings/secure.proto @@ -731,7 +731,9 @@ message SecureSettingsProto { } optional Zen zen = 71; + optional SettingProto charge_optimization_mode = 101 [ (android.privacy).dest = DEST_AUTOMATIC ]; + // Please insert fields in alphabetical order and group them into messages // if possible (to avoid reaching the method limit). - // Next tag = 101; + // Next tag = 102; } diff --git a/core/res/res/layout/notification_template_material_compact_heads_up_base.xml b/core/res/res/layout/notification_template_material_compact_heads_up_base.xml new file mode 100644 index 000000000000..57da89858b9d --- /dev/null +++ b/core/res/res/layout/notification_template_material_compact_heads_up_base.xml @@ -0,0 +1,87 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ 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 + --> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/status_bar_latest_event_content" + android:layout_width="match_parent" + android:layout_height="@dimen/notification_header_height" + android:clipChildren="false" + android:tag="compactHUN" + android:gravity="center_vertical" + android:theme="@style/Theme.DeviceDefault.Notification" + android:importantForAccessibility="no"> + <com.android.internal.widget.CachingIconView + android:id="@+id/icon" + android:layout_width="@dimen/notification_icon_circle_size" + android:layout_height="@dimen/notification_icon_circle_size" + android:layout_gravity="center_vertical|start" + android:layout_marginStart="@dimen/notification_icon_circle_start" + android:background="@drawable/notification_icon_circle" + android:padding="@dimen/notification_icon_circle_padding" + android:maxDrawableWidth="@dimen/notification_icon_circle_size" + android:maxDrawableHeight="@dimen/notification_icon_circle_size" + /> + <FrameLayout + android:id="@+id/alternate_expand_target" + android:layout_width="@dimen/notification_content_margin_start" + android:layout_height="match_parent" + android:layout_gravity="start" + android:importantForAccessibility="no" + android:focusable="false" + /> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginStart="@dimen/notification_content_margin_start" + android:orientation="horizontal" + > + <NotificationTopLineView + android:id="@+id/notification_top_line" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_centerVertical="true" + android:layout_weight="1" + android:clipChildren="false" + android:gravity="center_vertical" + android:theme="@style/Theme.DeviceDefault.Notification" + > + <TextView + android:id="@+id/title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/notification_header_separating_margin" + android:ellipsize="end" + android:fadingEdge="horizontal" + android:singleLine="true" + android:textAlignment="viewStart" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" + /> + <include layout="@layout/notification_top_line_views" /> + </NotificationTopLineView> + <FrameLayout + android:id="@+id/expand_button_touch_container" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:minWidth="@dimen/notification_content_margin_end" + > + <include layout="@layout/notification_expand_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical|end" + /> + </FrameLayout> + </LinearLayout> +</FrameLayout> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 1a618700cd17..cfe96c520571 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2042,10 +2042,17 @@ provider services. --> <string name="config_secondaryLocationTimeZoneProviderPackageName" translatable="false"></string> + <!-- Whether the telephony time zone detection feature is enabled. Setting this to false means + the feature cannot be used. Setting this to true means the feature will be enabled on the + device if FEATURE_TELEPHONY + (@see https: //developer.android.com/reference/android/content/pm/PackageManager#FEATURE_TELEPHONY) + is also supported. --> + <bool name="config_enableTelephonyTimeZoneDetection" translatable="false">true</bool> + <!-- Whether the time zone detection logic supports fall back from geolocation suggestions to telephony suggestions temporarily in certain circumstances. Reduces time zone detection latency during some scenarios like air travel. Only useful when both geolocation and - telephony time zone detection are supported on a device. + telephony time zone detection are supported and enabled on a device. See com.android.server.timezonedetector.TimeZoneDetectorStrategy for more information. --> <bool name="config_supportTelephonyTimeZoneFallback" translatable="false">true</bool> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 4f19c277f3a6..bf2bc26a5a8c 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2314,6 +2314,7 @@ <java-symbol type="string" name="config_primaryLocationTimeZoneProviderPackageName" /> <java-symbol type="bool" name="config_enableSecondaryLocationTimeZoneProvider" /> <java-symbol type="string" name="config_secondaryLocationTimeZoneProviderPackageName" /> + <java-symbol type="bool" name="config_enableTelephonyTimeZoneDetection" /> <java-symbol type="bool" name="config_supportTelephonyTimeZoneFallback" /> <java-symbol type="bool" name="config_autoResetAirplaneMode" /> <java-symbol type="string" name="config_notificationAccessConfirmationActivity" /> @@ -2338,6 +2339,7 @@ <java-symbol type="layout" name="notification_material_action_tombstone" /> <java-symbol type="layout" name="notification_template_material_base" /> <java-symbol type="layout" name="notification_template_material_heads_up_base" /> + <java-symbol type="layout" name="notification_template_material_compact_heads_up_base" /> <java-symbol type="layout" name="notification_template_material_big_base" /> <java-symbol type="layout" name="notification_template_material_big_picture" /> <java-symbol type="layout" name="notification_template_material_inbox" /> diff --git a/libs/hostgraphics/ADisplay.cpp b/libs/hostgraphics/ADisplay.cpp index 9cc1f40184e3..58fa08281a61 100644 --- a/libs/hostgraphics/ADisplay.cpp +++ b/libs/hostgraphics/ADisplay.cpp @@ -94,14 +94,14 @@ namespace android { int ADisplay_acquirePhysicalDisplays(ADisplay*** outDisplays) { // This is running on host, so there are no physical displays available. // Create 1 fake display instead. - DisplayImpl** const impls = reinterpret_cast<DisplayImpl**>( - malloc(sizeof(DisplayImpl*) + sizeof(DisplayImpl))); + DisplayImpl** const impls = + reinterpret_cast<DisplayImpl**>(malloc(sizeof(DisplayImpl*) + sizeof(DisplayImpl))); DisplayImpl* const displayData = reinterpret_cast<DisplayImpl*>(impls + 1); - displayData[0] = DisplayImpl{ADisplayType::DISPLAY_TYPE_INTERNAL, - ADataSpace::ADATASPACE_UNKNOWN, - AHardwareBuffer_Format::AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM, - DisplayConfigImpl()}; + displayData[0] = + DisplayImpl{ADisplayType::DISPLAY_TYPE_INTERNAL, ADataSpace::ADATASPACE_UNKNOWN, + AHardwareBuffer_Format::AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM, + DisplayConfigImpl()}; impls[0] = displayData; *outDisplays = reinterpret_cast<ADisplay**>(impls); return 1; diff --git a/libs/hostgraphics/Fence.cpp b/libs/hostgraphics/Fence.cpp index 9e54816651c4..4383bf02a00e 100644 --- a/libs/hostgraphics/Fence.cpp +++ b/libs/hostgraphics/Fence.cpp @@ -20,4 +20,4 @@ namespace android { const sp<Fence> Fence::NO_FENCE = sp<Fence>(new Fence); -} // namespace android
\ No newline at end of file +} // namespace android diff --git a/libs/hostgraphics/HostBufferQueue.cpp b/libs/hostgraphics/HostBufferQueue.cpp index b4fd5d9aab3d..7e14b88a47fa 100644 --- a/libs/hostgraphics/HostBufferQueue.cpp +++ b/libs/hostgraphics/HostBufferQueue.cpp @@ -15,21 +15,26 @@ */ #include <gui/BufferQueue.h> - #include <system/window.h> namespace android { class HostBufferQueue : public IGraphicBufferProducer, public IGraphicBufferConsumer { public: - HostBufferQueue() : mWidth(0), mHeight(0) { } + HostBufferQueue() : mWidth(0), mHeight(0) {} -// Consumer - virtual status_t setConsumerIsProtected(bool isProtected) { return OK; } + // Consumer + virtual status_t setConsumerIsProtected(bool isProtected) { + return OK; + } - virtual status_t detachBuffer(int slot) { return OK; } + virtual status_t detachBuffer(int slot) { + return OK; + } - virtual status_t getReleasedBuffers(uint64_t* slotMask) { return OK; } + virtual status_t getReleasedBuffers(uint64_t* slotMask) { + return OK; + } virtual status_t setDefaultBufferSize(uint32_t w, uint32_t h) { mWidth = w; @@ -38,26 +43,36 @@ public: return OK; } - virtual status_t setDefaultBufferFormat(PixelFormat defaultFormat) { return OK; } + virtual status_t setDefaultBufferFormat(PixelFormat defaultFormat) { + return OK; + } - virtual status_t setDefaultBufferDataSpace(android_dataspace defaultDataSpace) { return OK; } + virtual status_t setDefaultBufferDataSpace(android_dataspace defaultDataSpace) { + return OK; + } - virtual status_t discardFreeBuffers() { return OK; } + virtual status_t discardFreeBuffers() { + return OK; + } virtual status_t acquireBuffer(BufferItem* buffer, nsecs_t presentWhen, - uint64_t maxFrameNumber = 0) { + uint64_t maxFrameNumber = 0) { buffer->mGraphicBuffer = mBuffer; buffer->mSlot = 0; return OK; } - virtual status_t setMaxAcquiredBufferCount(int maxAcquiredBuffers) { return OK; } + virtual status_t setMaxAcquiredBufferCount(int maxAcquiredBuffers) { + return OK; + } - virtual status_t setConsumerUsageBits(uint64_t usage) { return OK; } + virtual status_t setConsumerUsageBits(uint64_t usage) { + return OK; + } -// Producer + // Producer virtual int query(int what, int* value) { - switch(what) { + switch (what) { case NATIVE_WINDOW_WIDTH: *value = mWidth; break; @@ -83,8 +98,7 @@ private: }; void BufferQueue::createBufferQueue(sp<IGraphicBufferProducer>* outProducer, - sp<IGraphicBufferConsumer>* outConsumer) { - + sp<IGraphicBufferConsumer>* outConsumer) { sp<HostBufferQueue> obj(new HostBufferQueue()); *outProducer = obj; diff --git a/libs/hostgraphics/PublicFormat.cpp b/libs/hostgraphics/PublicFormat.cpp index af6d2738c801..2a2eec63467c 100644 --- a/libs/hostgraphics/PublicFormat.cpp +++ b/libs/hostgraphics/PublicFormat.cpp @@ -30,4 +30,4 @@ PublicFormat mapHalFormatDataspaceToPublicFormat(int format, android_dataspace d return static_cast<PublicFormat>(format); } -} // namespace android
\ No newline at end of file +} // namespace android diff --git a/libs/hostgraphics/gui/BufferItem.h b/libs/hostgraphics/gui/BufferItem.h index 01409e19c715..e95a9231dfaf 100644 --- a/libs/hostgraphics/gui/BufferItem.h +++ b/libs/hostgraphics/gui/BufferItem.h @@ -17,16 +17,15 @@ #ifndef ANDROID_GUI_BUFFERITEM_H #define ANDROID_GUI_BUFFERITEM_H +#include <system/graphics.h> #include <ui/Fence.h> #include <ui/Rect.h> - -#include <system/graphics.h> - #include <utils/StrongPointer.h> namespace android { class Fence; + class GraphicBuffer; // The only thing we need here for layoutlib is mGraphicBuffer. The rest of the fields are added @@ -37,6 +36,7 @@ public: enum { INVALID_BUFFER_SLOT = -1 }; BufferItem() : mGraphicBuffer(nullptr), mFence(Fence::NO_FENCE) {} + ~BufferItem() {} sp<GraphicBuffer> mGraphicBuffer; @@ -60,6 +60,6 @@ public: bool mTransformToDisplayInverse; }; -} +} // namespace android #endif // ANDROID_GUI_BUFFERITEM_H diff --git a/libs/hostgraphics/gui/BufferItemConsumer.h b/libs/hostgraphics/gui/BufferItemConsumer.h index 707b313eb102..c25941151800 100644 --- a/libs/hostgraphics/gui/BufferItemConsumer.h +++ b/libs/hostgraphics/gui/BufferItemConsumer.h @@ -17,32 +17,30 @@ #ifndef ANDROID_GUI_BUFFERITEMCONSUMER_H #define ANDROID_GUI_BUFFERITEMCONSUMER_H -#include <utils/RefBase.h> - #include <gui/ConsumerBase.h> #include <gui/IGraphicBufferConsumer.h> +#include <utils/RefBase.h> namespace android { class BufferItemConsumer : public ConsumerBase { public: - BufferItemConsumer( - const sp<IGraphicBufferConsumer>& consumer, - uint64_t consumerUsage, - int bufferCount, - bool controlledByApp) : mConsumer(consumer) { - } + BufferItemConsumer(const sp<IGraphicBufferConsumer>& consumer, uint64_t consumerUsage, + int bufferCount, bool controlledByApp) + : mConsumer(consumer) {} - status_t acquireBuffer(BufferItem *item, nsecs_t presentWhen, bool waitForFence = true) { + status_t acquireBuffer(BufferItem* item, nsecs_t presentWhen, bool waitForFence = true) { return mConsumer->acquireBuffer(item, presentWhen, 0); } - status_t releaseBuffer( - const BufferItem &item, const sp<Fence>& releaseFence = Fence::NO_FENCE) { return OK; } + status_t releaseBuffer(const BufferItem& item, + const sp<Fence>& releaseFence = Fence::NO_FENCE) { + return OK; + } - void setName(const String8& name) { } + void setName(const String8& name) {} - void setFrameAvailableListener(const wp<FrameAvailableListener>& listener) { } + void setFrameAvailableListener(const wp<FrameAvailableListener>& listener) {} status_t setDefaultBufferSize(uint32_t width, uint32_t height) { return mConsumer->setDefaultBufferSize(width, height); @@ -56,16 +54,23 @@ public: return mConsumer->setDefaultBufferDataSpace(defaultDataSpace); } - void abandon() { } + void abandon() {} - status_t detachBuffer(int slot) { return OK; } + status_t detachBuffer(int slot) { + return OK; + } + + status_t discardFreeBuffers() { + return OK; + } - status_t discardFreeBuffers() { return OK; } + void freeBufferLocked(int slotIndex) {} - void freeBufferLocked(int slotIndex) { } + status_t addReleaseFenceLocked(int slot, const sp<GraphicBuffer> graphicBuffer, + const sp<Fence>& fence) { + return OK; + } - status_t addReleaseFenceLocked( - int slot, const sp<GraphicBuffer> graphicBuffer, const sp<Fence>& fence) { return OK; } private: sp<IGraphicBufferConsumer> mConsumer; }; diff --git a/libs/hostgraphics/gui/BufferQueue.h b/libs/hostgraphics/gui/BufferQueue.h index aa3e7268e11c..67a8c00fd267 100644 --- a/libs/hostgraphics/gui/BufferQueue.h +++ b/libs/hostgraphics/gui/BufferQueue.h @@ -29,7 +29,7 @@ public: enum { NO_BUFFER_AVAILABLE = IGraphicBufferConsumer::NO_BUFFER_AVAILABLE }; static void createBufferQueue(sp<IGraphicBufferProducer>* outProducer, - sp<IGraphicBufferConsumer>* outConsumer); + sp<IGraphicBufferConsumer>* outConsumer); }; } // namespace android diff --git a/libs/hostgraphics/gui/ConsumerBase.h b/libs/hostgraphics/gui/ConsumerBase.h index 9002953c0848..7f7309e8a3a8 100644 --- a/libs/hostgraphics/gui/ConsumerBase.h +++ b/libs/hostgraphics/gui/ConsumerBase.h @@ -18,7 +18,6 @@ #define ANDROID_GUI_CONSUMERBASE_H #include <gui/BufferItem.h> - #include <utils/RefBase.h> namespace android { @@ -28,10 +27,11 @@ public: struct FrameAvailableListener : public virtual RefBase { // See IConsumerListener::onFrame{Available,Replaced} virtual void onFrameAvailable(const BufferItem& item) = 0; + virtual void onFrameReplaced(const BufferItem& /* item */) {} }; }; } // namespace android -#endif // ANDROID_GUI_CONSUMERBASE_H
\ No newline at end of file +#endif // ANDROID_GUI_CONSUMERBASE_H diff --git a/libs/hostgraphics/gui/IGraphicBufferConsumer.h b/libs/hostgraphics/gui/IGraphicBufferConsumer.h index 9eb67b218800..14ac4fe71cc8 100644 --- a/libs/hostgraphics/gui/IGraphicBufferConsumer.h +++ b/libs/hostgraphics/gui/IGraphicBufferConsumer.h @@ -16,16 +16,16 @@ #pragma once -#include <utils/RefBase.h> - #include <ui/PixelFormat.h> - #include <utils/Errors.h> +#include <utils/RefBase.h> namespace android { class BufferItem; + class Fence; + class GraphicBuffer; class IGraphicBufferConsumer : virtual public RefBase { @@ -62,4 +62,4 @@ public: virtual status_t discardFreeBuffers() = 0; }; -} // namespace android
\ No newline at end of file +} // namespace android diff --git a/libs/hostgraphics/gui/IGraphicBufferProducer.h b/libs/hostgraphics/gui/IGraphicBufferProducer.h index 1447a4fd3695..8fd8590d10d7 100644 --- a/libs/hostgraphics/gui/IGraphicBufferProducer.h +++ b/libs/hostgraphics/gui/IGraphicBufferProducer.h @@ -17,9 +17,8 @@ #ifndef ANDROID_GUI_IGRAPHICBUFFERPRODUCER_H #define ANDROID_GUI_IGRAPHICBUFFERPRODUCER_H -#include <utils/RefBase.h> - #include <ui/GraphicBuffer.h> +#include <utils/RefBase.h> namespace android { diff --git a/libs/hostgraphics/gui/Surface.h b/libs/hostgraphics/gui/Surface.h index 0d810370c47c..2774f89cb54c 100644 --- a/libs/hostgraphics/gui/Surface.h +++ b/libs/hostgraphics/gui/Surface.h @@ -28,17 +28,25 @@ namespace android { class Surface : public ANativeObjectBase<ANativeWindow, Surface, RefBase> { public: explicit Surface(const sp<IGraphicBufferProducer>& bufferProducer, bool controlledByApp = false) - : mBufferProducer(bufferProducer) { + : mBufferProducer(bufferProducer) { ANativeWindow::perform = hook_perform; ANativeWindow::dequeueBuffer = hook_dequeueBuffer; ANativeWindow::query = hook_query; } - static bool isValid(const sp<Surface>& surface) { return surface != nullptr; } + + static bool isValid(const sp<Surface>& surface) { + return surface != nullptr; + } + void allocateBuffers() {} - uint64_t getNextFrameNumber() const { return 0; } + uint64_t getNextFrameNumber() const { + return 0; + } - int setScalingMode(int mode) { return 0; } + int setScalingMode(int mode) { + return 0; + } virtual int disconnect(int api, IGraphicBufferProducer::DisconnectMode mode = @@ -50,16 +58,28 @@ public: // TODO: implement this return 0; } - virtual int unlockAndPost() { return 0; } - virtual int query(int what, int* value) const { return mBufferProducer->query(what, value); } - status_t setDequeueTimeout(nsecs_t timeout) { return OK; } + virtual int unlockAndPost() { + return 0; + } - nsecs_t getLastDequeueStartTime() const { return 0; } + virtual int query(int what, int* value) const { + return mBufferProducer->query(what, value); + } + + status_t setDequeueTimeout(nsecs_t timeout) { + return OK; + } + + nsecs_t getLastDequeueStartTime() const { + return 0; + } virtual void destroy() {} - int getBuffersDataSpace() { return 0; } + int getBuffersDataSpace() { + return 0; + } protected: virtual ~Surface() {} @@ -89,15 +109,31 @@ protected: *buffer = mBuffer.get(); return OK; } - virtual int cancelBuffer(ANativeWindowBuffer* buffer, int fenceFd) { return 0; } - virtual int queueBuffer(ANativeWindowBuffer* buffer, int fenceFd) { return 0; } - virtual int perform(int operation, va_list args) { return 0; } - virtual int setSwapInterval(int interval) { return 0; } - virtual int setBufferCount(int bufferCount) { return 0; } + + virtual int cancelBuffer(ANativeWindowBuffer* buffer, int fenceFd) { + return 0; + } + + virtual int queueBuffer(ANativeWindowBuffer* buffer, int fenceFd) { + return 0; + } + + virtual int perform(int operation, va_list args) { + return 0; + } + + virtual int setSwapInterval(int interval) { + return 0; + } + + virtual int setBufferCount(int bufferCount) { + return 0; + } private: // can't be copied Surface& operator=(const Surface& rhs); + Surface(const Surface& rhs); const sp<IGraphicBufferProducer> mBufferProducer; @@ -106,4 +142,4 @@ private: } // namespace android -#endif // ANDROID_GUI_SURFACE_H +#endif // ANDROID_GUI_SURFACE_H diff --git a/libs/hostgraphics/ui/Fence.h b/libs/hostgraphics/ui/Fence.h index 04d535c3a211..187c3116f61c 100644 --- a/libs/hostgraphics/ui/Fence.h +++ b/libs/hostgraphics/ui/Fence.h @@ -17,8 +17,8 @@ #ifndef ANDROID_FENCE_H #define ANDROID_FENCE_H -#include <utils/String8.h> #include <utils/RefBase.h> +#include <utils/String8.h> typedef int64_t nsecs_t; @@ -26,11 +26,14 @@ namespace android { class Fence : public LightRefBase<Fence> { public: - Fence() { } - Fence(int) { } + Fence() {} + + Fence(int) {} + static const sp<Fence> NO_FENCE; static constexpr nsecs_t SIGNAL_TIME_PENDING = INT64_MAX; static constexpr nsecs_t SIGNAL_TIME_INVALID = -1; + static sp<Fence> merge(const char* name, const sp<Fence>& f1, const sp<Fence>& f2) { return NO_FENCE; } @@ -40,16 +43,22 @@ public: } enum class Status { - Invalid, // Fence is invalid - Unsignaled, // Fence is valid but has not yet signaled - Signaled, // Fence is valid and has signaled + Invalid, // Fence is invalid + Unsignaled, // Fence is valid but has not yet signaled + Signaled, // Fence is valid and has signaled }; - status_t wait(int timeout) { return OK; } + status_t wait(int timeout) { + return OK; + } - status_t waitForever(const char* logname) { return OK; } + status_t waitForever(const char* logname) { + return OK; + } - int dup() const { return 0; } + int dup() const { + return 0; + } inline Status getStatus() { // The sync_wait call underlying wait() has been measured to be diff --git a/libs/hostgraphics/ui/GraphicBuffer.h b/libs/hostgraphics/ui/GraphicBuffer.h index eec9b235cd6a..cda45e4660ca 100644 --- a/libs/hostgraphics/ui/GraphicBuffer.h +++ b/libs/hostgraphics/ui/GraphicBuffer.h @@ -19,34 +19,51 @@ #include <stdint.h> #include <sys/types.h> - -#include <vector> - #include <ui/ANativeObjectBase.h> #include <ui/PixelFormat.h> #include <ui/Rect.h> #include <utils/RefBase.h> +#include <vector> + namespace android { class GraphicBuffer : public ANativeObjectBase<ANativeWindowBuffer, GraphicBuffer, RefBase> { public: GraphicBuffer(uint32_t w, uint32_t h) { - data.resize(w*h); + data.resize(w * h); reserved[0] = data.data(); width = w; height = h; } - uint32_t getWidth() const { return static_cast<uint32_t>(width); } - uint32_t getHeight() const { return static_cast<uint32_t>(height); } - uint32_t getStride() const { return static_cast<uint32_t>(width); } - uint64_t getUsage() const { return 0; } - PixelFormat getPixelFormat() const { return PIXEL_FORMAT_RGBA_8888; } - Rect getBounds() const { return Rect(width, height); } + uint32_t getWidth() const { + return static_cast<uint32_t>(width); + } + + uint32_t getHeight() const { + return static_cast<uint32_t>(height); + } + + uint32_t getStride() const { + return static_cast<uint32_t>(width); + } + + uint64_t getUsage() const { + return 0; + } + + PixelFormat getPixelFormat() const { + return PIXEL_FORMAT_RGBA_8888; + } + + Rect getBounds() const { + return Rect(width, height); + } - status_t lockAsyncYCbCr(uint32_t inUsage, const Rect& rect, - android_ycbcr *ycbcr, int fenceFd) { return OK; } + status_t lockAsyncYCbCr(uint32_t inUsage, const Rect& rect, android_ycbcr* ycbcr, int fenceFd) { + return OK; + } status_t lockAsync(uint32_t inUsage, const Rect& rect, void** vaddr, int fenceFd, int32_t* outBytesPerPixel = nullptr, int32_t* outBytesPerStride = nullptr) { @@ -54,7 +71,9 @@ public: return OK; } - status_t unlockAsync(int *fenceFd) { return OK; } + status_t unlockAsync(int* fenceFd) { + return OK; + } private: std::vector<uint32_t> data; diff --git a/media/java/android/media/RingtoneManager.java b/media/java/android/media/RingtoneManager.java index 3432b3f5995a..86113df3a091 100644 --- a/media/java/android/media/RingtoneManager.java +++ b/media/java/android/media/RingtoneManager.java @@ -16,7 +16,6 @@ package android.media; -import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; @@ -573,12 +572,13 @@ public class RingtoneManager { FileUtils.closeQuietly(cursor); throw new FileNotFoundException("No item found for " + baseUri); } else if (cursor.getCount() > 1) { + int resultCount = cursor.getCount(); // Find more than 1 result. // We are not sure which one is the right ringtone file so just abandon this case. FileUtils.closeQuietly(cursor); throw new FileNotFoundException( "Find multiple ringtone candidates by title+ringtone_type query: count: " - + cursor.getCount()); + + resultCount); } if (cursor.moveToFirst()) { ringtoneUri = ContentUris.withAppendedId(baseUri, cursor.getLong(0)); diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java index 29867d924c78..25fecace903a 100644 --- a/nfc/java/android/nfc/NfcAdapter.java +++ b/nfc/java/android/nfc/NfcAdapter.java @@ -1250,7 +1250,11 @@ public final class NfcAdapter { /** * Controls whether the NFC adapter will allow transactions to proceed or be in observe mode * and simply observe and notify the APDU service of polling loop frames. See - * {@link #isObserveModeSupported()} for a description of observe mode. + * {@link #isObserveModeSupported()} for a description of observe mode. Only the package of the + * currently preferred service (the service set as preferred by the current foreground + * application via {@link CardEmulation#setPreferredService(Activity, ComponentName)} or the + * current Default Wallet Role Holder {@link android.app.role.RoleManager#ROLE_WALLET}), + * otherwise a call to this method will fail and return false. * * @param enabled false disables observe mode to allow the transaction to proceed while true * enables observe mode and does not allow transactions to proceed. diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index 73c96d98355b..151581134009 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -201,9 +201,9 @@ <!-- Connected devices settings. Message when Bluetooth is connected and active, showing remote device status and battery level for untethered headset. [CHAR LIMIT=NONE] --> <string name="bluetooth_active_battery_level_untethered">Active. L: <xliff:g id="battery_level_as_percentage" example="25%">%1$s</xliff:g>, R: <xliff:g id="battery_level_as_percentage" example="25%">%2$s</xliff:g> battery.</string> <!-- Connected devices settings. Message when Bluetooth is connected and active, showing remote device status and battery level for the left part of the untethered headset. [CHAR LIMIT=NONE] --> - <string name="bluetooth_active_battery_level_untethered_left">Active. L: <xliff:g id="battery_level_as_percentage" example="25%">%1$s</xliff:g> battery</string> + <string name="bluetooth_active_battery_level_untethered_left">Active. L: <xliff:g id="battery_level_as_percentage" example="25%">%1$s</xliff:g> battery.</string> <!-- Connected devices settings. Message when Bluetooth is connected and active, showing remote device status and battery level for the right part of the untethered headset. [CHAR LIMIT=NONE] --> - <string name="bluetooth_active_battery_level_untethered_right">Active. R: <xliff:g id="battery_level_as_percentage" example="25%">%1$s</xliff:g> battery</string> + <string name="bluetooth_active_battery_level_untethered_right">Active. R: <xliff:g id="battery_level_as_percentage" example="25%">%1$s</xliff:g> battery.</string> <!-- Connected devices settings. Message when Bluetooth is connected but not in use, showing remote device battery level. [CHAR LIMIT=NONE] --> <string name="bluetooth_battery_level"><xliff:g id="battery_level_as_percentage">%1$s</xliff:g> battery</string> <!-- Connected devices settings. Message on TV when Bluetooth is connected but not in use, showing remote device battery level. [CHAR LIMIT=NONE] --> @@ -220,11 +220,11 @@ <string name="bluetooth_saved_device">Saved</string> <!-- Connected device settings. Message when the left-side hearing aid device is active. [CHAR LIMIT=NONE] --> - <string name="bluetooth_hearing_aid_left_active">Active, left only</string> + <string name="bluetooth_hearing_aid_left_active">Active (left only)</string> <!-- Connected device settings. Message when the right-side hearing aid device is active. [CHAR LIMIT=NONE] --> - <string name="bluetooth_hearing_aid_right_active">Active, right only</string> + <string name="bluetooth_hearing_aid_right_active">Active (right only)</string> <!-- Connected device settings. Message when the left-side and right-side hearing aids device are active. [CHAR LIMIT=NONE] --> - <string name="bluetooth_hearing_aid_left_and_right_active">Active, left and right</string> + <string name="bluetooth_hearing_aid_left_and_right_active">Active (left and right)</string> <!-- Connected devices settings. Message when Bluetooth is connected and active for media only, showing remote device status and battery level. [CHAR LIMIT=NONE] --> <string name="bluetooth_active_media_only_battery_level">Active (media only). <xliff:g id="battery_level_as_percentage">%1$s</xliff:g> battery.</string> diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java index 06c41cb7cbc7..fd9a008ee078 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java @@ -2131,7 +2131,13 @@ public class ApplicationsState { public boolean filterApp(AppEntry entry) { return !AppUtils.isInstant(entry.info) && hasFlag(entry.info.privateFlags, - ApplicationInfo.PRIVATE_FLAG_HAS_DOMAIN_URLS); + ApplicationInfo.PRIVATE_FLAG_HAS_DOMAIN_URLS) + && !entry.hideInQuietMode; + } + + @Override + public void refreshAppEntryOnRebuild(@NonNull AppEntry appEntry, boolean hideInQuietMode) { + appEntry.hideInQuietMode = hideInQuietMode; } }; diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java index c13c4936927c..b356f542f480 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java @@ -697,7 +697,7 @@ public class CachedBluetoothDeviceTest { // Set device as Active for Hearing Aid and test connection state summary mCachedDevice.setHearingAidInfo(getLeftAshaHearingAidInfo()); mCachedDevice.onActiveDeviceChanged(true, BluetoothProfile.HEARING_AID); - assertThat(mCachedDevice.getConnectionSummary()).isEqualTo("Active, left only"); + assertThat(mCachedDevice.getConnectionSummary()).isEqualTo("Active (left only)"); // Set Hearing Aid profile to be disconnected and test connection state summary mCachedDevice.onActiveDeviceChanged(false, BluetoothProfile.HEARING_AID); @@ -717,7 +717,7 @@ public class CachedBluetoothDeviceTest { mCachedDevice.setHearingAidInfo(getLeftAshaHearingAidInfo()); mCachedDevice.onActiveDeviceChanged(true, BluetoothProfile.HEARING_AID); assertThat(mCachedDevice.getTvConnectionSummary().toString()).isEqualTo( - "Active, left only"); + "Active (left only)"); // Set Hearing Aid profile to be disconnected and test connection state summary mCachedDevice.onActiveDeviceChanged(false, BluetoothProfile.HEARING_AID); @@ -794,7 +794,7 @@ public class CachedBluetoothDeviceTest { // Act & Assert: // Get "Active" result without Battery Level. - assertThat(mCachedDevice.getConnectionSummary()).isEqualTo("Active, right only"); + assertThat(mCachedDevice.getConnectionSummary()).isEqualTo("Active (right only)"); } @Test @@ -810,7 +810,7 @@ public class CachedBluetoothDeviceTest { // Act & Assert: // Get "Active" result without Battery Level. assertThat(mCachedDevice.getTvConnectionSummary().toString()).isEqualTo( - "Active, right only"); + "Active (right only)"); } @Test @@ -828,7 +828,7 @@ public class CachedBluetoothDeviceTest { // Act & Assert: // Get "Active" result without Battery Level. - assertThat(mCachedDevice.getConnectionSummary()).isEqualTo("Active, left and right"); + assertThat(mCachedDevice.getConnectionSummary()).isEqualTo("Active (left and right)"); } @Test @@ -847,7 +847,7 @@ public class CachedBluetoothDeviceTest { // Act & Assert: // Get "Active" result without Battery Level. assertThat(mCachedDevice.getTvConnectionSummary().toString()) - .isEqualTo("Active, left and right"); + .isEqualTo("Active (left and right)"); } @Test @@ -894,7 +894,7 @@ public class CachedBluetoothDeviceTest { // Set device as Active for LE Audio and test connection state summary mCachedDevice.setHearingAidInfo(getLeftLeAudioHearingAidInfo()); mCachedDevice.onActiveDeviceChanged(true, BluetoothProfile.LE_AUDIO); - assertThat(mCachedDevice.getConnectionSummary()).isEqualTo("Active, left only"); + assertThat(mCachedDevice.getConnectionSummary()).isEqualTo("Active (left only)"); // Set LE Audio profile to be disconnected and test connection state summary mCachedDevice.onActiveDeviceChanged(false, BluetoothProfile.LE_AUDIO); @@ -915,7 +915,7 @@ public class CachedBluetoothDeviceTest { mCachedDevice.setHearingAidInfo(getLeftLeAudioHearingAidInfo()); mCachedDevice.onActiveDeviceChanged(true, BluetoothProfile.LE_AUDIO); assertThat(mCachedDevice.getTvConnectionSummary().toString()).isEqualTo( - "Active, left only"); + "Active (left only)"); // Set LE Audio profile to be disconnected and test connection state summary mCachedDevice.onActiveDeviceChanged(false, BluetoothProfile.LE_AUDIO); diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 75c0cec1ad72..9d02074cbe35 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -273,6 +273,7 @@ public class SecureSettings { Settings.Secure.AUDIO_DEVICE_INVENTORY, Settings.Secure.SCREEN_RESOLUTION_MODE, Settings.Secure.ACCESSIBILITY_FLOATING_MENU_TARGETS, - Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_SATURATION_LEVEL + Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_SATURATION_LEVEL, + Settings.Secure.CHARGE_OPTIMIZATION_MODE }; } diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index 8faf917cadc2..6cb9d50273d8 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -432,5 +432,6 @@ public class SecureSettingsValidators { Secure.RESOLUTION_MODE_UNKNOWN, Secure.RESOLUTION_MODE_FULL)); VALIDATORS.put(Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_SATURATION_LEVEL, new InclusiveIntegerRangeValidator(0, 10)); + VALIDATORS.put(Secure.CHARGE_OPTIMIZATION_MODE, new InclusiveIntegerRangeValidator(0, 10)); } } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java index fa9b279581c9..52e9b6d5aa46 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java @@ -2745,6 +2745,9 @@ class SettingsProtoDumpUtil { dumpSetting(s, p, Settings.Secure.ZEN_SETTINGS_SUGGESTION_VIEWED, SecureSettingsProto.Zen.SETTINGS_SUGGESTION_VIEWED); + dumpSetting(s, p, + Settings.Secure.CHARGE_OPTIMIZATION_MODE, + SecureSettingsProto.CHARGE_OPTIMIZATION_MODE); p.end(zenToken); // Please insert new settings using the same order as in SecureSettingsProto. diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java index 68167e1598e0..68bc96d17a84 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java @@ -167,7 +167,7 @@ final class SettingsState { private static final String STORAGE_MIGRATION_FLAG = "core_experiments_team_internal/com.android.providers.settings.storage_test_mission_1"; private static final String STORAGE_MIGRATION_MARKER_FILE = - "/metadata/aconfig/storage_test_mission_1"; + "/metadata/aconfig_test_missions/mission_1"; /** * This tag is applied to all aconfig default value-loaded flags. diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/modifier/BurnInModifiers.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/modifier/BurnInModifiers.kt index 238a230ff013..c109e517a581 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/modifier/BurnInModifiers.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/modifier/BurnInModifiers.kt @@ -61,11 +61,10 @@ fun Modifier.burnInAware( val scale = when { scaleViewModel.scaleClockOnly && isClock -> scaleViewModel.scale - !scaleViewModel.scaleClockOnly -> scaleViewModel.scale else -> 1f } - this.translationX = translationX + this.translationX = if (isClock) 0F else translationX this.translationY = translationY this.alpha = alpha this.scaleX = scale diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt index 7095875447b4..09ec76df3aea 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt @@ -88,7 +88,7 @@ constructor( } @Composable - fun SceneScope.LargeClock(modifier: Modifier = Modifier) { + fun SceneScope.LargeClock(burnInParams: BurnInParameters, modifier: Modifier = Modifier) { val currentClock by viewModel.currentClock.collectAsState() if (currentClock?.largeClock?.view == null) { return @@ -129,7 +129,13 @@ constructor( update = { it.ensureClockViewExists(checkNotNull(currentClock).largeClock.view) }, - modifier = Modifier.fillMaxSize() + modifier = + Modifier.fillMaxSize() + .burnInAware( + viewModel = aodBurnInViewModel, + params = burnInParams, + isClock = true + ) ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt index 0934b20562b4..e0540bf3175f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt @@ -149,6 +149,7 @@ constructor( } with(clockSection) { LargeClock( + burnInParams = burnIn.parameters, modifier = Modifier.fillMaxSize().thenIf(shouldOffSetClockToOneHalf) { // If we do not have a custom position animation, we want @@ -179,7 +180,12 @@ constructor( Column(modifier = modifier) { val currentClock = currentClockState.value ?: return@Column - with(weatherClockSection) { Time(clock = currentClock, modifier = Modifier) } + with(weatherClockSection) { + Time( + clock = currentClock, + burnInParams = burnIn.parameters, + ) + } val density = LocalDensity.current val context = LocalContext.current @@ -193,7 +199,12 @@ constructor( ) ) } - with(weatherClockSection) { LargeClockSectionBelowSmartspace(clock = currentClock) } + with(weatherClockSection) { + LargeClockSectionBelowSmartspace( + burnInParams = burnIn.parameters, + clock = currentClock, + ) + } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/WeatherClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/WeatherClockSection.kt index a7bb308ada5c..9a82da251b7f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/WeatherClockSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/WeatherClockSection.kt @@ -35,7 +35,9 @@ import com.android.compose.animation.scene.SceneScope import com.android.compose.modifiers.padding import com.android.systemui.customization.R as customizationR import com.android.systemui.keyguard.ui.composable.blueprint.WeatherClockElementKeys +import com.android.systemui.keyguard.ui.composable.modifier.burnInAware import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel +import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.plugins.clocks.ClockController import javax.inject.Inject @@ -50,19 +52,19 @@ constructor( @Composable fun SceneScope.Time( clock: ClockController, - modifier: Modifier = Modifier, + burnInParams: BurnInParameters, ) { Row( modifier = Modifier.padding( - horizontal = dimensionResource(customizationR.dimen.clock_padding_start) - ) + horizontal = dimensionResource(customizationR.dimen.clock_padding_start) + ) + .burnInAware(aodBurnInViewModel, burnInParams, isClock = true) ) { WeatherElement( weatherClockElementViewId = customizationR.id.weather_clock_time, clock = clock, elementKey = WeatherClockElementKeys.timeElementKey, - modifier = modifier, ) } } @@ -124,7 +126,7 @@ constructor( weatherClockElementViewId: Int, clock: ClockController, elementKey: ElementKey, - modifier: Modifier + modifier: Modifier = Modifier, ) { MovableElement(key = elementKey, modifier) { content { @@ -150,6 +152,7 @@ constructor( @Composable fun SceneScope.LargeClockSectionBelowSmartspace( + burnInParams: BurnInParameters, clock: ClockController, ) { Row( @@ -158,6 +161,7 @@ constructor( .padding( horizontal = dimensionResource(customizationR.dimen.clock_padding_start) ) + .burnInAware(aodBurnInViewModel, burnInParams, isClock = true) ) { Date(clock = clock, modifier = Modifier.wrapContentSize()) Box( diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt index da07f6d12a67..6b289f3c66a3 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt @@ -190,4 +190,4 @@ private class OneOffTransition( // TODO(b/290184746): Compute a good default visibility threshold that depends on the layout size // and screen density. -private const val ProgressVisibilityThreshold = 1e-3f +internal const val ProgressVisibilityThreshold = 1e-3f diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt index 32cebd1fd7e2..a595c665de7c 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt @@ -682,6 +682,17 @@ private class SwipeTransition( } if (isBouncing) { bouncingScene = targetScene + + // Immediately stop this transition if we are bouncing on a + // scene that does not bounce. + val overscrollSpec = currentOverscrollSpec + if ( + overscrollSpec != null && + overscrollSpec.transformationSpec.transformations + .isEmpty() + ) { + snapToScene(targetScene) + } } } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt index 7d43ca8ddd37..4273b4fbf7b8 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.isSpecified import androidx.compose.ui.geometry.isUnspecified import androidx.compose.ui.geometry.lerp import androidx.compose.ui.graphics.CompositingStrategy @@ -55,9 +56,15 @@ import kotlinx.coroutines.launch internal class Element(val key: ElementKey) { /** The mapping between a scene and the state this element has in that scene, if any. */ // TODO(b/316901148): Make this a normal map instead once we can make sure that new transitions - // are first seen by composition then layout/drawing code. See 316901148#comment2 for details. + // are first seen by composition then layout/drawing code. See b/316901148#comment2 for details. val sceneStates = SnapshotStateMap<SceneKey, SceneState>() + /** + * The last transition that was used when computing the state (size, position and alpha) of this + * element in any scene, or `null` if it was last laid out when idle. + */ + var lastTransition: TransitionState.Transition? = null + override fun toString(): String { return "Element(key=$key)" } @@ -65,9 +72,33 @@ internal class Element(val key: ElementKey) { /** The last and target state of this element in a given scene. */ @Stable class SceneState(val scene: SceneKey) { + /** + * The *target* state of this element in this scene, i.e. the state of this element when we + * are idle on this scene. + */ var targetSize by mutableStateOf(SizeUnspecified) var targetOffset by mutableStateOf(Offset.Unspecified) + /** The last state this element had in this scene. */ + var lastOffset = Offset.Unspecified + var lastScale = Scale.Unspecified + var lastAlpha = AlphaUnspecified + + /** The state of this element in this scene right before the last interruption (if any). */ + var offsetBeforeInterruption = Offset.Unspecified + var scaleBeforeInterruption = Scale.Unspecified + var alphaBeforeInterruption = AlphaUnspecified + + /** + * The delta values to add to this element state to have smoother interruptions. These + * should be multiplied by the + * [current interruption progress][TransitionState.Transition.interruptionProgress] so that + * they nicely animate from their values down to 0. + */ + var offsetInterruptionDelta = Offset.Zero + var scaleInterruptionDelta = Scale.Zero + var alphaInterruptionDelta = 0f + /** * The attached [ElementNode] a Modifier.element() for a given element and scene. During * composition, this set could have 0 to 2 elements. After composition and after all @@ -78,12 +109,15 @@ internal class Element(val key: ElementKey) { companion object { val SizeUnspecified = IntSize(Int.MAX_VALUE, Int.MAX_VALUE) + val AlphaUnspecified = Float.MAX_VALUE } } data class Scale(val scaleX: Float, val scaleY: Float, val pivot: Offset = Offset.Unspecified) { companion object { val Default = Scale(1f, 1f, Offset.Unspecified) + val Zero = Scale(0f, 0f, Offset.Zero) + val Unspecified = Scale(Float.MAX_VALUE, Float.MAX_VALUE, Offset.Unspecified) } } @@ -212,6 +246,10 @@ internal class ElementNode( val isOtherSceneOverscrolling = overscrollScene != null && overscrollScene != scene.key val isNotPartOfAnyOngoingTransitions = transitions.isNotEmpty() && transition == null if (isNotPartOfAnyOngoingTransitions || isOtherSceneOverscrolling) { + sceneState.lastOffset = Offset.Unspecified + sceneState.lastScale = Scale.Unspecified + sceneState.lastAlpha = Element.AlphaUnspecified + val placeable = measurable.measure(constraints) return layout(placeable.width, placeable.height) {} } @@ -233,7 +271,7 @@ internal class ElementNode( override fun ContentDrawScope.draw() { val transition = elementTransition(element, layoutImpl.state.currentTransitions) - val drawScale = getDrawScale(layoutImpl, scene, element, transition) + val drawScale = getDrawScale(layoutImpl, scene, element, transition, sceneState) if (drawScale == Scale.Default) { drawContent() } else { @@ -276,8 +314,116 @@ private fun elementTransition( element: Element, transitions: List<TransitionState.Transition>, ): TransitionState.Transition? { - return transitions.fastLastOrNull { transition -> - transition.fromScene in element.sceneStates || transition.toScene in element.sceneStates + val transition = + transitions.fastLastOrNull { transition -> + transition.fromScene in element.sceneStates || transition.toScene in element.sceneStates + } + + val previousTransition = element.lastTransition + element.lastTransition = transition + + if (transition != previousTransition && transition != null && previousTransition != null) { + // The previous transition was interrupted by another transition. + prepareInterruption(element) + } + + if (transition == null && previousTransition != null) { + // The transition was just finished. + element.sceneStates.values.forEach { sceneState -> + sceneState.offsetInterruptionDelta = Offset.Zero + sceneState.scaleInterruptionDelta = Scale.Zero + sceneState.alphaInterruptionDelta = 0f + } + } + + return transition +} + +private fun prepareInterruption(element: Element) { + // We look for the last unique state of this element so that we animate the delta with its + // future state. + val sceneStates = element.sceneStates.values + var lastUniqueState: Element.SceneState? = null + for (sceneState in sceneStates) { + val offset = sceneState.lastOffset + + // If the element was placed in this scene... + if (offset != Offset.Unspecified) { + // ... and it is the first (and potentially the only) scene where the element was + // placed, save the state for later. + if (lastUniqueState == null) { + lastUniqueState = sceneState + } else { + // The element was placed in multiple scenes: we abort the interruption for this + // element. + // TODO(b/290930950): Better support cases where a shared element animation is + // disabled and the same element is drawn/placed in multiple scenes at the same + // time. + lastUniqueState = null + break + } + } + } + + val lastOffset = lastUniqueState?.lastOffset ?: Offset.Unspecified + val lastScale = lastUniqueState?.lastScale ?: Scale.Unspecified + val lastAlpha = lastUniqueState?.lastAlpha ?: Element.AlphaUnspecified + + // Store the state of the element before the interruption and reset the deltas. + sceneStates.forEach { sceneState -> + sceneState.offsetBeforeInterruption = lastOffset + sceneState.scaleBeforeInterruption = lastScale + sceneState.alphaBeforeInterruption = lastAlpha + + sceneState.offsetInterruptionDelta = Offset.Zero + sceneState.scaleInterruptionDelta = Scale.Zero + sceneState.alphaInterruptionDelta = 0f + } +} + +/** + * Compute what [value] should be if we take the + * [interruption progress][TransitionState.Transition.interruptionProgress] of [transition] into + * account. + */ +private inline fun <T> computeInterruptedValue( + layoutImpl: SceneTransitionLayoutImpl, + transition: TransitionState.Transition?, + value: T, + unspecifiedValue: T, + zeroValue: T, + getValueBeforeInterruption: () -> T, + setValueBeforeInterruption: (T) -> Unit, + getInterruptionDelta: () -> T, + setInterruptionDelta: (T) -> Unit, + diff: (a: T, b: T) -> T, // a - b + add: (a: T, b: T, bProgress: Float) -> T, // a + (b * bProgress) +): T { + val valueBeforeInterruption = getValueBeforeInterruption() + + // If the value before the interruption is specified, it means that this is the first time we + // compute [value] right after an interruption. + if (valueBeforeInterruption != unspecifiedValue) { + // Compute and store the delta between the value before the interruption and the current + // value. + setInterruptionDelta(diff(valueBeforeInterruption, value)) + + // Reset the value before interruption now that we processed it. + setValueBeforeInterruption(unspecifiedValue) + } + + val delta = getInterruptionDelta() + return if (delta == zeroValue || transition == null) { + // There was no interruption or there is no transition: just return the value. + value + } else { + // Add `delta * interruptionProgress` to the value so that we animate to value. + val interruptionProgress = transition.interruptionProgress(layoutImpl) + if (interruptionProgress == 0f) { + value + } else { + add(value, delta, interruptionProgress) + } } } @@ -417,20 +563,47 @@ private fun elementAlpha( scene: Scene, element: Element, transition: TransitionState.Transition?, + sceneState: Element.SceneState, ): Float { - return computeValue( - layoutImpl, - scene, - element, - transition, - sceneValue = { 1f }, - transformation = { it.alpha }, - idleValue = 1f, - currentValue = { 1f }, - isSpecified = { true }, - ::lerp, - ) - .fastCoerceIn(0f, 1f) + val alpha = + computeValue( + layoutImpl, + scene, + element, + transition, + sceneValue = { 1f }, + transformation = { it.alpha }, + idleValue = 1f, + currentValue = { 1f }, + isSpecified = { true }, + ::lerp, + ) + .fastCoerceIn(0f, 1f) + + val interruptedAlpha = interruptedAlpha(layoutImpl, transition, sceneState, alpha) + sceneState.lastAlpha = interruptedAlpha + return interruptedAlpha +} + +private fun interruptedAlpha( + layoutImpl: SceneTransitionLayoutImpl, + transition: TransitionState.Transition?, + sceneState: Element.SceneState, + alpha: Float, +): Float { + return computeInterruptedValue( + layoutImpl, + transition, + value = alpha, + unspecifiedValue = Element.AlphaUnspecified, + zeroValue = 0f, + getValueBeforeInterruption = { sceneState.alphaBeforeInterruption }, + setValueBeforeInterruption = { sceneState.alphaBeforeInterruption = it }, + getInterruptionDelta = { sceneState.alphaInterruptionDelta }, + setInterruptionDelta = { sceneState.alphaInterruptionDelta = it }, + diff = { a, b -> a - b }, + add = { a, b, bProgress -> a + b * bProgress }, + ) } @OptIn(ExperimentalComposeUiApi::class) @@ -480,24 +653,70 @@ private fun ApproachMeasureScope.measure( ) } -private fun getDrawScale( +private fun ContentDrawScope.getDrawScale( layoutImpl: SceneTransitionLayoutImpl, scene: Scene, element: Element, transition: TransitionState.Transition?, + sceneState: Element.SceneState, ): Scale { - return computeValue( - layoutImpl, - scene, - element, - transition, - sceneValue = { Scale.Default }, - transformation = { it.drawScale }, - idleValue = Scale.Default, - currentValue = { Scale.Default }, - isSpecified = { true }, - ::lerp, - ) + val scale = + computeValue( + layoutImpl, + scene, + element, + transition, + sceneValue = { Scale.Default }, + transformation = { it.drawScale }, + idleValue = Scale.Default, + currentValue = { Scale.Default }, + isSpecified = { true }, + ::lerp, + ) + + fun Offset.specifiedOrCenter(): Offset { + return this.takeIf { isSpecified } ?: center + } + + val interruptedScale = + computeInterruptedValue( + layoutImpl, + transition, + value = scale, + unspecifiedValue = Scale.Unspecified, + zeroValue = Scale.Zero, + getValueBeforeInterruption = { sceneState.scaleBeforeInterruption }, + setValueBeforeInterruption = { sceneState.scaleBeforeInterruption = it }, + getInterruptionDelta = { sceneState.scaleInterruptionDelta }, + setInterruptionDelta = { sceneState.scaleInterruptionDelta = it }, + diff = { a, b -> + Scale( + scaleX = a.scaleX - b.scaleX, + scaleY = a.scaleY - b.scaleY, + pivot = + if (a.pivot.isUnspecified && b.pivot.isUnspecified) { + Offset.Unspecified + } else { + a.pivot.specifiedOrCenter() - b.pivot.specifiedOrCenter() + } + ) + }, + add = { a, b, bProgress -> + Scale( + scaleX = a.scaleX + b.scaleX * bProgress, + scaleY = a.scaleY + b.scaleY * bProgress, + pivot = + if (a.pivot.isUnspecified && b.pivot.isUnspecified) { + Offset.Unspecified + } else { + a.pivot.specifiedOrCenter() + b.pivot.specifiedOrCenter() * bProgress + } + ) + } + ) + + sceneState.lastScale = interruptedScale + return interruptedScale } @OptIn(ExperimentalComposeUiApi::class) @@ -524,6 +743,8 @@ private fun ApproachMeasureScope.place( // No need to place the element in this scene if we don't want to draw it anyways. if (!shouldPlaceElement(layoutImpl, scene, element, transition)) { + sceneState.lastOffset = Offset.Unspecified + sceneState.offsetBeforeInterruption = Offset.Unspecified return } @@ -542,15 +763,37 @@ private fun ApproachMeasureScope.place( ::lerp, ) - val offset = (targetOffset - currentOffset).round() - if (isElementOpaque(scene, element, transition)) { + val interruptedOffset = + computeInterruptedValue( + layoutImpl, + transition, + value = targetOffset, + unspecifiedValue = Offset.Unspecified, + zeroValue = Offset.Zero, + getValueBeforeInterruption = { sceneState.offsetBeforeInterruption }, + setValueBeforeInterruption = { sceneState.offsetBeforeInterruption = it }, + getInterruptionDelta = { sceneState.offsetInterruptionDelta }, + setInterruptionDelta = { sceneState.offsetInterruptionDelta = it }, + diff = { a, b -> a - b }, + add = { a, b, bProgress -> a + b * bProgress }, + ) + + sceneState.lastOffset = interruptedOffset + + val offset = (interruptedOffset - currentOffset).round() + if ( + isElementOpaque(scene, element, transition) && + interruptedAlpha(layoutImpl, transition, sceneState, alpha = 1f) == 1f + ) { + sceneState.lastAlpha = 1f + // TODO(b/291071158): Call placeWithLayer() if offset != IntOffset.Zero and size is not // animated once b/305195729 is fixed. Test that drawing is not invalidated in that // case. placeable.place(offset) } else { placeable.placeWithLayer(offset) { - alpha = elementAlpha(layoutImpl, scene, element, transition) + alpha = elementAlpha(layoutImpl, scene, element, transition, sceneState) compositingStrategy = CompositingStrategy.ModulateAlpha } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt index 20dcc2044c8b..ad691ba54607 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt @@ -49,7 +49,7 @@ internal class SceneTransitionLayoutImpl( internal var swipeSourceDetector: SwipeSourceDetector, internal var transitionInterceptionThreshold: Float, builder: SceneTransitionLayoutScope.() -> Unit, - private val coroutineScope: CoroutineScope, + internal val coroutineScope: CoroutineScope, ) { /** * The map of [Scene]s. diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt index f13c016e9d68..5fda77a3e0ae 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt @@ -18,6 +18,9 @@ package com.android.compose.animation.scene import android.util.Log import androidx.annotation.VisibleForTesting +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.animation.core.spring import androidx.compose.foundation.gestures.Orientation import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -34,6 +37,7 @@ import kotlin.math.absoluteValue import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch /** * The state of a [SceneTransitionLayout]. @@ -253,6 +257,12 @@ sealed interface TransitionState { } } + /** + * An animatable that animates from 1f to 0f. This will be used to nicely animate the sudden + * jump of values when this transitions interrupts another one. + */ + private var interruptionDecay: Animatable<Float, AnimationVector1D>? = null + init { check(fromScene != toScene) } @@ -289,6 +299,33 @@ sealed interface TransitionState { fromOverscrollSpec = fromSpec toOverscrollSpec = toSpec } + + internal open fun interruptionProgress( + layoutImpl: SceneTransitionLayoutImpl, + ): Float { + if (!layoutImpl.state.enableInterruptions) { + return 0f + } + + fun create(): Animatable<Float, AnimationVector1D> { + val animatable = Animatable(1f, visibilityThreshold = ProgressVisibilityThreshold) + layoutImpl.coroutineScope.launch { + val swipeSpec = layoutImpl.state.transitions.defaultSwipeSpec + val progressSpec = + spring( + stiffness = swipeSpec.stiffness, + dampingRatio = swipeSpec.dampingRatio, + visibilityThreshold = ProgressVisibilityThreshold, + ) + animatable.animateTo(0f, progressSpec) + } + + return animatable + } + + val animatable = interruptionDecay ?: create().also { interruptionDecay = it } + return animatable.value + } } interface HasOverscrollProperties { 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 b7fc91c4e762..b1d7055573b6 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 @@ -37,6 +37,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue @@ -55,7 +56,10 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.lerp import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.compose.animation.scene.TestScenes.SceneA import com.android.compose.animation.scene.TestScenes.SceneB @@ -1019,4 +1023,122 @@ class ElementTest { rule.onNode(isElement(TestElements.Foo)).assertDoesNotExist() rule.onNode(isElement(TestElements.Bar)).assertPositionInRootIsEqualTo(100.dp, 100.dp) } + + @Test + fun interruption() = runTest { + // 4 frames of animation. + val duration = 4 * 16 + + val state = + MutableSceneTransitionLayoutStateImpl( + SceneA, + transitions { + from(SceneA, to = SceneB) { spec = tween(duration, easing = LinearEasing) } + from(SceneB, to = SceneC) { spec = tween(duration, easing = LinearEasing) } + }, + enableInterruptions = false, + ) + + val layoutSize = DpSize(200.dp, 100.dp) + val fooSize = DpSize(20.dp, 10.dp) + + @Composable + fun SceneScope.Foo(modifier: Modifier = Modifier) { + Box(modifier.element(TestElements.Foo).size(fooSize)) + } + + rule.setContent { + SceneTransitionLayout(state, Modifier.size(layoutSize)) { + // In scene A, Foo is aligned at the TopStart. + scene(SceneA) { + Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.TopStart)) } + } + + // In scene B, Foo is aligned at the TopEnd, so it moves horizontally when coming + // from A. + scene(SceneB) { + Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.TopEnd)) } + } + + // In scene C, Foo is aligned at the BottomEnd, so it moves vertically when coming + // from B. + scene(SceneC) { + Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.BottomEnd)) } + } + } + } + + // The offset of Foo when idle in A, B or C. + val offsetInA = DpOffset.Zero + val offsetInB = DpOffset(layoutSize.width - fooSize.width, 0.dp) + val offsetInC = + DpOffset(layoutSize.width - fooSize.width, layoutSize.height - fooSize.height) + + // Initial state (idle in A). + rule + .onNode(isElement(TestElements.Foo, SceneA)) + .assertPositionInRootIsEqualTo(offsetInA.x, offsetInA.y) + + // Current transition is A => B at 50%. + val aToBProgress = 0.5f + val aToB = + transition( + from = SceneA, + to = SceneB, + progress = { aToBProgress }, + onFinish = neverFinish(), + ) + val offsetInAToB = lerp(offsetInA, offsetInB, aToBProgress) + rule.runOnUiThread { state.startTransition(aToB, transitionKey = null) } + rule + .onNode(isElement(TestElements.Foo, SceneB)) + .assertPositionInRootIsEqualTo(offsetInAToB.x, offsetInAToB.y) + + // Start B => C at 0%. + var bToCProgress by mutableFloatStateOf(0f) + var interruptionProgress by mutableFloatStateOf(1f) + val bToC = + transition( + from = SceneB, + to = SceneC, + progress = { bToCProgress }, + interruptionProgress = { interruptionProgress }, + ) + rule.runOnUiThread { state.startTransition(bToC, transitionKey = null) } + + // The offset interruption delta, which will be multiplied by the interruption progress then + // added to the current transition offset. + val interruptionDelta = offsetInAToB - offsetInB + + // Interruption progress is at 100% and bToC is at 0%, so Foo should be at the same offset + // as right before the interruption. + rule + .onNode(isElement(TestElements.Foo, SceneC)) + .assertPositionInRootIsEqualTo(offsetInAToB.x, offsetInAToB.y) + + // Move the transition forward at 30% and set the interruption progress to 50%. + bToCProgress = 0.3f + interruptionProgress = 0.5f + val offsetInBToC = lerp(offsetInB, offsetInC, bToCProgress) + val offsetInBToCWithInterruption = + offsetInBToC + + DpOffset( + interruptionDelta.x * interruptionProgress, + interruptionDelta.y * interruptionProgress, + ) + rule.waitForIdle() + rule + .onNode(isElement(TestElements.Foo, SceneC)) + .assertPositionInRootIsEqualTo( + offsetInBToCWithInterruption.x, + offsetInBToCWithInterruption.y, + ) + + // Finish the transition and interruption. + bToCProgress = 1f + interruptionProgress = 0f + rule + .onNode(isElement(TestElements.Foo, SceneC)) + .assertPositionInRootIsEqualTo(offsetInC.x, offsetInC.y) + } } diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt index 767057b585b8..c1218ae778ba 100644 --- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt @@ -18,12 +18,17 @@ package com.android.compose.animation.scene import androidx.compose.foundation.gestures.Orientation import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.test.TestScope /** A utility to easily create a [TransitionState.Transition] in tests. */ fun transition( from: SceneKey, to: SceneKey, progress: () -> Float = { 0f }, + interruptionProgress: () -> Float = { 100f }, isInitiatedByUserInput: Boolean = false, isUserInputOngoing: Boolean = false, isUpOrLeft: Boolean = false, @@ -55,5 +60,22 @@ fun transition( return onFinish(this) } + + override fun interruptionProgress(layoutImpl: SceneTransitionLayoutImpl): Float { + return interruptionProgress() + } + } +} + +/** + * Return a onFinish lambda that can be used with [transition] so that the transition never + * finishes. This allows to keep the transition in the current transitions list. + */ +fun TestScope.neverFinish(): (TransitionState.Transition) -> Job { + return { + backgroundScope.launch { + // Try to acquire a locked mutex so that this code never completes. + Mutex(locked = true).withLock {} + } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt index 31337a635bfb..e270d9efec97 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt @@ -28,6 +28,7 @@ import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepos import com.android.systemui.keyguard.domain.interactor.BurnInInteractor import com.android.systemui.keyguard.domain.interactor.burnInInteractor import com.android.systemui.keyguard.shared.model.BurnInModel +import com.android.systemui.keyguard.shared.model.ClockSize import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep @@ -59,6 +60,7 @@ class AodBurnInViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository + private val keyguardClockRepository = kosmos.fakeKeyguardClockRepository private lateinit var underTest: AodBurnInViewModel private var burnInParameters = BurnInParameters() @@ -67,6 +69,7 @@ class AodBurnInViewModelTest : SysuiTestCase() { @Before fun setUp() { mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) + mSetFlagsRule.disableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN) MockitoAnnotations.initMocks(this) whenever(burnInInteractor.burnIn(anyInt(), anyInt())).thenReturn(burnInFlow) @@ -298,4 +301,80 @@ class AodBurnInViewModelTest : SysuiTestCase() { assertThat(movement?.scale).isEqualTo(0.5f) assertThat(movement?.scaleClockOnly).isEqualTo(false) } + + @Test + fun translationAndScale_composeFlagOn_weatherLargeClock() = + testBurnInViewModelWhenComposeFlagOn( + isSmallClock = false, + isWeatherClock = true, + expectedScaleOnly = false + ) + + @Test + fun translationAndScale_composeFlagOn_weatherSmallClock() = + testBurnInViewModelWhenComposeFlagOn( + isSmallClock = true, + isWeatherClock = true, + expectedScaleOnly = true + ) + + @Test + fun translationAndScale_composeFlagOn_nonWeatherLargeClock() = + testBurnInViewModelWhenComposeFlagOn( + isSmallClock = false, + isWeatherClock = false, + expectedScaleOnly = true + ) + + @Test + fun translationAndScale_composeFlagOn_nonWeatherSmallClock() = + testBurnInViewModelWhenComposeFlagOn( + isSmallClock = true, + isWeatherClock = false, + expectedScaleOnly = true + ) + + private fun testBurnInViewModelWhenComposeFlagOn( + isSmallClock: Boolean, + isWeatherClock: Boolean, + expectedScaleOnly: Boolean + ) = + testScope.runTest { + mSetFlagsRule.enableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) + mSetFlagsRule.enableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN) + if (isSmallClock) { + keyguardClockRepository.setClockSize(ClockSize.SMALL) + // we need the following step to update stateFlow value + kosmos.testScope.collectLastValue(kosmos.keyguardClockViewModel.clockSize) + } + + whenever(clockController.config.useAlternateSmartspaceAODTransition) + .thenReturn(if (isWeatherClock) true else false) + + val movement by collectLastValue(underTest.movement(burnInParameters)) + + // Set to dozing (on AOD) + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + value = 1f, + transitionState = TransitionState.FINISHED + ), + validateStep = false, + ) + + // Trigger a change to the burn-in model + burnInFlow.value = + BurnInModel( + translationX = 20, + translationY = 30, + scale = 0.5f, + ) + + assertThat(movement?.translationX).isEqualTo(20) + assertThat(movement?.translationY).isEqualTo(30) + assertThat(movement?.scale).isEqualTo(0.5f) + assertThat(movement?.scaleClockOnly).isEqualTo(expectedScaleOnly) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt index 7420ea0ba56a..8ce50379fe5c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt @@ -30,7 +30,7 @@ import com.android.systemui.statusbar.policy.HeadsUpManagerTestUtil.createFullSc import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.settings.FakeGlobalSettings import com.android.systemui.util.time.FakeSystemClock -import com.google.common.truth.Truth +import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Rule import org.junit.Test @@ -132,7 +132,7 @@ class AvalancheControllerTest : SysuiTestCase() { mAvalancheController.update(headsUpEntry, runnableMock!!, "testLabel") // Entry is showing now - Truth.assertThat(mAvalancheController.headsUpEntryShowing).isEqualTo(headsUpEntry) + assertThat(mAvalancheController.headsUpEntryShowing).isEqualTo(headsUpEntry) } @Test @@ -147,14 +147,14 @@ class AvalancheControllerTest : SysuiTestCase() { // Entry has one Runnable val runnableList: List<Runnable?>? = mAvalancheController.nextMap[headsUpEntry] - Truth.assertThat(runnableList).isNotNull() - Truth.assertThat(runnableList!!.size).isEqualTo(1) + assertThat(runnableList).isNotNull() + assertThat(runnableList!!.size).isEqualTo(1) // Update mAvalancheController.update(headsUpEntry, runnableMock, "testLabel") // Entry has two Runnables - Truth.assertThat(runnableList.size).isEqualTo(2) + assertThat(runnableList.size).isEqualTo(2) } @Test @@ -172,7 +172,7 @@ class AvalancheControllerTest : SysuiTestCase() { mAvalancheController.update(headsUpEntry, runnableMock!!, "testLabel") // Entry is next - Truth.assertThat(mAvalancheController.nextMap.containsKey(headsUpEntry)).isTrue() + assertThat(mAvalancheController.nextMap.containsKey(headsUpEntry)).isTrue() } @Test @@ -185,7 +185,7 @@ class AvalancheControllerTest : SysuiTestCase() { mAvalancheController.delete(headsUpEntry, runnableMock, "testLabel") // Entry was removed from next - Truth.assertThat(mAvalancheController.nextMap.containsKey(headsUpEntry)).isFalse() + assertThat(mAvalancheController.nextMap.containsKey(headsUpEntry)).isFalse() // Runnable was not run Mockito.verify(runnableMock, Mockito.times(0)).run() @@ -201,7 +201,7 @@ class AvalancheControllerTest : SysuiTestCase() { mAvalancheController.delete(headsUpEntry, runnableMock!!, "testLabel") // Entry was removed from dropSet - Truth.assertThat(mAvalancheController.debugDropSet.contains(headsUpEntry)).isFalse() + assertThat(mAvalancheController.debugDropSet.contains(headsUpEntry)).isFalse() } @Test @@ -244,7 +244,27 @@ class AvalancheControllerTest : SysuiTestCase() { mAvalancheController.delete(showingEntry, runnableMock, "testLabel") // Next entry is shown - Truth.assertThat(mAvalancheController.headsUpEntryShowing).isEqualTo(nextEntry) + assertThat(mAvalancheController.headsUpEntryShowing).isEqualTo(nextEntry) + } + + + @Test + fun testDelete_showingEntryKeyBecomesPreviousHunKey() { + mAvalancheController.previousHunKey = "" + + // Entry is showing + val showingEntry = createHeadsUpEntry(id = 0) + mAvalancheController.headsUpEntryShowing = showingEntry + + // There's another entry waiting to show next + val nextEntry = createHeadsUpEntry(id = 1) + mAvalancheController.addToNext(nextEntry, runnableMock!!) + + // Delete + mAvalancheController.delete(showingEntry, runnableMock, "testLabel") + + // Next entry is shown + assertThat(mAvalancheController.previousHunKey).isEqualTo(showingEntry.mEntry!!.key) } @Test @@ -258,7 +278,7 @@ class AvalancheControllerTest : SysuiTestCase() { mAvalancheController.clearNext() val durationMs = mAvalancheController.getDurationMs(givenEntry, autoDismissMs = 5000) - Truth.assertThat(durationMs).isEqualTo(5000) + assertThat(durationMs).isEqualTo(5000) } @Test @@ -273,7 +293,7 @@ class AvalancheControllerTest : SysuiTestCase() { mAvalancheController.addToNext(nextEntry, runnableMock!!) val durationMs = mAvalancheController.getDurationMs(givenEntry, autoDismissMs = 5000) - Truth.assertThat(durationMs).isEqualTo(5000) + assertThat(durationMs).isEqualTo(5000) } @Test @@ -286,7 +306,7 @@ class AvalancheControllerTest : SysuiTestCase() { mAvalancheController.clearNext() val durationMs = mAvalancheController.getDurationMs(showingEntry, autoDismissMs = 5000) - Truth.assertThat(durationMs).isEqualTo(5000) + assertThat(durationMs).isEqualTo(5000) } @Test @@ -300,10 +320,10 @@ class AvalancheControllerTest : SysuiTestCase() { mAvalancheController.addToNext(nextEntry, runnableMock!!) // Next entry has lower priority - Truth.assertThat(nextEntry.compareNonTimeFields(showingEntry)).isEqualTo(1) + assertThat(nextEntry.compareNonTimeFields(showingEntry)).isEqualTo(1) val durationMs = mAvalancheController.getDurationMs(showingEntry, autoDismissMs = 5000) - Truth.assertThat(durationMs).isEqualTo(5000) + assertThat(durationMs).isEqualTo(5000) } @Test @@ -317,10 +337,10 @@ class AvalancheControllerTest : SysuiTestCase() { mAvalancheController.addToNext(nextEntry, runnableMock!!) // Same priority - Truth.assertThat(nextEntry.compareNonTimeFields(showingEntry)).isEqualTo(0) + assertThat(nextEntry.compareNonTimeFields(showingEntry)).isEqualTo(0) val durationMs = mAvalancheController.getDurationMs(showingEntry, autoDismissMs = 5000) - Truth.assertThat(durationMs).isEqualTo(1000) + assertThat(durationMs).isEqualTo(1000) } @Test @@ -334,9 +354,9 @@ class AvalancheControllerTest : SysuiTestCase() { mAvalancheController.addToNext(nextEntry, runnableMock!!) // Next entry has higher priority - Truth.assertThat(nextEntry.compareNonTimeFields(showingEntry)).isEqualTo(-1) + assertThat(nextEntry.compareNonTimeFields(showingEntry)).isEqualTo(-1) val durationMs = mAvalancheController.getDurationMs(showingEntry, autoDismissMs = 5000) - Truth.assertThat(durationMs).isEqualTo(500) + assertThat(durationMs).isEqualTo(500) } } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl index dcc14409f046..94b6fd42f701 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl @@ -156,9 +156,11 @@ interface ISystemUiProxy { oneway void animateNavBarLongPress(boolean isTouchDown, boolean shrink, long durationMs) = 54; /** - * Set the override value for home button long press duration in ms and slop multiplier. + * Set the override value for home button long press duration in ms and slop multiplier and + * haptic. */ - oneway void setOverrideHomeButtonLongPress(long duration, float slopMultiplier) = 55; + oneway void setOverrideHomeButtonLongPress(long duration, float slopMultiplier, boolean haptic) + = 55; // Next id = 56 } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt index 644bea0b762d..5b83a10c14c7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt @@ -27,6 +27,7 @@ import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.domain.interactor.BurnInInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.ComposeLockscreen import com.android.systemui.keyguard.shared.model.BurnInModel import com.android.systemui.keyguard.shared.model.ClockSize import com.android.systemui.keyguard.ui.StateToValue @@ -126,32 +127,41 @@ constructor( val useScaleOnly = useAltAod && keyguardClockViewModel.clockSize.value == ClockSize.LARGE - if (useScaleOnly) { - BurnInModel( - translationX = 0, - translationY = 0, - scale = MathUtils.lerp(burnIn.scale, 1f, 1f - interpolated), - ) - } else { - // Ensure the desired translation doesn't encroach on the top inset - val burnInY = MathUtils.lerp(0, burnIn.translationY, interpolated).toInt() - val translationY = - if (MigrateClocksToBlueprint.isEnabled) { - max(params.topInset - params.minViewY, burnInY) - } else { - max(params.topInset, params.minViewY + burnInY) - params.minViewY - } + val burnInY = MathUtils.lerp(0, burnIn.translationY, interpolated).toInt() + val translationY = + if (MigrateClocksToBlueprint.isEnabled) { + max(params.topInset - params.minViewY, burnInY) + } else { + max(params.topInset, params.minViewY + burnInY) - params.minViewY + } + if (ComposeLockscreen.isEnabled) { BurnInModel( translationX = MathUtils.lerp(0, burnIn.translationX, interpolated).toInt(), translationY = translationY, - scale = - MathUtils.lerp( - /* start= */ burnIn.scale, - /* stop= */ 1f, - /* amount= */ 1f - interpolated, - ), - scaleClockOnly = true, + scale = MathUtils.lerp(burnIn.scale, 1f, 1f - interpolated), + scaleClockOnly = !useScaleOnly, ) + } else { + if (useScaleOnly) { + BurnInModel( + translationX = 0, + translationY = 0, + scale = MathUtils.lerp(burnIn.scale, 1f, 1f - interpolated), + ) + } else { + // Ensure the desired translation doesn't encroach on the top inset + BurnInModel( + translationX = MathUtils.lerp(0, burnIn.translationX, interpolated).toInt(), + translationY = translationY, + scale = + MathUtils.lerp( + /* start= */ burnIn.scale, + /* stop= */ 1f, + /* amount= */ 1f - interpolated, + ), + scaleClockOnly = true, + ) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java index 43c73c4e7f35..1a2ae8a68d74 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java @@ -243,6 +243,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements private Optional<Long> mHomeButtonLongPressDurationMs; private Optional<Long> mOverrideHomeButtonLongPressDurationMs = Optional.empty(); private Optional<Float> mOverrideHomeButtonLongPressSlopMultiplier = Optional.empty(); + private boolean mHomeButtonLongPressHapticEnabled = true; /** @see android.view.WindowInsetsController#setSystemBarsAppearance(int, int) */ private @Appearance int mAppearance; @@ -410,13 +411,15 @@ public class NavigationBar extends ViewController<NavigationBarView> implements } @Override - public void setOverrideHomeButtonLongPress(long duration, float slopMultiplier) { - Log.d(TAG, "setOverrideHomeButtonLongPress receives: " + duration + "; " - + slopMultiplier); + public void setOverrideHomeButtonLongPress(long duration, float slopMultiplier, + boolean haptic) { + Log.d(TAG, "setOverrideHomeButtonLongPress receives: " + duration + ";" + + slopMultiplier + ";" + haptic); mOverrideHomeButtonLongPressDurationMs = Optional.of(duration) .filter(value -> value > 0); mOverrideHomeButtonLongPressSlopMultiplier = Optional.of(slopMultiplier) .filter(value -> value > 0); + mHomeButtonLongPressHapticEnabled = haptic; mOverrideHomeButtonLongPressDurationMs.ifPresent(aLong -> Log.d(TAG, "Use duration override: " + aLong)); mOverrideHomeButtonLongPressSlopMultiplier.ifPresent(aFloat @@ -463,9 +466,11 @@ public class NavigationBar extends ViewController<NavigationBarView> implements private final Runnable mEnableLayoutTransitions = () -> mView.setLayoutTransitionsEnabled(true); private final Runnable mOnVariableDurationHomeLongClick = () -> { if (onHomeLongClick(mView.getHomeButton().getCurrentView())) { - mView.getHomeButton().getCurrentView().performHapticFeedback( - HapticFeedbackConstants.LONG_PRESS, - HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); + if (mHomeButtonLongPressHapticEnabled) { + mView.getHomeButton().getCurrentView().performHapticFeedback( + HapticFeedbackConstants.LONG_PRESS, + HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); + } } }; @@ -1042,7 +1047,8 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mView.getHomeButton().setOnLongClickListener(null); } else { mView.getHomeButton().getCurrentView().setLongClickable(true); - mView.getHomeButton().getCurrentView().setHapticFeedbackEnabled(true); + mView.getHomeButton().getCurrentView().setHapticFeedbackEnabled( + mHomeButtonLongPressHapticEnabled); mView.getHomeButton().setOnLongClickListener(this::onHomeLongClick); } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index 1ddc0946905c..520331ae72af 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -264,9 +264,10 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis } @Override - public void setOverrideHomeButtonLongPress(long duration, float slopMultiplier) { + public void setOverrideHomeButtonLongPress(long duration, float slopMultiplier, + boolean haptic) { verifyCallerAndClearCallingIdentityPostMain("setOverrideHomeButtonLongPress", - () -> notifySetOverrideHomeButtonLongPress(duration, slopMultiplier)); + () -> notifySetOverrideHomeButtonLongPress(duration, slopMultiplier, haptic)); } @Override @@ -956,9 +957,11 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis } } - private void notifySetOverrideHomeButtonLongPress(long duration, float slopMultiplier) { + private void notifySetOverrideHomeButtonLongPress(long duration, float slopMultiplier, + boolean haptic) { for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { - mConnectionCallbacks.get(i).setOverrideHomeButtonLongPress(duration, slopMultiplier); + mConnectionCallbacks.get(i) + .setOverrideHomeButtonLongPress(duration, slopMultiplier, haptic); } } @@ -1119,8 +1122,9 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis default void startAssistant(Bundle bundle) {} default void setAssistantOverridesRequested(int[] invocationTypes) {} default void animateNavBarLongPress(boolean isTouchDown, boolean shrink, long durationMs) {} - /** Set override of home button long press duration and touch slop multiplier. */ - default void setOverrideHomeButtonLongPress(long override, float slopMultiplier) {} + /** Set override of home button long press duration, touch slop multiplier, and haptic. */ + default void setOverrideHomeButtonLongPress( + long override, float slopMultiplier, boolean haptic) {} } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HeadsUpStyleProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HeadsUpStyleProvider.kt new file mode 100644 index 000000000000..816e5c132432 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HeadsUpStyleProvider.kt @@ -0,0 +1,37 @@ +/* + * 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.notification.row + +import android.app.Flags +import javax.inject.Inject + +/** + * A class managing the heads up style to be applied based on user settings, immersive mode and + * other factors. + */ +interface HeadsUpStyleProvider { + fun shouldApplyCompactStyle(): Boolean +} + +class HeadsUpStyleProviderImpl @Inject constructor() : HeadsUpStyleProvider { + + /** + * TODO(b/270709257) This feature is under development. This method returns Compact when the + * flag is enabled for fish fooding purpose. + */ + override fun shouldApplyCompactStyle(): Boolean = Flags.compactHeadsUpNotification() +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java index 31e69c98549b..2f038714212b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java @@ -90,6 +90,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder private final Executor mInflationExecutor; private final SmartReplyStateInflater mSmartReplyStateInflater; private final NotifLayoutInflaterFactory.Provider mNotifLayoutInflaterFactoryProvider; + private final HeadsUpStyleProvider mHeadsUpStyleProvider; + private final NotificationContentInflaterLogger mLogger; @Inject @@ -101,6 +103,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder @NotifInflation Executor inflationExecutor, SmartReplyStateInflater smartRepliesInflater, NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider, + HeadsUpStyleProvider headsUpStyleProvider, NotificationContentInflaterLogger logger) { mRemoteViewCache = remoteViewCache; mRemoteInputManager = remoteInputManager; @@ -109,6 +112,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder mInflationExecutor = inflationExecutor; mSmartReplyStateInflater = smartRepliesInflater; mNotifLayoutInflaterFactoryProvider = notifLayoutInflaterFactoryProvider; + mHeadsUpStyleProvider = headsUpStyleProvider; mLogger = logger; } @@ -158,6 +162,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder /* isMediaFlagEnabled = */ mIsMediaInQS, mSmartReplyStateInflater, mNotifLayoutInflaterFactoryProvider, + mHeadsUpStyleProvider, mLogger); if (mInflateSynchronously) { task.onPostExecute(task.doInBackground()); @@ -184,6 +189,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder packageContext, row, mNotifLayoutInflaterFactoryProvider, + mHeadsUpStyleProvider, mLogger); result = inflateSmartReplyViews(result, reInflateFlags, entry, row.getContext(), @@ -370,6 +376,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder boolean usesIncreasedHeadsUpHeight, Context packageContext, ExpandableNotificationRow row, NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider, + HeadsUpStyleProvider headsUpStyleProvider, NotificationContentInflaterLogger logger) { return TraceUtils.trace("NotificationContentInflater.createRemoteViews", () -> { InflationProgress result = new InflationProgress(); @@ -388,8 +395,13 @@ public class NotificationContentInflater implements NotificationRowContentBinder if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) { logger.logAsyncTaskProgress(entryForLogging, "creating heads up remote view"); - result.newHeadsUpView = builder.createHeadsUpContentView( - usesIncreasedHeadsUpHeight); + final boolean isHeadsUpCompact = headsUpStyleProvider.shouldApplyCompactStyle(); + if (isHeadsUpCompact) { + result.newHeadsUpView = builder.createCompactHeadsUpContentView(); + } else { + result.newHeadsUpView = builder.createHeadsUpContentView( + usesIncreasedHeadsUpHeight); + } } if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) { @@ -1067,6 +1079,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder private final boolean mIsMediaInQS; private final SmartReplyStateInflater mSmartRepliesInflater; private final NotifLayoutInflaterFactory.Provider mNotifLayoutInflaterFactoryProvider; + private final HeadsUpStyleProvider mHeadsUpStyleProvider; private final NotificationContentInflaterLogger mLogger; private AsyncInflationTask( @@ -1085,6 +1098,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder boolean isMediaFlagEnabled, SmartReplyStateInflater smartRepliesInflater, NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider, + HeadsUpStyleProvider headsUpStyleProvider, NotificationContentInflaterLogger logger) { mEntry = entry; mRow = row; @@ -1102,6 +1116,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder mConversationProcessor = conversationProcessor; mIsMediaInQS = isMediaFlagEnabled; mNotifLayoutInflaterFactoryProvider = notifLayoutInflaterFactoryProvider; + mHeadsUpStyleProvider = headsUpStyleProvider; mLogger = logger; entry.setInflationTask(this); } @@ -1166,7 +1181,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder InflationProgress inflationProgress = createRemoteViews(mReInflateFlags, recoveredBuilder, mIsMinimized, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, packageContext, mRow, - mNotifLayoutInflaterFactoryProvider, mLogger); + mNotifLayoutInflaterFactoryProvider, mHeadsUpStyleProvider, mLogger); mLogger.logAsyncTaskProgress(mEntry, "getting existing smart reply state (on wrong thread!)"); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java index 200a08a2ea65..17c20268bc1c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java @@ -50,4 +50,12 @@ public abstract class NotificationRowModule { @SysUISingleton public abstract NotifRemoteViewsFactoryContainer provideNotifRemoteViewsFactoryContainer( NotifRemoteViewsFactoryContainerImpl containerImpl); + + /** + * Provides heads up style manager + */ + @Binds + @SysUISingleton + public abstract HeadsUpStyleProvider provideHeadsUpStyleManager( + HeadsUpStyleProviderImpl headsUpStyleManagerImpl); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCompactHeadsUpTemplateViewWrapper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCompactHeadsUpTemplateViewWrapper.kt new file mode 100644 index 000000000000..ce87d2f46d90 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCompactHeadsUpTemplateViewWrapper.kt @@ -0,0 +1,34 @@ +/* + * 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.notification.row.wrapper + +import android.content.Context +import android.view.View +import com.android.systemui.statusbar.notification.FeedbackIcon +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow + +/** + * Compact Heads up Notifications template that doesn't set feedback icon and audibly alert icons + */ +class NotificationCompactHeadsUpTemplateViewWrapper( + ctx: Context, + view: View, + row: ExpandableNotificationRow +) : NotificationTemplateViewWrapper(ctx, view, row) { + override fun setFeedbackIcon(icon: FeedbackIcon?) = Unit + override fun setRecentlyAudiblyAlerted(audiblyAlerted: Boolean) = Unit +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java index 50f3e7896442..4244542f1f61 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java @@ -72,7 +72,10 @@ public abstract class NotificationViewWrapper implements TransformableView { return new NotificationConversationTemplateViewWrapper(ctx, v, row); } else if ("call".equals(v.getTag())) { return new NotificationCallTemplateViewWrapper(ctx, v, row); + } else if ("compactHUN".equals((v.getTag()))) { + return new NotificationCompactHeadsUpTemplateViewWrapper(ctx, v, row); } + if (row.getEntry().getSbn().getNotification().isStyle( Notification.DecoratedCustomViewStyle.class)) { return new NotificationDecoratedCustomViewWrapper(ctx, v, row); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java index dedf3668ee94..e520957975f3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java @@ -38,6 +38,7 @@ import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.BypassController; import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.SectionProvider; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; +import com.android.systemui.statusbar.policy.AvalancheController; import java.io.PrintWriter; @@ -56,6 +57,8 @@ public class AmbientState implements Dumpable { private final SectionProvider mSectionProvider; private final BypassController mBypassController; private final LargeScreenShadeInterpolator mLargeScreenShadeInterpolator; + private final AvalancheController mAvalancheController; + /** * Used to read bouncer states. */ @@ -269,12 +272,14 @@ public class AmbientState implements Dumpable { @NonNull SectionProvider sectionProvider, @NonNull BypassController bypassController, @Nullable StatusBarKeyguardViewManager statusBarKeyguardViewManager, - @NonNull LargeScreenShadeInterpolator largeScreenShadeInterpolator + @NonNull LargeScreenShadeInterpolator largeScreenShadeInterpolator, + AvalancheController avalancheController ) { mSectionProvider = sectionProvider; mBypassController = bypassController; mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; mLargeScreenShadeInterpolator = largeScreenShadeInterpolator; + mAvalancheController = avalancheController; reload(context); dumpManager.registerDumpable(this); } @@ -287,6 +292,14 @@ public class AmbientState implements Dumpable { mBaseZHeight = getBaseHeight(mZDistanceBetweenElements); } + String getAvalancheShowingHunKey() { + return mAvalancheController.getShowingHunKey(); + } + + String getAvalanchePreviousHunKey() { + return mAvalancheController.getPreviousHunKey(); + } + void setOverExpansion(float overExpansion) { mOverExpansion = overExpansion; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt index 36c23d381844..2670a95329cc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt @@ -40,6 +40,9 @@ class AvalancheController @Inject constructor( // HUN showing right now, in the floating state where full shade is hidden, on launcher or AOD @VisibleForTesting var headsUpEntryShowing: HeadsUpEntry? = null + // Key of HUN previously showing, is being removed or was removed + var previousHunKey: String = "" + // List of runnables to run for the HUN showing right now private var headsUpEntryShowingRunnableList: MutableList<Runnable> = ArrayList() @@ -63,6 +66,10 @@ class AvalancheController @Inject constructor( dumpManager.registerNormalDumpable(tag, /* module */ this) } + fun getShowingHunKey(): String { + return getKey(headsUpEntryShowing) + } + /** Run or delay Runnable for given HeadsUpEntry */ fun update(entry: HeadsUpEntry?, runnable: Runnable, label: String) { if (!NotificationThrottleHun.isEnabled) { @@ -134,8 +141,10 @@ class AvalancheController @Inject constructor( debugDropSet.remove(entry) } else if (isShowing(entry)) { log { "$fn => [remove showing ${getKey(entry)}]" } + previousHunKey = getKey(headsUpEntryShowing) + runnable.run() - showNext() + showNextAfterRemove() } else { log { "$fn => [removing untracked ${getKey(entry)}]" } } @@ -238,7 +247,7 @@ class AvalancheController @Inject constructor( } } - private fun showNext() { + private fun showNextAfterRemove() { log { "SHOW NEXT" } headsUpEntryShowing = null @@ -284,6 +293,7 @@ class AvalancheController @Inject constructor( private fun getStateStr(): String { return "SHOWING: [${getKey(headsUpEntryShowing)}]" + + "\nPREVIOUS: [$previousHunKey]" + "\nNEXT LIST: $nextListStr" + "\nNEXT MAP: $nextMapStr" + "\nDROPPED: $dropSetStr" @@ -325,10 +335,10 @@ class AvalancheController @Inject constructor( fun getKey(entry: HeadsUpEntry?): String { if (entry == null) { - return "null" + return "HeadsUpEntry null" } if (entry.mEntry == null) { - return entry.toString() + return "HeadsUpEntry.mEntry null" } return entry.mEntry!!.key } diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt index c380a51f6a59..fbe11847f3d3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt @@ -64,14 +64,7 @@ class TransitionAnimatorTest : SysuiTestCase() { @get:Rule(order = 0) val deviceEmulationRule = DeviceEmulationRule(emulationSpec) @get:Rule(order = 1) val activityRule = ActivityScenarioRule(EmptyTestActivity::class.java) - @get:Rule(order = 2) - val motionRule = - ViewMotionTestRule<EmptyTestActivity>( - pathManager, - { activityRule.scenario }, - context = context, - bitmapDiffer = null, - ) + @get:Rule(order = 2) val motionRule = ViewMotionTestRule(pathManager, { activityRule.scenario }) @Test fun backgroundAnimation_whenLaunching() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java index 8c225113677b..03a84036b4b5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java @@ -95,6 +95,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase { @Mock private InflatedSmartReplyState mInflatedSmartReplyState; @Mock private InflatedSmartReplyViewHolder mInflatedSmartReplies; @Mock private NotifLayoutInflaterFactory.Provider mNotifLayoutInflaterFactoryProvider; + @Mock private HeadsUpStyleProvider mHeadsUpStyleProvider; @Mock private NotifLayoutInflaterFactory mNotifLayoutInflaterFactory; private final SmartReplyStateInflater mSmartReplyStateInflater = @@ -138,6 +139,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase { mock(Executor.class), mSmartReplyStateInflater, mNotifLayoutInflaterFactoryProvider, + mHeadsUpStyleProvider, mock(NotificationContentInflaterLogger.class)); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java index 954335efd33a..f35ec74114de 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java @@ -199,6 +199,7 @@ public class NotificationTestHelper { mock(Executor.class), new MockSmartReplyInflater(), mock(NotifLayoutInflaterFactory.Provider.class), + mock(HeadsUpStyleProvider.class), mock(NotificationContentInflaterLogger.class)); contentBinder.setInflateSynchronously(true); mBindStage = new RowContentBindStage(contentBinder, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt index 4715b33aa40a..fb1594898f24 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt @@ -23,6 +23,7 @@ import com.android.systemui.dump.DumpManager import com.android.systemui.shade.transition.LargeScreenShadeInterpolator import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager +import com.android.systemui.statusbar.policy.AvalancheController import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat @@ -41,6 +42,7 @@ class AmbientStateTest : SysuiTestCase() { private val bypassController = StackScrollAlgorithm.BypassController { false } private val statusBarKeyguardViewManager = mock<StatusBarKeyguardViewManager>() private val largeScreenShadeInterpolator = mock<LargeScreenShadeInterpolator>() + private val avalancheController = mock<AvalancheController>() private lateinit var sut: AmbientState @@ -53,7 +55,8 @@ class AmbientStateTest : SysuiTestCase() { sectionProvider, bypassController, statusBarKeyguardViewManager, - largeScreenShadeInterpolator + largeScreenShadeInterpolator, + avalancheController ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index 89ae9f4e3547..939d0556e347 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -94,6 +94,7 @@ import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRe import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.ScreenOffAnimationController; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; +import com.android.systemui.statusbar.policy.AvalancheController; import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController; import org.junit.Assert; @@ -140,6 +141,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { @Mock private NotificationStackSizeCalculator mNotificationStackSizeCalculator; @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; @Mock private LargeScreenShadeInterpolator mLargeScreenShadeInterpolator; + @Mock private AvalancheController mAvalancheController; @Before public void setUp() throws Exception { @@ -153,7 +155,8 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { mNotificationSectionsManager, mBypassController, mStatusBarKeyguardViewManager, - mLargeScreenShadeInterpolator + mLargeScreenShadeInterpolator, + mAvalancheController )); // Register the debug flags we use diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt index 2c2b1831bcfd..82725d656bb9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt @@ -25,6 +25,7 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager +import com.android.systemui.statusbar.policy.AvalancheController import com.android.systemui.util.mockito.mock import com.google.common.truth.Expect import com.google.common.truth.Truth.assertThat @@ -48,6 +49,7 @@ class StackScrollAlgorithmTest : SysuiTestCase() { @JvmField @Rule var expect: Expect = Expect.create() private val largeScreenShadeInterpolator = mock<LargeScreenShadeInterpolator>() + private val avalancheController = mock<AvalancheController>() private val hostView = FrameLayout(context) private val stackScrollAlgorithm = StackScrollAlgorithm(context, hostView) @@ -71,6 +73,7 @@ class StackScrollAlgorithmTest : SysuiTestCase() { /* bypassController */ { false }, mStatusBarKeyguardViewManager, largeScreenShadeInterpolator, + avalancheController ) private val testableResources = mContext.getOrCreateTestableResources() diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/AmbientStateKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/AmbientStateKosmos.kt index 7f6f698c2932..383e31d99314 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/AmbientStateKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/AmbientStateKosmos.kt @@ -33,5 +33,6 @@ val Kosmos.ambientState by Fixture { /*bypassController=*/ stackScrollAlgorithmBypassController, /*statusBarKeyguardViewManager=*/ statusBarKeyguardViewManager, /*largeScreenShadeInterpolator=*/ largeScreenShadeInterpolator, + /*avalancheController=*/ avalancheController, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmKosmos.kt index 67343c95f6e1..e20ce270fc14 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmKosmos.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.stack import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.statusbar.policy.AvalancheController import com.android.systemui.util.mockito.mock var Kosmos.stackScrollAlgorithmSectionProvider by Fixture { @@ -27,3 +28,7 @@ var Kosmos.stackScrollAlgorithmSectionProvider by Fixture { var Kosmos.stackScrollAlgorithmBypassController by Fixture { mock<StackScrollAlgorithm.BypassController>() } + +var Kosmos.avalancheController by Fixture { + mock<AvalancheController>() +} diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java index e1710649f925..2d1aba4b43aa 100644 --- a/services/core/java/com/android/server/TelephonyRegistry.java +++ b/services/core/java/com/android/server/TelephonyRegistry.java @@ -1132,20 +1132,17 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { return; } - int phoneId = -1; int subscriptionId = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID; - if(Flags.preventSystemServerAndPhoneDeadlock()) { - // Legacy applications pass SubscriptionManager.DEFAULT_SUB_ID, - // force all illegal subId to SubscriptionManager.DEFAULT_SUB_ID - if (!SubscriptionManager.isValidSubscriptionId(subId)) { - if (DBG) { - log("invalid subscription id, use default id"); - } - } else { //APP specify subID - subscriptionId = subId; + // Legacy applications pass SubscriptionManager.DEFAULT_SUB_ID, + // force all illegal subId to SubscriptionManager.DEFAULT_SUB_ID + if (!SubscriptionManager.isValidSubscriptionId(subId)) { + if (DBG) { + log("invalid subscription id, use default id"); } - phoneId = getPhoneIdFromSubId(subscriptionId); + } else { //APP specify subID + subscriptionId = subId; } + int phoneId = getPhoneIdFromSubId(subscriptionId); synchronized (mRecords) { // register @@ -1166,23 +1163,8 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { r.renounceFineLocationAccess = renounceFineLocationAccess; r.callerUid = Binder.getCallingUid(); r.callerPid = Binder.getCallingPid(); - - if(!Flags.preventSystemServerAndPhoneDeadlock()) { - // Legacy applications pass SubscriptionManager.DEFAULT_SUB_ID, - // force all illegal subId to SubscriptionManager.DEFAULT_SUB_ID - if (!SubscriptionManager.isValidSubscriptionId(subId)) { - if (DBG) { - log("invalid subscription id, use default id"); - } - r.subId = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID; - } else {//APP specify subID - r.subId = subId; - } - r.phoneId = getPhoneIdFromSubId(r.subId); - } else { - r.subId = subscriptionId; - r.phoneId = phoneId; - } + r.subId = subscriptionId; + r.phoneId = phoneId; r.eventList = events; if (DBG) { @@ -1928,14 +1910,8 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } private void notifyCarrierNetworkChangeWithPermission(int subId, boolean active) { - int phoneId = -1; - if(Flags.preventSystemServerAndPhoneDeadlock()) { - phoneId = getPhoneIdFromSubId(subId); - } + int phoneId = getPhoneIdFromSubId(subId); synchronized (mRecords) { - if(!Flags.preventSystemServerAndPhoneDeadlock()) { - phoneId = getPhoneIdFromSubId(subId); - } mCarrierNetworkChangeState[phoneId] = active; if (VDBG) { diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java index 38fa79f7f44a..e2c4b4638207 100644 --- a/services/core/java/com/android/server/audio/SpatializerHelper.java +++ b/services/core/java/com/android/server/audio/SpatializerHelper.java @@ -88,6 +88,10 @@ public class SpatializerHelper { /*package*/ static final SparseIntArray SPAT_MODE_FOR_DEVICE_TYPE = new SparseIntArray(14) { { append(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, Spatialization.Mode.TRANSAURAL); + // Speaker safe is considered compatible with spatial audio because routing media usage + // to speaker safe only happens in transient situations and should not affect app + // decisions to play spatial audio content. + append(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER_SAFE, Spatialization.Mode.TRANSAURAL); append(AudioDeviceInfo.TYPE_WIRED_HEADSET, Spatialization.Mode.BINAURAL); append(AudioDeviceInfo.TYPE_WIRED_HEADPHONES, Spatialization.Mode.BINAURAL); // assumption for A2DP: mostly headsets @@ -805,7 +809,7 @@ public class SpatializerHelper { private boolean isDeviceCompatibleWithSpatializationModes(@NonNull AudioDeviceAttributes ada) { // modeForDevice will be neither transaural or binaural for devices that do not support - // spatial audio. For instance mono devices like earpiece, speaker safe or sco must + // spatial audio. For instance mono devices like earpiece or sco must // not be included. final byte modeForDevice = (byte) SPAT_MODE_FOR_DEVICE_TYPE.get(ada.getType(), /*default when type not found*/ -1); diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index f32c11d90c0d..8686e9a920e4 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -250,8 +250,19 @@ public class InputManagerService extends IInputManager.Stub private final Object mAssociationsLock = new Object(); @GuardedBy("mAssociationsLock") private final Map<String, Integer> mRuntimeAssociations = new ArrayMap<>(); + + // The associations of input devices to displays by port. Maps from {InputDevice#mName} (String) + // to {DisplayInfo#uniqueId} (String) so that events from the Input Device go to a + // specific display. @GuardedBy("mAssociationsLock") private final Map<String, String> mUniqueIdAssociations = new ArrayMap<>(); + + // The associations of input devices to displays by descriptor. Maps from + // {InputDevice#mDescriptor} to {DisplayInfo#uniqueId} (String) so that events from the + // input device go to a specific display. + @GuardedBy("mAssociationsLock") + private final Map<String, String> mUniqueIdAssociationsByDescriptor = new ArrayMap<>(); + // The map from input port (String) to the keyboard layout identifiers (comma separated string // containing language tag and layout type) associated with the corresponding keyboard device. // Currently only accessed by InputReader. @@ -1741,8 +1752,8 @@ public class InputManagerService extends IInputManager.Stub /** * Add a runtime association between the input port and the display port. This overrides any * static associations. - * @param inputPort The port of the input device. - * @param displayPort The physical port of the associated display. + * @param inputPort the port of the input device + * @param displayPort the physical port of the associated display */ @Override // Binder call public void addPortAssociation(@NonNull String inputPort, int displayPort) { @@ -1763,7 +1774,7 @@ public class InputManagerService extends IInputManager.Stub /** * Remove the runtime association between the input port and the display port. Any existing * static association for the cleared input port will be restored. - * @param inputPort The port of the input device to be cleared. + * @param inputPort the port of the input device to be cleared */ @Override // Binder call public void removePortAssociation(@NonNull String inputPort) { @@ -1813,6 +1824,49 @@ public class InputManagerService extends IInputManager.Stub mNative.changeUniqueIdAssociation(); } + /** + * Adds a runtime association between the input device descriptor and the display unique id. + * @param inputDeviceDescriptor the descriptor of the input device + * @param displayUniqueId the unique ID of the display + */ + @Override // Binder call + public void addUniqueIdAssociationByDescriptor(@NonNull String inputDeviceDescriptor, + @NonNull String displayUniqueId) { + if (!checkCallingPermission( + android.Manifest.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY, + "addUniqueIdAssociationByDescriptor()")) { + throw new SecurityException( + "Requires ASSOCIATE_INPUT_DEVICE_TO_DISPLAY permission"); + } + + Objects.requireNonNull(inputDeviceDescriptor); + Objects.requireNonNull(displayUniqueId); + synchronized (mAssociationsLock) { + mUniqueIdAssociationsByDescriptor.put(inputDeviceDescriptor, displayUniqueId); + } + mNative.changeUniqueIdAssociation(); + } + + /** + * Removes the runtime association between the input device and the display. + * @param inputDeviceDescriptor the descriptor of the input device + */ + @Override // Binder call + public void removeUniqueIdAssociationByDescriptor(@NonNull String inputDeviceDescriptor) { + if (!checkCallingPermission( + android.Manifest.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY, + "removeUniqueIdAssociationByDescriptor()")) { + throw new SecurityException( + "Requires ASSOCIATE_INPUT_DEVICE_TO_DISPLAY permission"); + } + + Objects.requireNonNull(inputDeviceDescriptor); + synchronized (mAssociationsLock) { + mUniqueIdAssociationsByDescriptor.remove(inputDeviceDescriptor); + } + mNative.changeUniqueIdAssociation(); + } + void setTypeAssociationInternal(@NonNull String inputPort, @NonNull String type) { Objects.requireNonNull(inputPort); Objects.requireNonNull(type); @@ -2190,6 +2244,13 @@ public class InputManagerService extends IInputManager.Stub pw.println(" uniqueId: " + v); }); } + if (!mUniqueIdAssociationsByDescriptor.isEmpty()) { + pw.println("Unique Id Associations:"); + mUniqueIdAssociationsByDescriptor.forEach((k, v) -> { + pw.print(" descriptor: " + k); + pw.println(" uniqueId: " + v); + }); + } if (!mDeviceTypeAssociations.isEmpty()) { pw.println("Type Associations:"); mDeviceTypeAssociations.forEach((k, v) -> { @@ -2633,6 +2694,17 @@ public class InputManagerService extends IInputManager.Stub // Native callback @SuppressWarnings("unused") + private String[] getInputUniqueIdAssociationsByDescriptor() { + final Map<String, String> associations; + synchronized (mAssociationsLock) { + associations = new HashMap<>(mUniqueIdAssociationsByDescriptor); + } + + return flatten(associations); + } + + // Native callback + @SuppressWarnings("unused") @VisibleForTesting String[] getDeviceTypeAssociations() { final Map<String, String> associations; diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java index 74adf5e0d52c..f981d791d383 100644 --- a/services/core/java/com/android/server/media/MediaSessionService.java +++ b/services/core/java/com/android/server/media/MediaSessionService.java @@ -215,12 +215,6 @@ public class MediaSessionService extends SystemService implements Monitor { private final MediaCommunicationManager.SessionCallback mSession2TokenCallback = new MediaCommunicationManager.SessionCallback() { @Override - // TODO (b/324266224): Deprecate this method once other overload is published. - public void onSession2TokenCreated(Session2Token token) { - addSession(token, Process.INVALID_PID); - } - - @Override public void onSession2TokenCreated(Session2Token token, int pid) { addSession(token, pid); } diff --git a/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessorImpl.java b/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessorImpl.java index 40353a2d2353..f1248a3f5f0e 100644 --- a/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessorImpl.java +++ b/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessorImpl.java @@ -435,7 +435,8 @@ public final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { @Override public boolean isTelephonyTimeZoneDetectionFeatureSupported() { - return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY); + return getConfigBoolean(com.android.internal.R.bool.config_enableTelephonyTimeZoneDetection) + && mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY); } @Override diff --git a/services/core/java/com/android/server/wm/CameraIdPackageNameBiMapping.java b/services/core/java/com/android/server/wm/CameraIdPackageNameBiMapping.java new file mode 100644 index 000000000000..d9730cbb036c --- /dev/null +++ b/services/core/java/com/android/server/wm/CameraIdPackageNameBiMapping.java @@ -0,0 +1,78 @@ +/* + * 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.wm; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.ArrayMap; + +import java.util.Map; + +/** + * Bidirectional mapping (1:1) for the currently active cameraId and the app package that opened it. + * + * <p>This class is not thread-safe. + */ +final class CameraIdPackageNameBiMapping { + private final Map<String, String> mPackageToCameraIdMap = new ArrayMap<>(); + private final Map<String, String> mCameraIdToPackageMap = new ArrayMap<>(); + + boolean isEmpty() { + return mCameraIdToPackageMap.isEmpty(); + } + + void put(@NonNull String packageName, @NonNull String cameraId) { + // Always using the last connected camera ID for the package even for the concurrent + // camera use case since we can't guess which camera is more important anyway. + removePackageName(packageName); + removeCameraId(cameraId); + mPackageToCameraIdMap.put(packageName, cameraId); + mCameraIdToPackageMap.put(cameraId, packageName); + } + + boolean containsPackageName(@NonNull String packageName) { + return mPackageToCameraIdMap.containsKey(packageName); + } + + @Nullable + String getCameraId(@NonNull String packageName) { + return mPackageToCameraIdMap.get(packageName); + } + + void removeCameraId(@NonNull String cameraId) { + final String packageName = mCameraIdToPackageMap.get(cameraId); + if (packageName == null) { + return; + } + mPackageToCameraIdMap.remove(packageName, cameraId); + mCameraIdToPackageMap.remove(cameraId, packageName); + } + + @NonNull + String getSummaryForDisplayRotationHistoryRecord() { + return "{ mPackageToCameraIdMap=" + mPackageToCameraIdMap + " }"; + } + + private void removePackageName(@NonNull String packageName) { + String cameraId = mPackageToCameraIdMap.get(packageName); + if (cameraId == null) { + return; + } + mPackageToCameraIdMap.remove(packageName, cameraId); + mCameraIdToPackageMap.remove(cameraId, packageName); + } +} diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java index e808decc354d..c8a7ef11a700 100644 --- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java @@ -46,7 +46,6 @@ import android.content.res.Configuration; import android.hardware.camera2.CameraManager; import android.os.Handler; import android.os.RemoteException; -import android.util.ArrayMap; import android.util.ArraySet; import android.widget.Toast; @@ -56,7 +55,6 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; import com.android.server.UiThread; -import java.util.Map; import java.util.Set; /** @@ -100,7 +98,8 @@ final class DisplayRotationCompatPolicy { // camera id by a package name when determining rotation; 2) get a package name by a camera id // when camera connection is closed and we need to clean up our records. @GuardedBy("this") - private final CameraIdPackageNameBiMap mCameraIdPackageBiMap = new CameraIdPackageNameBiMap(); + private final CameraIdPackageNameBiMapping mCameraIdPackageBiMap = + new CameraIdPackageNameBiMapping(); @GuardedBy("this") private final Set<String> mScheduledToBeRemovedCameraIdSet = new ArraySet<>(); @GuardedBy("this") @@ -516,54 +515,4 @@ final class DisplayRotationCompatPolicy { } return topActivity.mLetterboxUiController.isRefreshAfterRotationRequested(); } - - private static class CameraIdPackageNameBiMap { - - private final Map<String, String> mPackageToCameraIdMap = new ArrayMap<>(); - private final Map<String, String> mCameraIdToPackageMap = new ArrayMap<>(); - - boolean isEmpty() { - return mCameraIdToPackageMap.isEmpty(); - } - - void put(String packageName, String cameraId) { - // Always using the last connected camera ID for the package even for the concurrent - // camera use case since we can't guess which camera is more important anyway. - removePackageName(packageName); - removeCameraId(cameraId); - mPackageToCameraIdMap.put(packageName, cameraId); - mCameraIdToPackageMap.put(cameraId, packageName); - } - - boolean containsPackageName(String packageName) { - return mPackageToCameraIdMap.containsKey(packageName); - } - - @Nullable - String getCameraId(String packageName) { - return mPackageToCameraIdMap.get(packageName); - } - - void removeCameraId(String cameraId) { - String packageName = mCameraIdToPackageMap.get(cameraId); - if (packageName == null) { - return; - } - mPackageToCameraIdMap.remove(packageName, cameraId); - mCameraIdToPackageMap.remove(cameraId, packageName); - } - - String getSummaryForDisplayRotationHistoryRecord() { - return "{ mPackageToCameraIdMap=" + mPackageToCameraIdMap + " }"; - } - - private void removePackageName(String packageName) { - String cameraId = mPackageToCameraIdMap.get(packageName); - if (cameraId == null) { - return; - } - mPackageToCameraIdMap.remove(packageName, cameraId); - mCameraIdToPackageMap.remove(cameraId, packageName); - } - } } diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 88c47f3cc5f8..553f721554d4 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -129,6 +129,7 @@ static struct { jmethodID getExcludedDeviceNames; jmethodID getInputPortAssociations; jmethodID getInputUniqueIdAssociations; + jmethodID getInputUniqueIdAssociationsByDescriptor; jmethodID getDeviceTypeAssociations; jmethodID getKeyboardLayoutAssociations; jmethodID getHoverTapTimeout; @@ -634,11 +635,15 @@ void NativeInputManager::getReaderConfiguration(InputReaderConfiguration* outCon env->DeleteLocalRef(portAssociations); } - outConfig->uniqueIdAssociations = + outConfig->uniqueIdAssociationsByPort = readMapFromInterleavedJavaArray<std::string>(gServiceClassInfo .getInputUniqueIdAssociations, "getInputUniqueIdAssociations"); + outConfig->uniqueIdAssociationsByDescriptor = readMapFromInterleavedJavaArray< + std::string>(gServiceClassInfo.getInputUniqueIdAssociationsByDescriptor, + "getInputUniqueIdAssociationsByDescriptor"); + outConfig->deviceTypeAssociations = readMapFromInterleavedJavaArray<std::string>(gServiceClassInfo .getDeviceTypeAssociations, @@ -3093,6 +3098,9 @@ int register_android_server_InputManager(JNIEnv* env) { GET_METHOD_ID(gServiceClassInfo.getInputUniqueIdAssociations, clazz, "getInputUniqueIdAssociations", "()[Ljava/lang/String;"); + GET_METHOD_ID(gServiceClassInfo.getInputUniqueIdAssociationsByDescriptor, clazz, + "getInputUniqueIdAssociationsByDescriptor", "()[Ljava/lang/String;"); + GET_METHOD_ID(gServiceClassInfo.getDeviceTypeAssociations, clazz, "getDeviceTypeAssociations", "()[Ljava/lang/String;"); diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java index 5d3e4994d718..75e8e68dd98a 100644 --- a/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java @@ -116,7 +116,7 @@ public class UserWakeupStoreTest { mUserWakeupStore.addUserWakeup(USER_ID_1, TEST_TIMESTAMP - 7_000); mUserWakeupStore.addUserWakeup(USER_ID_1, finalAlarmTime); assertEquals(1, mUserWakeupStore.getUserIdsToWakeup(TEST_TIMESTAMP).length); - final long alarmTime = mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_1) + final long alarmTime = mUserWakeupStore.getWakeupTimeForUser(USER_ID_1) + BUFFER_TIME_MS; assertTrue(finalAlarmTime + USER_START_TIME_DEVIATION_LIMIT_MS >= alarmTime); assertTrue(finalAlarmTime - USER_START_TIME_DEVIATION_LIMIT_MS <= alarmTime); @@ -129,8 +129,8 @@ public class UserWakeupStoreTest { mUserWakeupStore.addUserWakeup(USER_ID_3, TEST_TIMESTAMP - 13_000); assertEquals(3, mUserWakeupStore.getUserIdsToWakeup(TEST_TIMESTAMP).length); mUserWakeupStore.removeUserWakeup(USER_ID_3); - assertEquals(-1, mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_3)); - assertTrue(mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_2) > 0); + assertEquals(-1, mUserWakeupStore.getWakeupTimeForUser(USER_ID_3)); + assertTrue(mUserWakeupStore.getWakeupTimeForUser(USER_ID_2) > 0); } @Test @@ -139,10 +139,10 @@ public class UserWakeupStoreTest { mUserWakeupStore.addUserWakeup(USER_ID_2, TEST_TIMESTAMP - 3_000); mUserWakeupStore.addUserWakeup(USER_ID_3, TEST_TIMESTAMP - 13_000); assertEquals(mUserWakeupStore.getNextWakeupTime(), - mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_1)); + mUserWakeupStore.getWakeupTimeForUser(USER_ID_1)); mUserWakeupStore.removeUserWakeup(USER_ID_1); assertEquals(mUserWakeupStore.getNextWakeupTime(), - mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_3)); + mUserWakeupStore.getWakeupTimeForUser(USER_ID_3)); } @Test @@ -154,16 +154,16 @@ public class UserWakeupStoreTest { mUserWakeupStore.init(); final long realtime = SystemClock.elapsedRealtime(); assertEquals(0, mUserWakeupStore.getUserIdsToWakeup(TEST_TIMESTAMP).length); - assertTrue(mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_2) > realtime); - assertTrue(mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_1) - < mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_3)); - assertTrue(mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_3) - < mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_2)); - assertTrue(mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_1) - realtime + assertTrue(mUserWakeupStore.getWakeupTimeForUser(USER_ID_2) > realtime); + assertTrue(mUserWakeupStore.getWakeupTimeForUser(USER_ID_1) + < mUserWakeupStore.getWakeupTimeForUser(USER_ID_3)); + assertTrue(mUserWakeupStore.getWakeupTimeForUser(USER_ID_3) + < mUserWakeupStore.getWakeupTimeForUser(USER_ID_2)); + assertTrue(mUserWakeupStore.getWakeupTimeForUser(USER_ID_1) - realtime < BUFFER_TIME_MS + USER_START_TIME_DEVIATION_LIMIT_MS); - assertTrue(mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_3) - realtime + assertTrue(mUserWakeupStore.getWakeupTimeForUser(USER_ID_3) - realtime < 2 * BUFFER_TIME_MS + USER_START_TIME_DEVIATION_LIMIT_MS); - assertTrue(mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_2) - realtime + assertTrue(mUserWakeupStore.getWakeupTimeForUser(USER_ID_2) - realtime < 3 * BUFFER_TIME_MS + USER_START_TIME_DEVIATION_LIMIT_MS); } //TODO: b/330264023 - Add tests for I/O in usersWithAlarmClocks.xml. diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/InputManagerMockHelper.java b/services/tests/servicestests/src/com/android/server/companion/virtual/InputManagerMockHelper.java index 74e854e49c2a..b33a8aa28544 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/InputManagerMockHelper.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/InputManagerMockHelper.java @@ -79,8 +79,9 @@ class InputManagerMockHelper { when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[0]); doAnswer(inv -> mDevices.get(inv.getArgument(0))) .when(mIInputManagerMock).getInputDevice(anyInt()); - doAnswer(inv -> mUniqueIdAssociation.put(inv.getArgument(0), inv.getArgument(1))).when( - mIInputManagerMock).addUniqueIdAssociation(anyString(), anyString()); + doAnswer(inv -> mUniqueIdAssociation.put(inv.getArgument(0), + inv.getArgument(1))).when(mIInputManagerMock).addUniqueIdAssociation( + anyString(), anyString()); doAnswer(inv -> mUniqueIdAssociation.remove(inv.getArgument(0))).when( mIInputManagerMock).removeUniqueIdAssociation(anyString()); diff --git a/services/tests/wmtests/src/com/android/server/wm/CameraIdPackageNameBiMappingTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraIdPackageNameBiMappingTests.java new file mode 100644 index 000000000000..f3a20a678919 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/CameraIdPackageNameBiMappingTests.java @@ -0,0 +1,107 @@ +/* + * 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.wm; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; + +/** + * Tests for {@link CameraIdPackageNameBiMapping}. + * + * Build/Install/Run: + * atest WmTests:CameraIdPackageNameBiMapTests + */ +@SmallTest +@Presubmit +public class CameraIdPackageNameBiMappingTests { + private CameraIdPackageNameBiMapping mMapping; + + private static final String PACKAGE_1 = "PACKAGE_1"; + private static final String PACKAGE_2 = "PACKAGE_2"; + private static final String CAMERA_ID_1 = "1234"; + private static final String CAMERA_ID_2 = "5678"; + + @Before + public void setUp() { + mMapping = new CameraIdPackageNameBiMapping(); + } + + @Test + public void mappingEmptyAtStart() { + assertTrue(mMapping.isEmpty()); + } + + @Test + public void addPackageAndId_containsPackage() { + mMapping.put(PACKAGE_1, CAMERA_ID_1); + assertTrue(mMapping.containsPackageName(PACKAGE_1)); + } + + @Test + public void addTwoPackagesAndId_containsPackages() { + mMapping.put(PACKAGE_1, CAMERA_ID_1); + mMapping.put(PACKAGE_2, CAMERA_ID_2); + assertTrue(mMapping.containsPackageName(PACKAGE_1)); + assertTrue(mMapping.containsPackageName(PACKAGE_2)); + } + + @Test + public void addPackageAndId_mapContainsPackageAndId() { + mMapping.put(PACKAGE_1, CAMERA_ID_1); + assertEquals(CAMERA_ID_1, mMapping.getCameraId(PACKAGE_1)); + } + + @Test + public void changeCameraId_newestCameraId() { + mMapping.put(PACKAGE_1, CAMERA_ID_1); + mMapping.put(PACKAGE_1, CAMERA_ID_2); + assertEquals(CAMERA_ID_2, mMapping.getCameraId(PACKAGE_1)); + } + + @Test + public void changePackage_newestPackage() { + mMapping.put(PACKAGE_1, CAMERA_ID_1); + mMapping.put(PACKAGE_2, CAMERA_ID_1); + assertFalse(mMapping.containsPackageName(PACKAGE_1)); + assertTrue(mMapping.containsPackageName(PACKAGE_2)); + assertEquals(CAMERA_ID_1, mMapping.getCameraId(PACKAGE_2)); + } + + @Test + public void addAndRemoveCameraId_containsOtherPackage() { + mMapping.put(PACKAGE_1, CAMERA_ID_1); + mMapping.put(PACKAGE_2, CAMERA_ID_2); + mMapping.removeCameraId(CAMERA_ID_1); + assertFalse(mMapping.containsPackageName(PACKAGE_1)); + assertTrue(mMapping.containsPackageName(PACKAGE_2)); + } + + @Test + public void addAndRemoveOnlyCameraId_empty() { + mMapping.put(PACKAGE_1, CAMERA_ID_1); + mMapping.removeCameraId(CAMERA_ID_1); + assertTrue(mMapping.isEmpty()); + } +} diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt index f6f766a6a5b0..c9c657404b68 100644 --- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt +++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt @@ -19,17 +19,26 @@ package com.android.server.input import android.content.Context import android.content.ContextWrapper +import android.hardware.display.DisplayManager import android.hardware.display.DisplayViewport +import android.hardware.display.VirtualDisplay import android.hardware.input.InputManager import android.hardware.input.InputManagerGlobal +import android.os.InputEventInjectionSync +import android.os.SystemClock import android.os.test.TestLooper import android.platform.test.annotations.Presubmit import android.platform.test.annotations.RequiresFlagsDisabled import android.platform.test.flag.junit.DeviceFlagsValueProvider import android.provider.Settings -import android.test.mock.MockContentResolver +import android.view.View.OnKeyListener import android.view.Display +import android.view.InputDevice +import android.view.KeyEvent import android.view.PointerIcon +import android.view.SurfaceHolder +import android.view.SurfaceView +import android.test.mock.MockContentResolver import androidx.test.platform.app.InstrumentationRegistry import com.android.internal.util.test.FakeSettingsProvider import com.google.common.truth.Truth.assertThat @@ -48,6 +57,7 @@ import org.mockito.Mock import org.mockito.Mockito.`when` import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.spy import org.mockito.Mockito.times @@ -412,6 +422,175 @@ class InputManagerServiceTests { verify(wmCallbacks).notifyPointerDisplayIdChanged(overrideDisplayId, 0f, 0f) thread.join(100 /*millis*/) } + + private fun createVirtualDisplays(count: Int): List<VirtualDisplay> { + val displayManager: DisplayManager = context.getSystemService( + DisplayManager::class.java + ) as DisplayManager + val virtualDisplays = mutableListOf<VirtualDisplay>() + for (i in 0 until count) { + virtualDisplays.add(displayManager.createVirtualDisplay( + /* displayName= */ "testVirtualDisplay$i", + /* width= */ 100, + /* height= */ 100, + /* densityDpi= */ 100, + /* surface= */ null, + /* flags= */ 0 + )) + } + return virtualDisplays + } + + // Helper function that creates a KeyEvent with Keycode A with the given action + private fun createKeycodeAEvent(inputDevice: InputDevice, action: Int): KeyEvent { + val eventTime = SystemClock.uptimeMillis() + return KeyEvent( + /* downTime= */ eventTime, + /* eventTime= */ eventTime, + /* action= */ action, + /* code= */ KeyEvent.KEYCODE_A, + /* repeat= */ 0, + /* metaState= */ 0, + /* deviceId= */ inputDevice.id, + /* scanCode= */ 0, + /* flags= */ KeyEvent.FLAG_FROM_SYSTEM, + /* source= */ InputDevice.SOURCE_KEYBOARD + ) + } + + private fun createInputDevice(): InputDevice { + return InputDevice.Builder() + .setId(123) + .setName("abc") + .setDescriptor("def") + .setSources(InputDevice.SOURCE_KEYBOARD) + .build() + } + + @Test + fun addUniqueIdAssociationByDescriptor_verifyAssociations() { + // Overall goal is to have 2 displays and verify that events from the InputDevice are + // sent only to the view that is on the associated display. + // So, associate the InputDevice with display 1, then send and verify KeyEvents. + // Then remove associations, then associate the InputDevice with display 2, then send + // and verify commands. + + // Make 2 virtual displays with some mock SurfaceViews + val mockSurfaceView1 = mock(SurfaceView::class.java) + val mockSurfaceView2 = mock(SurfaceView::class.java) + val mockSurfaceHolder1 = mock(SurfaceHolder::class.java) + `when`(mockSurfaceView1.holder).thenReturn(mockSurfaceHolder1) + val mockSurfaceHolder2 = mock(SurfaceHolder::class.java) + `when`(mockSurfaceView2.holder).thenReturn(mockSurfaceHolder2) + + val virtualDisplays = createVirtualDisplays(2) + + // Simulate an InputDevice + val inputDevice = createInputDevice() + + // Associate input device with display + service.addUniqueIdAssociationByDescriptor( + inputDevice.descriptor, + virtualDisplays[0].display.displayId.toString() + ) + + // Simulate 2 different KeyEvents + val downEvent = createKeycodeAEvent(inputDevice, KeyEvent.ACTION_DOWN) + val upEvent = createKeycodeAEvent(inputDevice, KeyEvent.ACTION_UP) + + // Create a mock OnKeyListener object + val mockOnKeyListener = mock(OnKeyListener::class.java) + + // Verify that the event went to Display 1 not Display 2 + service.injectInputEvent(downEvent, InputEventInjectionSync.NONE) + + // Call the onKey method on the mock OnKeyListener object + mockOnKeyListener.onKey(mockSurfaceView1, /* keyCode= */ KeyEvent.KEYCODE_A, downEvent) + mockOnKeyListener.onKey(mockSurfaceView2, /* keyCode= */ KeyEvent.KEYCODE_A, upEvent) + + // Verify that the onKey method was called with the expected arguments + verify(mockOnKeyListener).onKey(mockSurfaceView1, KeyEvent.KEYCODE_A, downEvent) + verify(mockOnKeyListener, never()).onKey(mockSurfaceView2, KeyEvent.KEYCODE_A, downEvent) + + // Remove association + service.removeUniqueIdAssociationByDescriptor(inputDevice.descriptor) + + // Associate with Display 2 + service.addUniqueIdAssociationByDescriptor( + inputDevice.descriptor, + virtualDisplays[1].display.displayId.toString() + ) + + // Simulate a KeyEvent + service.injectInputEvent(upEvent, InputEventInjectionSync.NONE) + + // Verify that the event went to Display 2 not Display 1 + verify(mockOnKeyListener).onKey(mockSurfaceView2, KeyEvent.KEYCODE_A, upEvent) + verify(mockOnKeyListener, never()).onKey(mockSurfaceView1, KeyEvent.KEYCODE_A, upEvent) + } + + // TODO(b/324075859): Rename this method to addUniqueIdAssociationByPort_verifyAssociations + @Test + fun addUniqueIdAssociation_verifyAssociations() { + // Overall goal is to have 2 displays and verify that events from the InputDevice are + // sent only to the view that is on the associated display. + // So, associate the InputDevice with display 1, then send and verify KeyEvents. + // Then remove associations, then associate the InputDevice with display 2, then send + // and verify commands. + + // Make 2 virtual displays with some mock SurfaceViews + val mockSurfaceView1 = mock(SurfaceView::class.java) + val mockSurfaceView2 = mock(SurfaceView::class.java) + val mockSurfaceHolder1 = mock(SurfaceHolder::class.java) + `when`(mockSurfaceView1.holder).thenReturn(mockSurfaceHolder1) + val mockSurfaceHolder2 = mock(SurfaceHolder::class.java) + `when`(mockSurfaceView2.holder).thenReturn(mockSurfaceHolder2) + + val virtualDisplays = createVirtualDisplays(2) + + // Simulate an InputDevice + val inputDevice = createInputDevice() + + // Associate input device with display + service.addUniqueIdAssociation( + inputDevice.name, + virtualDisplays[0].display.displayId.toString() + ) + + // Simulate 2 different KeyEvents + val downEvent = createKeycodeAEvent(inputDevice, KeyEvent.ACTION_DOWN) + val upEvent = createKeycodeAEvent(inputDevice, KeyEvent.ACTION_UP) + + // Create a mock OnKeyListener object + val mockOnKeyListener = mock(OnKeyListener::class.java) + + // Verify that the event went to Display 1 not Display 2 + service.injectInputEvent(downEvent, InputEventInjectionSync.NONE) + + // Call the onKey method on the mock OnKeyListener object + mockOnKeyListener.onKey(mockSurfaceView1, /* keyCode= */ KeyEvent.KEYCODE_A, downEvent) + mockOnKeyListener.onKey(mockSurfaceView2, /* keyCode= */ KeyEvent.KEYCODE_A, upEvent) + + // Verify that the onKey method was called with the expected arguments + verify(mockOnKeyListener).onKey(mockSurfaceView1, KeyEvent.KEYCODE_A, downEvent) + verify(mockOnKeyListener, never()).onKey(mockSurfaceView2, KeyEvent.KEYCODE_A, downEvent) + + // Remove association + service.removeUniqueIdAssociation(inputDevice.name) + + // Associate with Display 2 + service.addUniqueIdAssociation( + inputDevice.name, + virtualDisplays[1].display.displayId.toString() + ) + + // Simulate a KeyEvent + service.injectInputEvent(upEvent, InputEventInjectionSync.NONE) + + // Verify that the event went to Display 2 not Display 1 + verify(mockOnKeyListener).onKey(mockSurfaceView2, KeyEvent.KEYCODE_A, upEvent) + verify(mockOnKeyListener, never()).onKey(mockSurfaceView1, KeyEvent.KEYCODE_A, upEvent) + } } private fun <T> whenever(methodCall: T): OngoingStubbing<T> = `when`(methodCall) |