diff options
799 files changed, 22496 insertions, 6415 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 302168d845fa..834398e5c2c2 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -824,6 +824,11 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } +cc_aconfig_library { + name: "android.media.tv.flags-aconfig-cc", + aconfig_declarations: "android.media.tv.flags-aconfig", +} + // Permissions aconfig_declarations { name: "android.permission.flags-aconfig", diff --git a/apex/jobscheduler/service/aconfig/alarm.aconfig b/apex/jobscheduler/service/aconfig/alarm.aconfig index a6e980726a9a..9181ef0c532a 100644 --- a/apex/jobscheduler/service/aconfig/alarm.aconfig +++ b/apex/jobscheduler/service/aconfig/alarm.aconfig @@ -7,3 +7,13 @@ flag { description: "Persist list of users with alarms scheduled and wakeup stopped users before alarms are due" bug: "314907186" } + +flag { + name: "acquire_wakelock_before_send" + namespace: "backstage_power" + description: "Acquire the userspace alarm wakelock before sending the alarm" + bug: "391413964" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java index 829442aed6ac..f89b13dce307 100644 --- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java +++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java @@ -5334,6 +5334,18 @@ public class AlarmManagerService extends SystemService { public void deliverLocked(Alarm alarm, long nowELAPSED) { final long workSourceToken = ThreadLocalWorkSource.setUid( getAlarmAttributionUid(alarm)); + + if (Flags.acquireWakelockBeforeSend()) { + // Acquire the wakelock before starting the app. This needs to be done to avoid + // random stalls in the receiving app in case a suspend attempt is already in + // progress. See b/391413964 for an incident where this was found to happen. + if (mBroadcastRefCount == 0) { + setWakelockWorkSource(alarm.workSource, alarm.creatorUid, alarm.statsTag, true); + mWakeLock.acquire(); + mHandler.obtainMessage(AlarmHandler.REPORT_ALARMS_ACTIVE, 1, 0).sendToTarget(); + } + } + try { if (alarm.operation != null) { // PendingIntent alarm @@ -5399,14 +5411,16 @@ public class AlarmManagerService extends SystemService { ThreadLocalWorkSource.restore(workSourceToken); } - // The alarm is now in flight; now arrange wakelock and stats tracking if (DEBUG_WAKELOCK) { Slog.d(TAG, "mBroadcastRefCount -> " + (mBroadcastRefCount + 1)); } - if (mBroadcastRefCount == 0) { - setWakelockWorkSource(alarm.workSource, alarm.creatorUid, alarm.statsTag, true); - mWakeLock.acquire(); - mHandler.obtainMessage(AlarmHandler.REPORT_ALARMS_ACTIVE, 1, 0).sendToTarget(); + if (!Flags.acquireWakelockBeforeSend()) { + // The alarm is now in flight; now arrange wakelock and stats tracking + if (mBroadcastRefCount == 0) { + setWakelockWorkSource(alarm.workSource, alarm.creatorUid, alarm.statsTag, true); + mWakeLock.acquire(); + mHandler.obtainMessage(AlarmHandler.REPORT_ALARMS_ACTIVE, 1, 0).sendToTarget(); + } } final InFlight inflight = new InFlight(AlarmManagerService.this, alarm, nowELAPSED); mInFlight.add(inflight); diff --git a/api/StubLibraries.bp b/api/StubLibraries.bp index 787fdee6ee16..3314b4aa0d71 100644 --- a/api/StubLibraries.bp +++ b/api/StubLibraries.bp @@ -648,7 +648,7 @@ java_api_library { java_api_library { name: "android-non-updatable.stubs.module_lib.from-text", - api_surface: "module_lib", + api_surface: "module-lib", api_contributions: [ "api-stubs-docs-non-updatable.api.contribution", "system-api-stubs-docs-non-updatable.api.contribution", @@ -668,7 +668,7 @@ java_api_library { // generated from this module, as this module is strictly used for hiddenapi only. java_api_library { name: "android-non-updatable.stubs.test_module_lib", - api_surface: "module_lib", + api_surface: "module-lib", api_contributions: [ "api-stubs-docs-non-updatable.api.contribution", "system-api-stubs-docs-non-updatable.api.contribution", @@ -689,7 +689,7 @@ java_api_library { java_api_library { name: "android-non-updatable.stubs.system_server.from-text", - api_surface: "system_server", + api_surface: "system-server", api_contributions: [ "api-stubs-docs-non-updatable.api.contribution", "system-api-stubs-docs-non-updatable.api.contribution", diff --git a/core/api/current.txt b/core/api/current.txt index 6964866db7f3..e9a63f74d59f 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -452,7 +452,7 @@ package android { field public static final int activityCloseExitAnimation = 16842939; // 0x10100bb field public static final int activityOpenEnterAnimation = 16842936; // 0x10100b8 field public static final int activityOpenExitAnimation = 16842937; // 0x10100b9 - field @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public static final int adServiceTypes; + field @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public static final int adServiceTypes = 16844452; // 0x10106a4 field public static final int addPrintersActivity = 16843750; // 0x10103e6 field public static final int addStatesFromChildren = 16842992; // 0x10100f0 field public static final int adjustViewBounds = 16843038; // 0x101011e @@ -1017,7 +1017,7 @@ package android { field public static final int insetRight = 16843192; // 0x10101b8 field public static final int insetTop = 16843193; // 0x10101b9 field public static final int installLocation = 16843447; // 0x10102b7 - field @FlaggedApi("android.security.enable_intent_matching_flags") public static final int intentMatchingFlags; + field @FlaggedApi("android.security.enable_intent_matching_flags") public static final int intentMatchingFlags = 16844457; // 0x10106a9 field public static final int interactiveUiTimeout = 16844181; // 0x1010595 field public static final int interpolator = 16843073; // 0x1010141 field public static final int intro = 16844395; // 0x101066b @@ -1072,7 +1072,7 @@ package android { field public static final int label = 16842753; // 0x1010001 field public static final int labelFor = 16843718; // 0x10103c6 field @Deprecated public static final int labelTextSize = 16843317; // 0x1010235 - field @FlaggedApi("android.view.inputmethod.ime_switcher_revamp_api") public static final int languageSettingsActivity; + field @FlaggedApi("android.view.inputmethod.ime_switcher_revamp_api") public static final int languageSettingsActivity = 16844453; // 0x10106a5 field public static final int languageTag = 16844040; // 0x1010508 field public static final int largeHeap = 16843610; // 0x101035a field public static final int largeScreens = 16843398; // 0x1010286 @@ -1085,7 +1085,7 @@ package android { field public static final int layout = 16842994; // 0x10100f2 field public static final int layoutAnimation = 16842988; // 0x10100ec field public static final int layoutDirection = 16843698; // 0x10103b2 - field @FlaggedApi("android.view.inputmethod.ime_switcher_revamp_api") public static final int layoutLabel; + field @FlaggedApi("android.view.inputmethod.ime_switcher_revamp_api") public static final int layoutLabel = 16844458; // 0x10106aa field public static final int layoutMode = 16843738; // 0x10103da field public static final int layout_above = 16843140; // 0x1010184 field public static final int layout_alignBaseline = 16843142; // 0x1010186 @@ -1207,7 +1207,7 @@ package android { field public static final int minResizeHeight = 16843670; // 0x1010396 field public static final int minResizeWidth = 16843669; // 0x1010395 field public static final int minSdkVersion = 16843276; // 0x101020c - field @FlaggedApi("android.sdk.major_minor_versioning_scheme") public static final int minSdkVersionFull; + field @FlaggedApi("android.sdk.major_minor_versioning_scheme") public static final int minSdkVersionFull = 16844461; // 0x10106ad field public static final int minWidth = 16843071; // 0x101013f field public static final int minimumHorizontalAngle = 16843901; // 0x101047d field public static final int minimumVerticalAngle = 16843902; // 0x101047e @@ -1282,7 +1282,7 @@ package android { field public static final int paddingStart = 16843699; // 0x10103b3 field public static final int paddingTop = 16842967; // 0x10100d7 field public static final int paddingVertical = 16844094; // 0x101053e - field @FlaggedApi("android.content.pm.app_compat_option_16kb") public static final int pageSizeCompat; + field @FlaggedApi("android.content.pm.app_compat_option_16kb") public static final int pageSizeCompat = 16844459; // 0x10106ab field public static final int panelBackground = 16842846; // 0x101005e field public static final int panelColorBackground = 16842849; // 0x1010061 field public static final int panelColorForeground = 16842848; // 0x1010060 @@ -1624,7 +1624,7 @@ package android { field public static final int summaryColumn = 16843426; // 0x10102a2 field public static final int summaryOff = 16843248; // 0x10101f0 field public static final int summaryOn = 16843247; // 0x10101ef - field @FlaggedApi("android.view.accessibility.supplemental_description") public static final int supplementalDescription; + field @FlaggedApi("android.view.accessibility.supplemental_description") public static final int supplementalDescription = 16844456; // 0x10106a8 field public static final int supportedTypes = 16844369; // 0x1010651 field public static final int supportsAssist = 16844016; // 0x10104f0 field public static final int supportsBatteryGameMode = 16844374; // 0x1010656 @@ -1871,7 +1871,7 @@ package android { field public static final int wallpaperIntraOpenExitAnimation = 16843416; // 0x1010298 field public static final int wallpaperOpenEnterAnimation = 16843411; // 0x1010293 field public static final int wallpaperOpenExitAnimation = 16843412; // 0x1010294 - field @FlaggedApi("android.nfc.nfc_associated_role_services") public static final int wantsRoleHolderPriority; + field @FlaggedApi("android.nfc.nfc_associated_role_services") public static final int wantsRoleHolderPriority = 16844460; // 0x10106ac field public static final int webTextViewStyle = 16843449; // 0x10102b9 field public static final int webViewStyle = 16842885; // 0x1010085 field public static final int weekDayTextAppearance = 16843592; // 0x1010348 @@ -11134,6 +11134,7 @@ package android.content { field public static final String TELEPHONY_IMS_SERVICE = "telephony_ims"; field public static final String TELEPHONY_SERVICE = "phone"; field public static final String TELEPHONY_SUBSCRIPTION_SERVICE = "telephony_subscription_service"; + field public static final String TETHERING_SERVICE = "tethering"; field public static final String TEXT_CLASSIFICATION_SERVICE = "textclassification"; field public static final String TEXT_SERVICES_MANAGER_SERVICE = "textservices"; field @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public static final String TV_AD_SERVICE = "tv_ad"; @@ -42267,6 +42268,7 @@ package android.service.notification { method public int getRank(); method @NonNull public java.util.List<android.app.Notification.Action> getSmartActions(); method @NonNull public java.util.List<java.lang.CharSequence> getSmartReplies(); + method @FlaggedApi("android.app.nm_summarization") @Nullable public String getSummarization(); method public int getSuppressedVisualEffects(); method public int getUserSentiment(); method public boolean isAmbient(); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 937a9ffaf210..7483316e94b0 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -464,7 +464,7 @@ package android { public static final class R.attr { field public static final int allowClearUserDataOnFailedRestore = 16844288; // 0x1010600 - field @FlaggedApi("android.permission.flags.replace_body_sensor_permission_enabled") public static final int backgroundPermission; + field @FlaggedApi("android.permission.flags.replace_body_sensor_permission_enabled") public static final int backgroundPermission = 16844455; // 0x10106a7 field @FlaggedApi("android.content.res.manifest_flagging") public static final int featureFlag = 16844428; // 0x101068c field public static final int gameSessionService = 16844373; // 0x1010655 field public static final int hotwordDetectionService = 16844326; // 0x1010626 @@ -543,7 +543,7 @@ package android { field public static final int config_systemCallStreaming = 17039431; // 0x1040047 field public static final int config_systemCompanionDeviceProvider = 17039417; // 0x1040039 field public static final int config_systemContacts = 17039403; // 0x104002b - field @FlaggedApi("android.content.pm.sdk_dependency_installer") public static final int config_systemDependencyInstaller; + field @FlaggedApi("android.content.pm.sdk_dependency_installer") public static final int config_systemDependencyInstaller = 17039434; // 0x104004a field public static final int config_systemFinancedDeviceController = 17039430; // 0x1040046 field public static final int config_systemGallery = 17039399; // 0x1040027 field public static final int config_systemNotificationIntelligence = 17039413; // 0x1040035 @@ -555,7 +555,7 @@ package android { field public static final int config_systemTextIntelligence = 17039414; // 0x1040036 field public static final int config_systemUi = 17039418; // 0x104003a field public static final int config_systemUiIntelligence = 17039410; // 0x1040032 - field @FlaggedApi("android.permission.flags.system_vendor_intelligence_role_enabled") public static final int config_systemVendorIntelligence; + field @FlaggedApi("android.permission.flags.system_vendor_intelligence_role_enabled") public static final int config_systemVendorIntelligence = 17039435; // 0x104004b field public static final int config_systemVisualIntelligence = 17039415; // 0x1040037 field public static final int config_systemWearHealthService = 17039428; // 0x1040044 field public static final int config_systemWellbeing = 17039408; // 0x1040030 @@ -3800,7 +3800,6 @@ package android.content { field public static final String STATS_MANAGER = "stats"; field public static final String SYSTEM_CONFIG_SERVICE = "system_config"; field public static final String SYSTEM_UPDATE_SERVICE = "system_update"; - field public static final String TETHERING_SERVICE = "tethering"; field @FlaggedApi("com.android.net.thread.platform.flags.thread_enabled_platform") public static final String THREAD_NETWORK_SERVICE = "thread_network"; field public static final String TIME_MANAGER_SERVICE = "time_manager"; field public static final String TRANSLATION_MANAGER_SERVICE = "translation"; @@ -13446,6 +13445,7 @@ package android.service.notification { field public static final String KEY_RANKING_SCORE = "key_ranking_score"; field public static final String KEY_SENSITIVE_CONTENT = "key_sensitive_content"; field public static final String KEY_SNOOZE_CRITERIA = "key_snooze_criteria"; + field @FlaggedApi("android.app.nm_summarization") public static final String KEY_SUMMARIZATION = "key_summarization"; field public static final String KEY_TEXT_REPLIES = "key_text_replies"; field @FlaggedApi("android.service.notification.notification_classification") public static final String KEY_TYPE = "key_type"; field public static final String KEY_USER_SENTIMENT = "key_user_sentiment"; diff --git a/core/api/test-current.txt b/core/api/test-current.txt index f06bd48a8cd8..36ef4f5f06ee 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -478,8 +478,8 @@ package android.app { method public void destroy(); method @NonNull public java.util.Set<java.lang.String> getAdoptedShellPermissions(); method @Deprecated public boolean grantRuntimePermission(String, String, android.os.UserHandle); - method public boolean injectInputEvent(@NonNull android.view.InputEvent, boolean, boolean); - method public void injectInputEventToInputFilter(@NonNull android.view.InputEvent); + method @Deprecated @FlaggedApi("com.android.input.flags.deprecate_uiautomation_input_injection") public boolean injectInputEvent(@NonNull android.view.InputEvent, boolean, boolean); + method @Deprecated @FlaggedApi("com.android.input.flags.deprecate_uiautomation_input_injection") public void injectInputEventToInputFilter(@NonNull android.view.InputEvent); method public boolean isNodeInCache(@NonNull android.view.accessibility.AccessibilityNodeInfo); method public void removeOverridePermissionState(int, @NonNull String); method @Deprecated public boolean revokeRuntimePermission(String, String, android.os.UserHandle); @@ -1634,6 +1634,10 @@ package android.hardware.camera2.params { method public void setColorSpace(@NonNull android.graphics.ColorSpace.Named); } + @FlaggedApi("com.android.internal.camera.flags.camera_multi_client") public final class SharedSessionConfiguration { + ctor public SharedSessionConfiguration(int, @NonNull long[]); + } + } package android.hardware.devicestate { diff --git a/core/java/android/app/ActivityTaskManager.java b/core/java/android/app/ActivityTaskManager.java index 16dcf2ad7e45..8d20e46c7df8 100644 --- a/core/java/android/app/ActivityTaskManager.java +++ b/core/java/android/app/ActivityTaskManager.java @@ -25,6 +25,7 @@ import android.annotation.SystemService; import android.annotation.TestApi; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; @@ -483,6 +484,19 @@ public class ActivityTaskManager { } /** + * @return Whether the app could be universal resizeable (assuming it's on a large screen and + * ignoring possible overrides) + * @hide + */ + public boolean canBeUniversalResizeable(@NonNull ApplicationInfo appInfo) { + try { + return getService().canBeUniversalResizeable(appInfo); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Detaches the navigation bar from the app it was attached to during a transition. * @hide */ diff --git a/core/java/android/app/IActivityTaskManager.aidl b/core/java/android/app/IActivityTaskManager.aidl index c6f62a21641d..4b1afa517122 100644 --- a/core/java/android/app/IActivityTaskManager.aidl +++ b/core/java/android/app/IActivityTaskManager.aidl @@ -158,6 +158,12 @@ interface IActivityTaskManager { void reportAssistContextExtras(in IBinder assistToken, in Bundle extras, in AssistStructure structure, in AssistContent content, in Uri referrer); + /** + * @return whether the app could be universal resizeable (assuming it's on a large screen and + * ignoring possible overrides) + */ + boolean canBeUniversalResizeable(in ApplicationInfo appInfo); + void setFocusedRootTask(int taskId); ActivityTaskManager.RootTaskInfo getFocusedRootTaskInfo(); Rect getTaskBounds(int taskId); diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index 8fa2362139a1..ff39329a0d2d 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -114,7 +114,6 @@ interface INotificationManager NotificationChannel getNotificationChannelForPackage(String pkg, int uid, String channelId, String conversationId, boolean includeDeleted); void deleteNotificationChannel(String pkg, String channelId); ParceledListSlice getNotificationChannels(String callingPkg, String targetPkg, int userId); - ParceledListSlice getOrCreateNotificationChannels(String callingPkg, String targetPkg, int userId, boolean createPrefsIfNeeded); ParceledListSlice getNotificationChannelsForPackage(String pkg, int uid, boolean includeDeleted); int getNumNotificationChannelsForPackage(String pkg, int uid, boolean includeDeleted); int getDeletedChannelCount(String pkg, int uid); diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index fcb817ede6b3..40db6dd1b0ba 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -1772,6 +1772,11 @@ public class Notification implements Parcelable */ public static final String EXTRA_FOREGROUND_APPS = "android.foregroundApps"; + /** + * @hide + */ + public static final String EXTRA_SUMMARIZED_CONTENT = "android.summarization"; + @UnsupportedAppUsage private Icon mSmallIcon; @UnsupportedAppUsage @@ -4393,6 +4398,9 @@ public class Notification implements Parcelable */ @Nullable public Pair<RemoteInput, Action> findRemoteInputActionPair(boolean requiresFreeform) { + if (isPromotedOngoing()) { + return null; + } if (actions == null) { return null; } @@ -6454,6 +6462,11 @@ public class Notification implements Parcelable if (mActions == null) return Collections.emptyList(); List<Notification.Action> standardActions = new ArrayList<>(); for (Notification.Action action : mActions) { + // Actions with RemoteInput are ignored for RONs. + if (mN.isPromotedOngoing() + && hasValidRemoteInput(action)) { + continue; + } if (!action.isContextual()) { standardActions.add(action); } diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index 1e1ec602d0a2..21dad28560df 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -1264,8 +1264,7 @@ public class NotificationManager { mNotificationChannelListCache.query(new NotificationChannelQuery( mContext.getOpPackageName(), mContext.getPackageName(), - mContext.getUserId(), - true))); // create (default channel) if needed + mContext.getUserId()))); } else { INotificationManager service = service(); try { @@ -1293,8 +1292,7 @@ public class NotificationManager { mNotificationChannelListCache.query(new NotificationChannelQuery( mContext.getOpPackageName(), mContext.getPackageName(), - mContext.getUserId(), - true))); // create (default channel) if needed + mContext.getUserId()))); } else { INotificationManager service = service(); try { @@ -1320,8 +1318,7 @@ public class NotificationManager { return mNotificationChannelListCache.query(new NotificationChannelQuery( mContext.getOpPackageName(), mContext.getPackageName(), - mContext.getUserId(), - false)); + mContext.getUserId())); } else { INotificationManager service = service(); try { @@ -1461,8 +1458,8 @@ public class NotificationManager { public List<NotificationChannel> apply(NotificationChannelQuery query) { INotificationManager service = service(); try { - return service.getOrCreateNotificationChannels(query.callingPkg, - query.targetPkg, query.userId, query.createIfNeeded).getList(); + return service.getNotificationChannels(query.callingPkg, + query.targetPkg, query.userId).getList(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1490,8 +1487,7 @@ public class NotificationManager { private record NotificationChannelQuery( String callingPkg, String targetPkg, - int userId, - boolean createIfNeeded) {} + int userId) {} /** * @hide diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java index b7285c38290c..01868cc601fe 100644 --- a/core/java/android/app/StatusBarManager.java +++ b/core/java/android/app/StatusBarManager.java @@ -60,6 +60,7 @@ import com.android.internal.statusbar.NotificationVisibility; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -195,12 +196,40 @@ public class StatusBarManager { */ private static final int DEFAULT_SIM_LOCKED_DISABLED_FLAGS = DISABLE_EXPAND; - /** @hide */ - public static final int NAVIGATION_HINT_BACK_ALT = 1 << 0; - /** @hide */ - public static final int NAVIGATION_HINT_IME_SHOWN = 1 << 1; - /** @hide */ - public static final int NAVIGATION_HINT_IME_SWITCHER_SHOWN = 1 << 2; + /** + * The back button is visually adjusted to indicate that it will dismiss the IME when pressed. + * This only takes effect while the IME is visible. By default, it is set while the IME is + * visible, but may be overridden by the + * {@link android.inputmethodservice.InputMethodService.BackDispositionMode backDispositionMode} + * set by the IME. + * + * @hide + */ + public static final int NAVBAR_BACK_DISMISS_IME = 1 << 0; + /** + * The IME is visible. + * + * @hide + */ + public static final int NAVBAR_IME_VISIBLE = 1 << 1; + /** + * The IME Switcher button is visible. This only takes effect while the IME is visible. + * + * @hide + */ + public static final int NAVBAR_IME_SWITCHER_BUTTON_VISIBLE = 1 << 2; + /** + * Navigation bar state flags. + * + * @hide + */ + @IntDef(flag = true, prefix = { "NAVBAR_" }, value = { + NAVBAR_BACK_DISMISS_IME, + NAVBAR_IME_VISIBLE, + NAVBAR_IME_SWITCHER_BUTTON_VISIBLE, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface NavbarFlags {} /** @hide */ public static final int WINDOW_STATUS_BAR = 1; @@ -1325,6 +1354,22 @@ public class StatusBarManager { } /** @hide */ + @NonNull + public static String navbarFlagsToString(@NavbarFlags int flags) { + final var flagStrings = new ArrayList<String>(); + if ((flags & NAVBAR_BACK_DISMISS_IME) != 0) { + flagStrings.add("NAVBAR_BACK_DISMISS_IME"); + } + if ((flags & NAVBAR_IME_VISIBLE) != 0) { + flagStrings.add("NAVBAR_IME_VISIBLE"); + } + if ((flags & NAVBAR_IME_SWITCHER_BUTTON_VISIBLE) != 0) { + flagStrings.add("NAVBAR_IME_SWITCHER_BUTTON_VISIBLE"); + } + return String.join(" | ", flagStrings); + } + + /** @hide */ public static String windowStateToString(int state) { if (state == WINDOW_STATE_HIDING) return "WINDOW_STATE_HIDING"; if (state == WINDOW_STATE_HIDDEN) return "WINDOW_STATE_HIDDEN"; diff --git a/core/java/android/app/UiAutomation.java b/core/java/android/app/UiAutomation.java index 6f8e335cff80..8021ab4865af 100644 --- a/core/java/android/app/UiAutomation.java +++ b/core/java/android/app/UiAutomation.java @@ -18,6 +18,8 @@ package android.app; import static android.view.Display.DEFAULT_DISPLAY; +import static com.android.input.flags.Flags.FLAG_DEPRECATE_UIAUTOMATION_INPUT_INJECTION; + import android.accessibilityservice.AccessibilityGestureEvent; import android.accessibilityservice.AccessibilityService; import android.accessibilityservice.AccessibilityService.Callbacks; @@ -26,6 +28,7 @@ import android.accessibilityservice.AccessibilityServiceInfo; import android.accessibilityservice.IAccessibilityServiceClient; import android.accessibilityservice.IAccessibilityServiceConnection; import android.accessibilityservice.MagnificationConfig; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -108,7 +111,10 @@ import java.util.concurrent.TimeoutException; * client should be using a higher-level library or implement high-level functions. * For example, performing a tap on the screen requires construction and injecting * of a touch down and up events which have to be delivered to the system by a - * call to {@link #injectInputEvent(InputEvent, boolean)}. + * call to {@link #injectInputEvent(InputEvent, boolean)}. <strong>Note:</strong> For CTS tests, it + * is preferable to inject input events using uinput (com.android.cts.input.UinputDevice) or hid + * devices (com.android.cts.input.HidDevice). Alternatively, use InjectInputInProcess + * (com.android.cts.input.InjectInputInProcess) for in-process injection. * </p> * <p> * The APIs exposed by this class operate across applications enabling a client @@ -222,7 +228,8 @@ public final class UiAutomation { private OnAccessibilityEventListener mOnAccessibilityEventListener; - private boolean mWaitingForEventDelivery; + // Count the nested clients waiting for data delivery + private int mCurrentEventWatchersCount = 0; private long mLastEventTimeMillis; @@ -956,9 +963,17 @@ public final class UiAutomation { * <strong>Note:</strong> It is caller's responsibility to recycle the event. * </p> * - * @param event The event to inject. - * @param sync Whether to inject the event synchronously. - * @return Whether event injection succeeded. + * <p> + * <strong>Note:</strong> Avoid this method when injecting input events in CTS tests. Instead + * use uinput (com.android.cts.input.UinputDevice) + * or hid devices (com.android.cts.input.HidDevice), as they provide a more accurate simulation + * of real device behavior. Alternatively, InjectInputInProcess + * (com.android.cts.input.InjectInputProcess) can be used for in-process injection. + * </p> + * + * @param event the event to inject + * @param sync whether to inject the event synchronously + * @return {@code true} if event injection succeeded */ public boolean injectInputEvent(InputEvent event, boolean sync) { return injectInputEvent(event, sync, true /* waitForAnimations */); @@ -971,15 +986,21 @@ public final class UiAutomation { * <strong>Note:</strong> It is caller's responsibility to recycle the event. * </p> * - * @param event The event to inject. - * @param sync Whether to inject the event synchronously. - * @param waitForAnimations Whether to wait for all window container animations and surface - * operations to complete. - * @return Whether event injection succeeded. + * @param event the event to inject + * @param sync whether to inject the event synchronously. + * @param waitForAnimations whether to wait for all window container animations and surface + * operations to complete + * @return {@code true} if event injection succeeded * + * @deprecated for CTS tests prefer inject input events using uinput + * (com.android.cts.input.UinputDevice) or hid devices (com.android.cts.input.HidDevice). + * Alternatively, InjectInputInProcess (com.android.cts.input.InjectInputProcess) can be used + * for in-process injection. * @hide */ @TestApi + @Deprecated // Deprecated for CTS tests + @FlaggedApi(FLAG_DEPRECATE_UIAUTOMATION_INPUT_INJECTION) public boolean injectInputEvent(@NonNull InputEvent event, boolean sync, boolean waitForAnimations) { try { @@ -1002,9 +1023,15 @@ public final class UiAutomation { * Events injected to the input subsystem using the standard {@link #injectInputEvent} method * skip the accessibility input filter to avoid feedback loops. * + * @deprecated for CTS tests prefer inject input events using uinput + * (com.android.cts.input.UinputDevice) or hid devices (com.android.cts.input.HidDevice). + * Alternatively, InjectInputInProcess (com.android.cts.input.InjectInputProcess) can be used + * for in-process injection. * @hide */ @TestApi + @Deprecated + @FlaggedApi(FLAG_DEPRECATE_UIAUTOMATION_INPUT_INJECTION) public void injectInputEventToInputFilter(@NonNull InputEvent event) { try { mUiAutomationConnection.injectInputEventToInputFilter(event); @@ -1132,75 +1159,75 @@ public final class UiAutomation { */ public AccessibilityEvent executeAndWaitForEvent(Runnable command, AccessibilityEventFilter filter, long timeoutMillis) throws TimeoutException { + int watchersDepth; + // Track events added after the index for this command, it is to support nested calls. + // This doesn't support concurrent calls correctly. + int eventQueueStartIndex; + final long executionStartTimeMillis; + // Acquire the lock and prepare for receiving events. synchronized (mLock) { throwIfNotConnectedLocked(); - mEventQueue.clear(); - // Prepare to wait for an event. - mWaitingForEventDelivery = true; + watchersDepth = ++mCurrentEventWatchersCount; + executionStartTimeMillis = SystemClock.uptimeMillis(); + eventQueueStartIndex = mEventQueue.size(); + } + if (VERBOSE) { + Log.v(LOG_TAG, "executeAndWaitForEvent starts at depth=" + watchersDepth + ", " + + "command=" + command + ", filter=" + filter + ", timeout=" + timeoutMillis); } - // Note: We have to release the lock since calling out with this lock held - // can bite. We will correctly filter out events from other interactions, - // so starting to collect events before running the action is just fine. - - // We will ignore events from previous interactions. - final long executionStartTimeMillis = SystemClock.uptimeMillis(); - // Execute the command *without* the lock being held. - command.run(); - - List<AccessibilityEvent> receivedEvents = new ArrayList<>(); - - // Acquire the lock and wait for the event. try { - // Wait for the event. - final long startTimeMillis = SystemClock.uptimeMillis(); - while (true) { - List<AccessibilityEvent> localEvents = new ArrayList<>(); - synchronized (mLock) { - localEvents.addAll(mEventQueue); - mEventQueue.clear(); - } - // Drain the event queue - while (!localEvents.isEmpty()) { - AccessibilityEvent event = localEvents.remove(0); - // Ignore events from previous interactions. - if (event.getEventTime() < executionStartTimeMillis) { - continue; - } - if (filter.accept(event)) { - return event; - } - receivedEvents.add(event); - } - // Check if timed out and if not wait. - final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; - final long remainingTimeMillis = timeoutMillis - elapsedTimeMillis; - if (remainingTimeMillis <= 0) { - throw new TimeoutException("Expected event not received within: " - + timeoutMillis + " ms among: " + receivedEvents); + // Execute the command *without* the lock being held. + command.run(); + synchronized (mLock) { + if (watchersDepth != mCurrentEventWatchersCount) { + throw new IllegalStateException("Unexpected event watchers count, expected: " + + watchersDepth + ", actual: " + mCurrentEventWatchersCount); } + } + final long startTimeMillis = SystemClock.uptimeMillis(); + List<AccessibilityEvent> receivedEvents = new ArrayList<>(); + long elapsedTimeMillis = 0; + int currentQueueSize = 0; + while (timeoutMillis > elapsedTimeMillis) { + AccessibilityEvent event = null; synchronized (mLock) { - if (mEventQueue.isEmpty()) { + currentQueueSize = mEventQueue.size(); + if (eventQueueStartIndex < currentQueueSize) { + event = mEventQueue.get(eventQueueStartIndex++); + } else { try { - mLock.wait(remainingTimeMillis); + mLock.wait(timeoutMillis - elapsedTimeMillis); } catch (InterruptedException ie) { /* ignore */ } } } + elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; + if (event == null || event.getEventTime() < executionStartTimeMillis) { + continue; + } + if (filter.accept(event)) { + return event; + } + receivedEvents.add(event); } - } finally { - int size = receivedEvents.size(); - for (int i = 0; i < size; i++) { - receivedEvents.get(i).recycle(); + if (eventQueueStartIndex < currentQueueSize) { + Log.w(LOG_TAG, "Timed out before reading all events from the queue"); } - + throw new TimeoutException("Expected event not received before timeout, events: " + + receivedEvents); + } finally { synchronized (mLock) { - mWaitingForEventDelivery = false; - mEventQueue.clear(); + if (--mCurrentEventWatchersCount == 0) { + mEventQueue.clear(); + } mLock.notifyAll(); } + if (VERBOSE) { + Log.v(LOG_TAG, "executeAndWaitForEvent ends at depth=" + watchersDepth); + } } } @@ -1957,7 +1984,7 @@ public final class UiAutomation { // It is not guaranteed that the accessibility framework sends events by the // order of event timestamp. mLastEventTimeMillis = Math.max(mLastEventTimeMillis, event.getEventTime()); - if (mWaitingForEventDelivery) { + if (mCurrentEventWatchersCount > 0) { mEventQueue.add(AccessibilityEvent.obtain(event)); } mLock.notifyAll(); diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig index 914ca73f1ce4..733a348aa825 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -62,13 +62,6 @@ flag { } flag { - name: "modes_ui_test" - namespace: "systemui" - description: "Guards new CTS tests for Modes; dependent on flags modes_api and modes_ui" - bug: "360862012" -} - -flag { name: "modes_hsum" namespace: "systemui" description: "Fixes for modes (and DND/Zen in general) with HSUM or secondary users" @@ -310,3 +303,17 @@ flag { description: "removes sbnholder from NLS" bug: "362981561" } + +flag { + name: "nm_summarization" + namespace: "systemui" + description: "Allows the NAS to summarize notifications" + bug: "390417189" +} + +flag { + name: "nm_summarization_ui" + namespace: "systemui" + description: "Shows summarized notifications in the UI" + bug: "390217880" +} diff --git a/core/java/android/app/supervision/ISupervisionManager.aidl b/core/java/android/app/supervision/ISupervisionManager.aidl index 4598421eb3bc..c3f3b1ced33c 100644 --- a/core/java/android/app/supervision/ISupervisionManager.aidl +++ b/core/java/android/app/supervision/ISupervisionManager.aidl @@ -22,4 +22,5 @@ package android.app.supervision; */ interface ISupervisionManager { boolean isSupervisionEnabledForUser(int userId); + String getActiveSupervisionAppPackage(int userId); } diff --git a/core/java/android/app/supervision/SupervisionManager.java b/core/java/android/app/supervision/SupervisionManager.java index 92241f3634e8..12432ddd0eb9 100644 --- a/core/java/android/app/supervision/SupervisionManager.java +++ b/core/java/android/app/supervision/SupervisionManager.java @@ -16,6 +16,7 @@ package android.app.supervision; +import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemService; import android.annotation.UserHandleAware; @@ -98,4 +99,20 @@ public class SupervisionManager { throw e.rethrowFromSystemServer(); } } + + /** + * Returns the package name of the app that is acting as the active supervision app or null if + * supervision is disabled. + * + * @hide + */ + @UserHandleAware + @Nullable + public String getActiveSupervisionAppPackage() { + try { + return mService.getActiveSupervisionAppPackage(mContext.getUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } diff --git a/core/java/android/app/supervision/flags.aconfig b/core/java/android/app/supervision/flags.aconfig index 18182b804627..232883cbfe00 100644 --- a/core/java/android/app/supervision/flags.aconfig +++ b/core/java/android/app/supervision/flags.aconfig @@ -48,3 +48,19 @@ flag { description: "Flag that enables the Supervision settings screen with top-level Android settings entry point" bug: "383404606" } + +flag { + name: "enable_app_approval" + is_exported: true + namespace: "supervision" + description: "Flag to enable the App Approval settings in Android settings UI" + bug: "390185393" +} + +flag { + name: "enable_supervision_pin_recovery_screen" + is_exported: true + namespace: "supervision" + description: "Flag that enables the Supervision pin recovery screen with Supervision settings entry point" + bug: "390500290" +} diff --git a/core/java/android/appwidget/AppWidgetHostView.java b/core/java/android/appwidget/AppWidgetHostView.java index df1028e9e04c..b9b5c6a8bbc3 100644 --- a/core/java/android/appwidget/AppWidgetHostView.java +++ b/core/java/android/appwidget/AppWidgetHostView.java @@ -20,7 +20,6 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Activity; import android.app.ActivityOptions; -import android.app.LoadedApk; import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.content.Context; @@ -753,9 +752,6 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW */ protected Context getRemoteContextEnsuringCorrectCachedApkPath() { try { - ApplicationInfo expectedAppInfo = mInfo.providerInfo.applicationInfo; - LoadedApk.checkAndUpdateApkPaths(expectedAppInfo); - // Return if cloned successfully, otherwise default Context newContext = mContext.createApplicationContext( mInfo.providerInfo.applicationInfo, Context.CONTEXT_RESTRICTED); diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig index 6da2a073ec19..1cf42820f356 100644 --- a/core/java/android/companion/virtual/flags/flags.aconfig +++ b/core/java/android/companion/virtual/flags/flags.aconfig @@ -18,13 +18,6 @@ flag { } flag { - namespace: "virtual_devices" - name: "media_projection_keyguard_restrictions" - description: "Auto-stop MP when the device locks" - bug: "348335290" -} - -flag { namespace: "virtual_devices" name: "virtual_display_insets" description: "APIs for specifying virtual display insets (via cutout)" diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index a126363237b8..efcaa0ea6f07 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -2722,10 +2722,10 @@ public abstract class ContentResolver implements ContentInterface { /** @hide - designated user version */ @UnsupportedAppUsage - public final void registerContentObserver(Uri uri, boolean notifyForDescendents, + public final void registerContentObserver(Uri uri, boolean notifyForDescendants, ContentObserver observer, @UserIdInt int userHandle) { try { - getContentService().registerContentObserver(uri, notifyForDescendents, + getContentService().registerContentObserver(uri, notifyForDescendants, observer.getContentObserver(), userHandle, mTargetSdkVersion); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 8d54673df74c..d811c0791c6c 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -4964,10 +4964,10 @@ public abstract class Context { /** * Use with {@link #getSystemService(String)} to retrieve a {@link android.net.TetheringManager} * for managing tethering functions. - * @hide + * * @see android.net.TetheringManager */ - @SystemApi + @SuppressLint("UnflaggedApi") public static final String TETHERING_SERVICE = "tethering"; /** diff --git a/core/java/android/content/pm/UserInfo.java b/core/java/android/content/pm/UserInfo.java index 8a3a3ad56a7b..582a1a9442ce 100644 --- a/core/java/android/content/pm/UserInfo.java +++ b/core/java/android/content/pm/UserInfo.java @@ -183,6 +183,12 @@ public class UserInfo implements Parcelable { * * <p>This is not necessarily the system user. For example, it will not be the system user on * devices for which {@link UserManager#isHeadlessSystemUserMode()} returns true. + * + * <p>NB: Features should ideally not limit functionality to the main user. Ideally, they + * should either work for all users or for all admin users. If a feature should only work for + * select users, its determination of which user should be done intelligently or be + * customizable. Not all devices support a main user, and the idea of singling out one user as + * special is contrary to overall multiuser goals. */ public static final int FLAG_MAIN = 0x00004000; diff --git a/core/java/android/credentials/flags.aconfig b/core/java/android/credentials/flags.aconfig index 430ed2b68342..449423f1ea1f 100644 --- a/core/java/android/credentials/flags.aconfig +++ b/core/java/android/credentials/flags.aconfig @@ -133,6 +133,7 @@ flag { metadata { purpose: PURPOSE_BUGFIX } + is_exported: true } flag { diff --git a/core/java/android/hardware/biometrics/ParentalControlsUtilsInternal.java b/core/java/android/hardware/biometrics/ParentalControlsUtilsInternal.java index de93234445ca..d3fb93588762 100644 --- a/core/java/android/hardware/biometrics/ParentalControlsUtilsInternal.java +++ b/core/java/android/hardware/biometrics/ParentalControlsUtilsInternal.java @@ -19,6 +19,7 @@ package android.hardware.biometrics; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.admin.DevicePolicyManager; +import android.app.supervision.SupervisionManager; import android.content.ComponentName; import android.content.Context; import android.os.Build; @@ -55,27 +56,44 @@ public class ParentalControlsUtilsInternal { return null; } - public static boolean parentConsentRequired(@NonNull Context context, - @NonNull DevicePolicyManager dpm, @BiometricAuthenticator.Modality int modality, + /** @return true if parental consent is required in order for biometric sensors to be used. */ + public static boolean parentConsentRequired( + @NonNull Context context, + @NonNull DevicePolicyManager dpm, + @Nullable SupervisionManager sm, + @BiometricAuthenticator.Modality int modality, @NonNull UserHandle userHandle) { if (getTestComponentName(context, userHandle.getIdentifier()) != null) { return true; } - return parentConsentRequired(dpm, modality, userHandle); + return parentConsentRequired(dpm, sm, modality, userHandle); } /** * @return true if parental consent is required in order for biometric sensors to be used. */ - public static boolean parentConsentRequired(@NonNull DevicePolicyManager dpm, - @BiometricAuthenticator.Modality int modality, @NonNull UserHandle userHandle) { - final ComponentName cn = getSupervisionComponentName(dpm, userHandle); - if (cn == null) { - return false; + public static boolean parentConsentRequired( + @NonNull DevicePolicyManager dpm, + @Nullable SupervisionManager sm, + @BiometricAuthenticator.Modality int modality, + @NonNull UserHandle userHandle) { + final int keyguardDisabledFeatures; + + if (android.app.supervision.flags.Flags.deprecateDpmSupervisionApis()) { + if (sm != null && !sm.isSupervisionEnabledForUser(userHandle.getIdentifier())) { + return false; + } + // Check for keyguard features disabled by any admin. + keyguardDisabledFeatures = dpm.getKeyguardDisabledFeatures(/* admin= */ null); + } else { + final ComponentName cn = getSupervisionComponentName(dpm, userHandle); + if (cn == null) { + return false; + } + keyguardDisabledFeatures = dpm.getKeyguardDisabledFeatures(cn); } - final int keyguardDisabledFeatures = dpm.getKeyguardDisabledFeatures(cn); final boolean dpmFpDisabled = containsFlag(keyguardDisabledFeatures, DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT); final boolean dpmFaceDisabled = containsFlag(keyguardDisabledFeatures, @@ -97,7 +115,9 @@ public class ParentalControlsUtilsInternal { return consentRequired; } + /** @deprecated Use {@link SupervisionManager} to check for supervision. */ @Nullable + @Deprecated public static ComponentName getSupervisionComponentName(@NonNull DevicePolicyManager dpm, @NonNull UserHandle userHandle) { return dpm.getProfileOwnerOrDeviceOwnerSupervisionComponent(userHandle); diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java index 5533a640b9d8..210653bb41e5 100644 --- a/core/java/android/hardware/camera2/CameraCharacteristics.java +++ b/core/java/android/hardware/camera2/CameraCharacteristics.java @@ -5256,9 +5256,6 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri * <p>DYNAMIC_RANGE_PROFILE: {STANDARD, HLG10}</p> * </li> * </ul> - * <p>All of the above configurations can be set up with a SessionConfiguration. The list of - * OutputConfiguration contains the stream configurations and DYNAMIC_RANGE_PROFILE, and - * the AE_TARGET_FPS_RANGE and VIDEO_STABILIZATION_MODE are set as session parameters.</p> * <p>When set to BAKLAVA, the additional stream combinations below are verified * by the compliance tests:</p> * <table> @@ -5268,6 +5265,8 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri * <th style="text-align: center;">Size</th> * <th style="text-align: center;">Target 2</th> * <th style="text-align: center;">Size</th> + * <th style="text-align: center;">Target 3</th> + * <th style="text-align: center;">Size</th> * </tr> * </thead> * <tbody> @@ -5276,15 +5275,34 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri * <td style="text-align: center;">S1080P</td> * <td style="text-align: center;">PRIV</td> * <td style="text-align: center;">S1080P</td> + * <td style="text-align: center;"></td> + * <td style="text-align: center;"></td> * </tr> * <tr> * <td style="text-align: center;">PRIV</td> * <td style="text-align: center;">S1080P</td> * <td style="text-align: center;">PRIV</td> * <td style="text-align: center;">S1440P</td> + * <td style="text-align: center;"></td> + * <td style="text-align: center;"></td> + * </tr> + * <tr> + * <td style="text-align: center;">PRIV</td> + * <td style="text-align: center;">S1080P</td> + * <td style="text-align: center;">YUV</td> + * <td style="text-align: center;">S1080P</td> + * <td style="text-align: center;">S1080P</td> + * <td style="text-align: center;">PRIV</td> * </tr> * </tbody> * </table> + * <ul> + * <li>VIDEO_STABILIZATION_MODE: {OFF, ON} for the newly added stream combinations given the + * presence of dedicated video stream</li> + * </ul> + * <p>All of the above configurations can be set up with a SessionConfiguration. The list of + * OutputConfiguration contains the stream configurations and DYNAMIC_RANGE_PROFILE, and + * the AE_TARGET_FPS_RANGE and VIDEO_STABILIZATION_MODE are set as session parameters.</p> * <p>This key is available on all devices.</p> */ @PublicKey diff --git a/core/java/android/hardware/camera2/params/SharedSessionConfiguration.java b/core/java/android/hardware/camera2/params/SharedSessionConfiguration.java index 365f870ba22d..b40c7d35c06b 100644 --- a/core/java/android/hardware/camera2/params/SharedSessionConfiguration.java +++ b/core/java/android/hardware/camera2/params/SharedSessionConfiguration.java @@ -21,6 +21,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.annotation.SystemApi; +import android.annotation.TestApi; import android.graphics.ColorSpace; import android.graphics.ImageFormat.Format; import android.hardware.DataSpace.NamedDataSpace; @@ -228,6 +229,7 @@ public final class SharedSessionConfiguration { * * @hide */ + @TestApi public SharedSessionConfiguration(int sharedColorSpace, @NonNull long[] sharedOutputConfigurations) { mColorSpace = sharedColorSpace; diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig index 62126963cba4..79323bf2f2f7 100644 --- a/core/java/android/hardware/input/input_framework.aconfig +++ b/core/java/android/hardware/input/input_framework.aconfig @@ -225,3 +225,13 @@ flag { description: "Removes modifiers from the original key event that activated the fallback, ensuring that only the intended fallback event is sent." bug: "382545048" } + +flag { + name: "abort_slow_multi_press" + namespace: "wear_frameworks" + description: "If a press that's a part of a multipress takes too long, the multipress gesture will be cancelled." + bug: "370095426" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/hardware/usb/OWNERS b/core/java/android/hardware/usb/OWNERS index 37604bc2eb65..1de8a242acfc 100644 --- a/core/java/android/hardware/usb/OWNERS +++ b/core/java/android/hardware/usb/OWNERS @@ -1,7 +1,7 @@ # Bug component: 175220 -anothermark@google.com +vmartensson@google.com +nkapron@google.com febinthattil@google.com -aprasath@google.com +shubhankarm@google.com badhri@google.com -kumarashishg@google.com
\ No newline at end of file diff --git a/core/java/android/inputmethodservice/NavigationBarController.java b/core/java/android/inputmethodservice/NavigationBarController.java index 019ba0045916..f420b5d7b886 100644 --- a/core/java/android/inputmethodservice/NavigationBarController.java +++ b/core/java/android/inputmethodservice/NavigationBarController.java @@ -16,9 +16,9 @@ package android.inputmethodservice; -import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT; -import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN; -import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN; +import static android.app.StatusBarManager.NAVBAR_BACK_DISMISS_IME; +import static android.app.StatusBarManager.NAVBAR_IME_SWITCHER_BUTTON_VISIBLE; +import static android.app.StatusBarManager.NAVBAR_IME_VISIBLE; import static android.view.WindowInsets.Type.captionBar; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; @@ -242,11 +242,11 @@ final class NavigationBarController { NavigationBarView.class::isInstance); if (navigationBarView != null) { // TODO(b/213337792): Support InputMethodService#setBackDisposition(). - // TODO(b/213337792): Set NAVIGATION_HINT_IME_SHOWN only when necessary. - final int hints = NAVIGATION_HINT_BACK_ALT | NAVIGATION_HINT_IME_SHOWN + // TODO(b/213337792): Set NAVBAR_IME_VISIBLE only when necessary. + final int flags = NAVBAR_BACK_DISMISS_IME | NAVBAR_IME_VISIBLE | (mShouldShowImeSwitcherWhenImeIsShown - ? NAVIGATION_HINT_IME_SWITCHER_SHOWN : 0); - navigationBarView.setNavigationIconHints(hints); + ? NAVBAR_IME_SWITCHER_BUTTON_VISIBLE : 0); + navigationBarView.setNavbarFlags(flags); navigationBarView.prepareNavButtons(this); } } else { @@ -515,11 +515,11 @@ final class NavigationBarController { NavigationBarView.class::isInstance); if (navigationBarView != null) { // TODO(b/213337792): Support InputMethodService#setBackDisposition(). - // TODO(b/213337792): Set NAVIGATION_HINT_IME_SHOWN only when necessary. - final int hints = NAVIGATION_HINT_BACK_ALT | NAVIGATION_HINT_IME_SHOWN + // TODO(b/213337792): Set NAVBAR_IME_VISIBLE only when necessary. + final int flags = NAVBAR_BACK_DISMISS_IME | NAVBAR_IME_VISIBLE | (mShouldShowImeSwitcherWhenImeIsShown - ? NAVIGATION_HINT_IME_SWITCHER_SHOWN : 0); - navigationBarView.setNavigationIconHints(hints); + ? NAVBAR_IME_SWITCHER_BUTTON_VISIBLE : 0); + navigationBarView.setNavbarFlags(flags); } } else { uninstallNavigationBarFrameIfNecessary(); diff --git a/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java b/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java index e7e46a9482c8..960a5b33434a 100644 --- a/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java +++ b/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java @@ -16,6 +16,8 @@ package android.inputmethodservice.navigationbar; +import static android.app.StatusBarManager.NAVBAR_BACK_DISMISS_IME; +import static android.app.StatusBarManager.NAVBAR_IME_SWITCHER_BUTTON_VISIBLE; import static android.inputmethodservice.navigationbar.NavigationBarConstants.DARK_MODE_ICON_COLOR_SINGLE_TONE; import static android.inputmethodservice.navigationbar.NavigationBarConstants.LIGHT_MODE_ICON_COLOR_SINGLE_TONE; import static android.inputmethodservice.navigationbar.NavigationBarConstants.NAVBAR_BACK_BUTTON_IME_OFFSET; @@ -28,6 +30,7 @@ import android.annotation.DrawableRes; import android.annotation.FloatRange; import android.annotation.NonNull; import android.app.StatusBarManager; +import android.app.StatusBarManager.NavbarFlags; import android.content.Context; import android.content.res.Configuration; import android.graphics.Canvas; @@ -63,7 +66,8 @@ public final class NavigationBarView extends FrameLayout { private int mCurrentRotation = -1; int mDisabledFlags = 0; - int mNavigationIconHints = StatusBarManager.NAVIGATION_HINT_BACK_ALT; + @NavbarFlags + private int mNavbarFlags; private final int mNavBarMode = NAV_BAR_MODE_GESTURAL; private KeyButtonDrawable mBackIcon; @@ -241,10 +245,9 @@ public final class NavigationBarView extends FrameLayout { } private void orientBackButton(KeyButtonDrawable drawable) { - final boolean useAltBack = - (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0; + final boolean isBackDismissIme = (mNavbarFlags & NAVBAR_BACK_DISMISS_IME) != 0; final boolean isRtl = mConfiguration.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; - float degrees = useAltBack ? (isRtl ? 90 : -90) : 0; + float degrees = isBackDismissIme ? (isRtl ? 90 : -90) : 0; if (drawable.getRotation() == degrees) { return; } @@ -256,7 +259,7 @@ public final class NavigationBarView extends FrameLayout { // Animate the back button's rotation to the new degrees and only in portrait move up the // back button to line up with the other buttons - float targetY = useAltBack + float targetY = isBackDismissIme ? -dpToPx(NAVBAR_BACK_BUTTON_IME_OFFSET, getResources()) : 0; ObjectAnimator navBarAnimator = ObjectAnimator.ofPropertyValuesHolder(drawable, @@ -280,24 +283,26 @@ public final class NavigationBarView extends FrameLayout { } /** - * Updates the navigation icons based on {@code hints}. + * Sets the navigation bar state flags. * - * @param hints bit flags defined in {@link StatusBarManager}. + * @param flags the navigation bar state flags. */ - public void setNavigationIconHints(int hints) { - if (hints == mNavigationIconHints) return; - final boolean newBackAlt = (hints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0; - final boolean oldBackAlt = - (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0; - if (newBackAlt != oldBackAlt) { - //onBackAltChanged(newBackAlt); + public void setNavbarFlags(@NavbarFlags int flags) { + if (flags == mNavbarFlags) { + return; + } + final boolean backDismissIme = (flags & StatusBarManager.NAVBAR_BACK_DISMISS_IME) != 0; + final boolean oldBackDismissIme = + (mNavbarFlags & StatusBarManager.NAVBAR_BACK_DISMISS_IME) != 0; + if (backDismissIme != oldBackDismissIme) { + //onBackDismissImeChanged(backDismissIme); } if (DEBUG) { - android.widget.Toast.makeText(getContext(), "Navigation icon hints = " + hints, 500) + android.widget.Toast.makeText(getContext(), "Navbar flags = " + flags, 500) .show(); } - mNavigationIconHints = hints; + mNavbarFlags = flags; updateNavButtonIcons(); } @@ -311,10 +316,12 @@ public final class NavigationBarView extends FrameLayout { getImeSwitchButton().setImageDrawable(mImeSwitcherIcon); - // Update IME button visibility, a11y and rotate button always overrides the appearance - final boolean imeSwitcherVisible = - (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN) != 0; - getImeSwitchButton().setVisibility(imeSwitcherVisible ? View.VISIBLE : View.INVISIBLE); + // Update IME switcher button visibility, a11y and rotate button always overrides + // the appearance. + final boolean isImeSwitcherButtonVisible = + (mNavbarFlags & NAVBAR_IME_SWITCHER_BUTTON_VISIBLE) != 0; + getImeSwitchButton() + .setVisibility(isImeSwitcherButtonVisible ? View.VISIBLE : View.INVISIBLE); getBackButton().setVisibility(View.VISIBLE); getHomeHandle().setVisibility(View.INVISIBLE); diff --git a/core/java/android/os/BaseBundle.java b/core/java/android/os/BaseBundle.java index 1041041b2a27..1cf293d46350 100644 --- a/core/java/android/os/BaseBundle.java +++ b/core/java/android/os/BaseBundle.java @@ -45,8 +45,7 @@ import java.util.function.BiFunction; * {@link PersistableBundle} subclass. */ @android.ravenwood.annotation.RavenwoodKeepWholeClass -@SuppressWarnings("HiddenSuperclass") -public class BaseBundle implements Parcel.ClassLoaderProvider { +public class BaseBundle { /** @hide */ protected static final String TAG = "Bundle"; static final boolean DEBUG = false; @@ -300,9 +299,8 @@ public class BaseBundle implements Parcel.ClassLoaderProvider { /** * Return the ClassLoader currently associated with this Bundle. - * @hide */ - public ClassLoader getClassLoader() { + ClassLoader getClassLoader() { return mClassLoader; } @@ -416,9 +414,6 @@ public class BaseBundle implements Parcel.ClassLoaderProvider { if ((mFlags & Bundle.FLAG_VERIFY_TOKENS_PRESENT) != 0) { Intent.maybeMarkAsMissingCreatorToken(object); } - } else if (object instanceof Bundle) { - Bundle bundle = (Bundle) object; - bundle.setClassLoaderSameAsContainerBundleWhenRetrievedFirstTime(this); } return (clazz != null) ? clazz.cast(object) : (T) object; } @@ -492,7 +487,7 @@ public class BaseBundle implements Parcel.ClassLoaderProvider { int[] numLazyValues = new int[]{0}; try { parcelledData.readArrayMap(map, count, !parcelledByNative, - /* lazy */ ownsParcel, this, numLazyValues); + /* lazy */ ownsParcel, mClassLoader, numLazyValues); } catch (BadParcelableException e) { if (sShouldDefuse) { Log.w(TAG, "Failed to parse Bundle, but defusing quietly", e); diff --git a/core/java/android/os/BatteryUsageStats.java b/core/java/android/os/BatteryUsageStats.java index f913fcfd56d4..86b8fad16275 100644 --- a/core/java/android/os/BatteryUsageStats.java +++ b/core/java/android/os/BatteryUsageStats.java @@ -161,6 +161,7 @@ public final class BatteryUsageStats implements Parcelable, Closeable { private final List<UserBatteryConsumer> mUserBatteryConsumers; private final AggregateBatteryConsumer[] mAggregateBatteryConsumers; private final BatteryStatsHistory mBatteryStatsHistory; + private final long mPreferredHistoryDurationMs; private final BatteryConsumer.BatteryConsumerDataLayout mBatteryConsumerDataLayout; private CursorWindow mBatteryConsumersCursorWindow; @@ -174,6 +175,7 @@ public final class BatteryUsageStats implements Parcelable, Closeable { mDischargedPowerUpperBound = builder.mDischargedPowerUpperBoundMah; mDischargeDurationMs = builder.mDischargeDurationMs; mBatteryStatsHistory = builder.mBatteryStatsHistory; + mPreferredHistoryDurationMs = builder.mPreferredHistoryDurationMs; mBatteryTimeRemainingMs = builder.mBatteryTimeRemainingMs; mChargeTimeRemainingMs = builder.mChargeTimeRemainingMs; mCustomPowerComponentNames = builder.mCustomPowerComponentNames; @@ -402,8 +404,10 @@ public final class BatteryUsageStats implements Parcelable, Closeable { if (source.readBoolean()) { mBatteryStatsHistory = BatteryStatsHistory.createFromBatteryUsageStatsParcel(source); + mPreferredHistoryDurationMs = source.readLong(); } else { mBatteryStatsHistory = null; + mPreferredHistoryDurationMs = 0; } } @@ -428,7 +432,7 @@ public final class BatteryUsageStats implements Parcelable, Closeable { if (mBatteryStatsHistory != null) { dest.writeBoolean(true); - mBatteryStatsHistory.writeToBatteryUsageStatsParcel(dest); + mBatteryStatsHistory.writeToBatteryUsageStatsParcel(dest, mPreferredHistoryDurationMs); } else { dest.writeBoolean(false); } @@ -919,6 +923,7 @@ public final class BatteryUsageStats implements Parcelable, Closeable { private final SparseArray<UserBatteryConsumer.Builder> mUserBatteryConsumerBuilders = new SparseArray<>(); private BatteryStatsHistory mBatteryStatsHistory; + private long mPreferredHistoryDurationMs; public Builder(@NonNull String[] customPowerComponentNames) { this(customPowerComponentNames, false, false, false, 0); @@ -1092,8 +1097,10 @@ public final class BatteryUsageStats implements Parcelable, Closeable { * Sets the parceled recent history. */ @NonNull - public Builder setBatteryHistory(BatteryStatsHistory batteryStatsHistory) { + public Builder setBatteryHistory(BatteryStatsHistory batteryStatsHistory, + long preferredHistoryDurationMs) { mBatteryStatsHistory = batteryStatsHistory; + mPreferredHistoryDurationMs = preferredHistoryDurationMs; return this; } diff --git a/core/java/android/os/BatteryUsageStatsQuery.java b/core/java/android/os/BatteryUsageStatsQuery.java index 6e67578fadc8..5aed39bd8fa6 100644 --- a/core/java/android/os/BatteryUsageStatsQuery.java +++ b/core/java/android/os/BatteryUsageStatsQuery.java @@ -25,6 +25,7 @@ import com.android.internal.os.MonotonicClock; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Arrays; +import java.util.concurrent.TimeUnit; /** * Query parameters for the {@link BatteryStatsManager#getBatteryUsageStats()} call. @@ -77,6 +78,7 @@ public final class BatteryUsageStatsQuery implements Parcelable { public static final int FLAG_BATTERY_USAGE_STATS_ACCUMULATED = 0x0080; private static final long DEFAULT_MAX_STATS_AGE_MS = 5 * 60 * 1000; + private static final long DEFAULT_PREFERRED_HISTORY_DURATION_MS = TimeUnit.HOURS.toMillis(2); private final int mFlags; @NonNull @@ -89,6 +91,7 @@ public final class BatteryUsageStatsQuery implements Parcelable { private long mMonotonicEndTime; private final double mMinConsumedPowerThreshold; private final @BatteryConsumer.PowerComponentId int[] mPowerComponents; + private final long mPreferredHistoryDurationMs; private BatteryUsageStatsQuery(@NonNull Builder builder) { mFlags = builder.mFlags; @@ -101,6 +104,7 @@ public final class BatteryUsageStatsQuery implements Parcelable { mMonotonicStartTime = builder.mMonotonicStartTime; mMonotonicEndTime = builder.mMonotonicEndTime; mPowerComponents = builder.mPowerComponents; + mPreferredHistoryDurationMs = builder.mPreferredHistoryDurationMs; } @BatteryUsageStatsFlags @@ -197,6 +201,13 @@ public final class BatteryUsageStatsQuery implements Parcelable { return mAggregatedToTimestamp; } + /** + * Returns the preferred duration of battery history (tail) to be included in the query result. + */ + public long getPreferredHistoryDurationMs() { + return mPreferredHistoryDurationMs; + } + @Override public String toString() { return "BatteryUsageStatsQuery{" @@ -209,6 +220,7 @@ public final class BatteryUsageStatsQuery implements Parcelable { + ", mMonotonicEndTime=" + mMonotonicEndTime + ", mMinConsumedPowerThreshold=" + mMinConsumedPowerThreshold + ", mPowerComponents=" + Arrays.toString(mPowerComponents) + + ", mMaxHistoryDurationMs=" + mPreferredHistoryDurationMs + '}'; } @@ -223,6 +235,7 @@ public final class BatteryUsageStatsQuery implements Parcelable { mAggregatedFromTimestamp = in.readLong(); mAggregatedToTimestamp = in.readLong(); mPowerComponents = in.createIntArray(); + mPreferredHistoryDurationMs = in.readLong(); } @Override @@ -237,6 +250,7 @@ public final class BatteryUsageStatsQuery implements Parcelable { dest.writeLong(mAggregatedFromTimestamp); dest.writeLong(mAggregatedToTimestamp); dest.writeIntArray(mPowerComponents); + dest.writeLong(mPreferredHistoryDurationMs); } @Override @@ -271,6 +285,7 @@ public final class BatteryUsageStatsQuery implements Parcelable { private long mAggregateToTimestamp; private double mMinConsumedPowerThreshold = 0; private @BatteryConsumer.PowerComponentId int[] mPowerComponents; + private long mPreferredHistoryDurationMs = DEFAULT_PREFERRED_HISTORY_DURATION_MS; /** * Builds a read-only BatteryUsageStatsQuery object. @@ -311,6 +326,16 @@ public final class BatteryUsageStatsQuery implements Parcelable { } /** + * Set the preferred amount of battery history to be included in the result, provided + * that `includeBatteryHistory` is also called. The actual amount of history included in + * the result may vary for performance reasons and may exceed the specified preference. + */ + public Builder setPreferredHistoryDurationMs(long preferredHistoryDurationMs) { + mPreferredHistoryDurationMs = preferredHistoryDurationMs; + return this; + } + + /** * Requests that per-process state data be included in the BatteryUsageStats, if * available. Check {@link BatteryUsageStats#isProcessStateDataIncluded()} on the result * to see if the data is available. diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java index ee62dea7f9e5..6b1e918a3c47 100644 --- a/core/java/android/os/Binder.java +++ b/core/java/android/os/Binder.java @@ -149,6 +149,11 @@ public class Binder implements IBinder { private static volatile boolean sStackTrackingEnabled = false; /** + * The extension binder object + */ + private IBinder mExtension = null; + + /** * Enable Binder IPC stack tracking. If enabled, every binder transaction will be logged to * {@link TransactionTracker}. * @@ -1237,7 +1242,9 @@ public class Binder implements IBinder { /** @hide */ @Override - public final native @Nullable IBinder getExtension(); + public final @Nullable IBinder getExtension() { + return mExtension; + } /** * Set the binder extension. @@ -1245,7 +1252,12 @@ public class Binder implements IBinder { * * @hide */ - public final native void setExtension(@Nullable IBinder extension); + public final void setExtension(@Nullable IBinder extension) { + mExtension = extension; + setExtensionNative(extension); + } + + private final native void setExtensionNative(@Nullable IBinder extension); /** * Default implementation rewinds the parcels and calls onTransact. On diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java index 55bfd451d97a..819d58d9f059 100644 --- a/core/java/android/os/Bundle.java +++ b/core/java/android/os/Bundle.java @@ -141,8 +141,6 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { STRIPPED.putInt("STRIPPED", 1); } - private boolean isFirstRetrievedFromABundle = false; - /** * Constructs a new, empty Bundle. */ @@ -1022,9 +1020,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { return null; } try { - Bundle bundle = (Bundle) o; - bundle.setClassLoaderSameAsContainerBundleWhenRetrievedFirstTime(this); - return bundle; + return (Bundle) o; } catch (ClassCastException e) { typeWarning(key, o, "Bundle", e); return null; @@ -1032,21 +1028,6 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { } /** - * Set the ClassLoader of a bundle to its container bundle. This is necessary so that when a - * bundle's ClassLoader is changed, it can be propagated to its children. Do this only when it - * is retrieved from the container bundle first time though. Once it is accessed outside of its - * container, its ClassLoader should no longer be changed by its container anymore. - * - * @param containerBundle the bundle this bundle is retrieved from. - */ - void setClassLoaderSameAsContainerBundleWhenRetrievedFirstTime(BaseBundle containerBundle) { - if (!isFirstRetrievedFromABundle) { - setClassLoader(containerBundle.getClassLoader()); - isFirstRetrievedFromABundle = true; - } - } - - /** * Returns the value associated with the given key, or {@code null} if * no mapping of the desired type exists for the given key or a {@code null} * value is explicitly associated with the key. diff --git a/core/java/android/os/CombinedMessageQueue/MessageQueue.java b/core/java/android/os/CombinedMessageQueue/MessageQueue.java index 50b621c46778..877f130a8b5a 100644 --- a/core/java/android/os/CombinedMessageQueue/MessageQueue.java +++ b/core/java/android/os/CombinedMessageQueue/MessageQueue.java @@ -39,7 +39,6 @@ import android.util.proto.ProtoOutputStream; import com.android.internal.ravenwood.RavenwoodEnvironment; import dalvik.annotation.optimization.NeverCompile; -import dalvik.annotation.optimization.NeverInline; import java.io.FileDescriptor; import java.lang.annotation.Retention; @@ -238,7 +237,6 @@ public final class MessageQueue { private final MatchDeliverableMessages mMatchDeliverableMessages = new MatchDeliverableMessages(); - @NeverInline private boolean isIdleConcurrent() { final long now = SystemClock.uptimeMillis(); @@ -269,7 +267,6 @@ public final class MessageQueue { return true; } - @NeverInline private boolean isIdleLegacy() { synchronized (this) { final long now = SystemClock.uptimeMillis(); @@ -292,14 +289,12 @@ public final class MessageQueue { } } - @NeverInline private void addIdleHandlerConcurrent(@NonNull IdleHandler handler) { synchronized (mIdleHandlersLock) { mIdleHandlers.add(handler); } } - @NeverInline private void addIdleHandlerLegacy(@NonNull IdleHandler handler) { synchronized (this) { mIdleHandlers.add(handler); @@ -326,15 +321,11 @@ public final class MessageQueue { addIdleHandlerLegacy(handler); } } - - @NeverInline private void removeIdleHandlerConcurrent(@NonNull IdleHandler handler) { synchronized (mIdleHandlersLock) { mIdleHandlers.remove(handler); } } - - @NeverInline private void removeIdleHandlerLegacy(@NonNull IdleHandler handler) { synchronized (this) { mIdleHandlers.remove(handler); @@ -358,14 +349,12 @@ public final class MessageQueue { } } - @NeverInline private boolean isPollingConcurrent() { // If the loop is quitting then it must not be idling. // We can assume mPtr != 0 when sQuitting is false. return !((boolean) sQuitting.getVolatile(this)) && nativeIsPolling(mPtr); } - @NeverInline private boolean isPollingLegacy() { synchronized (this) { return isPollingLocked(); @@ -396,7 +385,6 @@ public final class MessageQueue { // We can assume mPtr != 0 when mQuitting is false. return !mQuitting && nativeIsPolling(mPtr); } - @NeverInline private void addOnFileDescriptorEventListenerConcurrent(@NonNull FileDescriptor fd, @OnFileDescriptorEventListener.Events int events, @NonNull OnFileDescriptorEventListener listener) { @@ -405,7 +393,6 @@ public final class MessageQueue { } } - @NeverInline private void addOnFileDescriptorEventListenerLegacy(@NonNull FileDescriptor fd, @OnFileDescriptorEventListener.Events int events, @NonNull OnFileDescriptorEventListener listener) { @@ -455,14 +442,12 @@ public final class MessageQueue { } } - @NeverInline private void removeOnFileDescriptorEventListenerConcurrent(@NonNull FileDescriptor fd) { synchronized (mFileDescriptorRecordsLock) { updateOnFileDescriptorEventListenerLocked(fd, 0, null); } } - @NeverInline private void removeOnFileDescriptorEventListenerLegacy(@NonNull FileDescriptor fd) { synchronized (this) { updateOnFileDescriptorEventListenerLocked(fd, 0, null); @@ -616,7 +601,7 @@ public final class MessageQueue { /* This is only read/written from the Looper thread. For use with Concurrent MQ */ private int mNextPollTimeoutMillis; private boolean mMessageDirectlyQueued; - private Message nextMessage(boolean peek) { + private Message nextMessage(boolean peek, boolean returnEarliest) { int i = 0; while (true) { @@ -693,7 +678,7 @@ public final class MessageQueue { * If we have a barrier we should return the async node (if it exists and is ready) */ if (msgNode != null && msgNode.isBarrier()) { - if (asyncMsgNode != null && now >= asyncMsgNode.getWhen()) { + if (asyncMsgNode != null && (returnEarliest || now >= asyncMsgNode.getWhen())) { found = asyncMsgNode; } else { next = asyncMsgNode; @@ -707,7 +692,7 @@ public final class MessageQueue { earliest = pickEarliestNode(msgNode, asyncMsgNode); if (earliest != null) { - if (now >= earliest.getWhen()) { + if (returnEarliest || now >= earliest.getWhen()) { found = earliest; } else { next = earliest; @@ -796,7 +781,6 @@ public final class MessageQueue { } } - @NeverInline private Message nextConcurrent() { final long ptr = mPtr; if (ptr == 0) { @@ -813,7 +797,7 @@ public final class MessageQueue { mMessageDirectlyQueued = false; nativePollOnce(ptr, mNextPollTimeoutMillis); - Message msg = nextMessage(false); + Message msg = nextMessage(false, false); if (msg != null) { msg.markInUse(); return msg; @@ -871,7 +855,6 @@ public final class MessageQueue { } } - @NeverInline private Message nextLegacy() { // Return here if the message loop has already quit and been disposed. // This can happen if the application tries to restart a looper after quit @@ -1036,13 +1019,11 @@ public final class MessageQueue { } } - @NeverInline private int postSyncBarrierConcurrent() { return postSyncBarrier(SystemClock.uptimeMillis()); } - @NeverInline private int postSyncBarrierLegacy() { return postSyncBarrier(SystemClock.uptimeMillis()); } @@ -1162,7 +1143,6 @@ public final class MessageQueue { } } - @NeverInline private void removeSyncBarrierConcurrent(int token) { boolean removed; MessageNode first; @@ -1189,7 +1169,6 @@ public final class MessageQueue { } } - @NeverInline private void removeSyncBarrierLegacy(int token) { synchronized (this) { Message prev = null; @@ -1249,7 +1228,6 @@ public final class MessageQueue { } - @NeverInline private boolean enqueueMessageConcurrent(Message msg, long when) { if (msg.isInUse()) { throw new IllegalStateException(msg + " This message is already in use."); @@ -1258,7 +1236,6 @@ public final class MessageQueue { return enqueueMessageUnchecked(msg, when); } - @NeverInline private boolean enqueueMessageLegacy(Message msg, long when) { synchronized (this) { if (msg.isInUse()) { @@ -1397,27 +1374,27 @@ public final class MessageQueue { if (now >= msg.when) { // Got a message. mBlocked = false; - if (prevMsg != null) { - prevMsg.next = msg.next; - if (prevMsg.next == null) { - mLast = prevMsg; - } - } else { - mMessages = msg.next; - if (msg.next == null) { - mLast = null; - } - } - msg.next = null; - msg.markInUse(); - if (msg.isAsynchronous()) { - mAsyncMessageCount--; + } + if (prevMsg != null) { + prevMsg.next = msg.next; + if (prevMsg.next == null) { + mLast = prevMsg; } - if (TRACE) { - Trace.setCounter("MQ.Delivered", mMessagesDelivered.incrementAndGet()); + } else { + mMessages = msg.next; + if (msg.next == null) { + mLast = null; } - return msg; } + msg.next = null; + msg.markInUse(); + if (msg.isAsynchronous()) { + mAsyncMessageCount--; + } + if (TRACE) { + Trace.setCounter("MQ.Delivered", mMessagesDelivered.incrementAndGet()); + } + return msg; } } return null; @@ -1434,7 +1411,7 @@ public final class MessageQueue { throwIfNotTest(); Message ret; if (mUseConcurrent) { - ret = nextMessage(true); + ret = nextMessage(true, true); } else { ret = legacyPeekOrPoll(true); } @@ -1452,7 +1429,7 @@ public final class MessageQueue { Message pollForTest() { throwIfNotTest(); if (mUseConcurrent) { - return nextMessage(false); + return nextMessage(false, true); } else { return legacyPeekOrPoll(false); } @@ -1469,7 +1446,7 @@ public final class MessageQueue { throwIfNotTest(); if (mUseConcurrent) { // Call nextMessage to get the stack drained into our priority queues - nextMessage(true); + nextMessage(true, false); Iterator<MessageNode> queueIter = mPriorityQueue.iterator(); MessageNode queueNode = iterateNext(queueIter); @@ -1495,13 +1472,11 @@ public final class MessageQueue { private final MatchHandlerWhatAndObject mMatchHandlerWhatAndObject = new MatchHandlerWhatAndObject(); - @NeverInline private boolean hasMessagesConcurrent(Handler h, int what, Object object) { return findOrRemoveMessages(h, what, object, null, 0, mMatchHandlerWhatAndObject, false); } - @NeverInline private boolean hasMessagesLegacy(Handler h, int what, Object object) { synchronized (this) { Message p = mMessages; @@ -1540,13 +1515,11 @@ public final class MessageQueue { private final MatchHandlerWhatAndObjectEquals mMatchHandlerWhatAndObjectEquals = new MatchHandlerWhatAndObjectEquals(); - @NeverInline private boolean hasEqualMessagesConcurrent(Handler h, int what, Object object) { return findOrRemoveMessages(h, what, object, null, 0, mMatchHandlerWhatAndObjectEquals, false); } - @NeverInline private boolean hasEqualMessagesLegacy(Handler h, int what, Object object) { synchronized (this) { Message p = mMessages; @@ -1585,13 +1558,11 @@ public final class MessageQueue { private final MatchHandlerRunnableAndObject mMatchHandlerRunnableAndObject = new MatchHandlerRunnableAndObject(); - @NeverInline private boolean hasMessagesConcurrent(Handler h, Runnable r, Object object) { return findOrRemoveMessages(h, -1, object, r, 0, mMatchHandlerRunnableAndObject, false); } - @NeverInline private boolean hasMessagesLegacy(Handler h, Runnable r, Object object) { synchronized (this) { Message p = mMessages; @@ -1626,12 +1597,10 @@ public final class MessageQueue { } private final MatchHandler mMatchHandler = new MatchHandler(); - @NeverInline private boolean hasMessagesConcurrent(Handler h) { return findOrRemoveMessages(h, -1, null, null, 0, mMatchHandler, false); } - @NeverInline private boolean hasMessagesLegacy(Handler h) { synchronized (this) { Message p = mMessages; @@ -1656,12 +1625,10 @@ public final class MessageQueue { } } - @NeverInline private void removeMessagesConcurrent(Handler h, int what, Object object) { findOrRemoveMessages(h, what, object, null, 0, mMatchHandlerWhatAndObject, true); } - @NeverInline private void removeMessagesLegacy(Handler h, int what, Object object) { synchronized (this) { Message p = mMessages; @@ -1716,12 +1683,10 @@ public final class MessageQueue { } } - @NeverInline private void removeEqualMessagesConcurrent(Handler h, int what, Object object) { findOrRemoveMessages(h, what, object, null, 0, mMatchHandlerWhatAndObjectEquals, true); } - @NeverInline private void removeEqualMessagesLegacy(Handler h, int what, Object object) { synchronized (this) { Message p = mMessages; @@ -1777,12 +1742,10 @@ public final class MessageQueue { } } - @NeverInline private void removeMessagesConcurrent(Handler h, Runnable r, Object object) { findOrRemoveMessages(h, -1, object, r, 0, mMatchHandlerRunnableAndObject, true); } - @NeverInline private void removeMessagesLegacy(Handler h, Runnable r, Object object) { synchronized (this) { Message p = mMessages; @@ -1852,12 +1815,10 @@ public final class MessageQueue { private final MatchHandlerRunnableAndObjectEquals mMatchHandlerRunnableAndObjectEquals = new MatchHandlerRunnableAndObjectEquals(); - @NeverInline private void removeEqualMessagesConcurrent(Handler h, Runnable r, Object object) { findOrRemoveMessages(h, -1, object, r, 0, mMatchHandlerRunnableAndObjectEquals, true); } - @NeverInline private void removeEqualMessagesLegacy(Handler h, Runnable r, Object object) { synchronized (this) { Message p = mMessages; @@ -1926,12 +1887,10 @@ public final class MessageQueue { } private final MatchHandlerAndObject mMatchHandlerAndObject = new MatchHandlerAndObject(); - @NeverInline private void removeCallbacksAndMessagesConcurrent(Handler h, Object object) { findOrRemoveMessages(h, -1, object, null, 0, mMatchHandlerAndObject, true); } - @NeverInline private void removeCallbacksAndMessagesLegacy(Handler h, Object object) { synchronized (this) { Message p = mMessages; @@ -2000,12 +1959,10 @@ public final class MessageQueue { private final MatchHandlerAndObjectEquals mMatchHandlerAndObjectEquals = new MatchHandlerAndObjectEquals(); - @NeverInline void removeCallbacksAndEqualMessagesConcurrent(Handler h, Object object) { findOrRemoveMessages(h, -1, object, null, 0, mMatchHandlerAndObjectEquals, true); } - @NeverInline void removeCallbacksAndEqualMessagesLegacy(Handler h, Object object) { synchronized (this) { Message p = mMessages; diff --git a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java index 80da487a1358..7e0995c251b8 100644 --- a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java +++ b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java @@ -588,7 +588,7 @@ public final class MessageQueue { private static final AtomicLong mMessagesDelivered = new AtomicLong(); private boolean mMessageDirectlyQueued; - private Message nextMessage(boolean peek) { + private Message nextMessage(boolean peek, boolean returnEarliest) { int i = 0; while (true) { @@ -665,7 +665,7 @@ public final class MessageQueue { * If we have a barrier we should return the async node (if it exists and is ready) */ if (msgNode != null && msgNode.isBarrier()) { - if (asyncMsgNode != null && now >= asyncMsgNode.getWhen()) { + if (asyncMsgNode != null && (returnEarliest || now >= asyncMsgNode.getWhen())) { found = asyncMsgNode; } else { next = asyncMsgNode; @@ -679,7 +679,7 @@ public final class MessageQueue { earliest = pickEarliestNode(msgNode, asyncMsgNode); if (earliest != null) { - if (now >= earliest.getWhen()) { + if (returnEarliest || now >= earliest.getWhen()) { found = earliest; } else { next = earliest; @@ -784,7 +784,7 @@ public final class MessageQueue { mMessageDirectlyQueued = false; nativePollOnce(ptr, mNextPollTimeoutMillis); - Message msg = nextMessage(false); + Message msg = nextMessage(false, false); if (msg != null) { msg.markInUse(); return msg; @@ -1089,7 +1089,7 @@ public final class MessageQueue { */ Long peekWhenForTest() { throwIfNotTest(); - Message ret = nextMessage(true); + Message ret = nextMessage(true, true); return ret != null ? ret.when : null; } @@ -1102,7 +1102,7 @@ public final class MessageQueue { @Nullable Message pollForTest() { throwIfNotTest(); - return nextMessage(false); + return nextMessage(false, true); } /** @@ -1116,7 +1116,7 @@ public final class MessageQueue { throwIfNotTest(); // Call nextMessage to get the stack drained into our priority queues - nextMessage(true); + nextMessage(true, false); Iterator<MessageNode> queueIter = mPriorityQueue.iterator(); MessageNode queueNode = iterateNext(queueIter); diff --git a/core/java/android/os/LegacyMessageQueue/MessageQueue.java b/core/java/android/os/LegacyMessageQueue/MessageQueue.java index cde2ba56fcba..132bdd1e56b8 100644 --- a/core/java/android/os/LegacyMessageQueue/MessageQueue.java +++ b/core/java/android/os/LegacyMessageQueue/MessageQueue.java @@ -759,27 +759,27 @@ public final class MessageQueue { if (now >= msg.when) { // Got a message. mBlocked = false; - if (prevMsg != null) { - prevMsg.next = msg.next; - if (prevMsg.next == null) { - mLast = prevMsg; - } - } else { - mMessages = msg.next; - if (msg.next == null) { - mLast = null; - } - } - msg.next = null; - msg.markInUse(); - if (msg.isAsynchronous()) { - mAsyncMessageCount--; + } + if (prevMsg != null) { + prevMsg.next = msg.next; + if (prevMsg.next == null) { + mLast = prevMsg; } - if (TRACE) { - Trace.setCounter("MQ.Delivered", mMessagesDelivered.incrementAndGet()); + } else { + mMessages = msg.next; + if (msg.next == null) { + mLast = null; } - return msg; } + msg.next = null; + msg.markInUse(); + if (msg.isAsynchronous()) { + mAsyncMessageCount--; + } + if (TRACE) { + Trace.setCounter("MQ.Delivered", mMessagesDelivered.incrementAndGet()); + } + return msg; } } return null; diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index 3c4139d39762..e58934746c14 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -4661,7 +4661,7 @@ public final class Parcel { * @hide */ @Nullable - private Object readLazyValue(@Nullable ClassLoaderProvider loaderProvider) { + public Object readLazyValue(@Nullable ClassLoader loader) { int start = dataPosition(); int type = readInt(); if (isLengthPrefixed(type)) { @@ -4672,17 +4672,12 @@ public final class Parcel { int end = MathUtils.addOrThrow(dataPosition(), objectLength); int valueLength = end - start; setDataPosition(end); - return new LazyValue(this, start, valueLength, type, loaderProvider); + return new LazyValue(this, start, valueLength, type, loader); } else { - return readValue(type, getClassLoader(loaderProvider), /* clazz */ null); + return readValue(type, loader, /* clazz */ null); } } - @Nullable - private static ClassLoader getClassLoader(@Nullable ClassLoaderProvider loaderProvider) { - return loaderProvider == null ? null : loaderProvider.getClassLoader(); - } - private static final class LazyValue implements BiFunction<Class<?>, Class<?>[], Object> { /** @@ -4696,12 +4691,7 @@ public final class Parcel { private final int mPosition; private final int mLength; private final int mType; - // this member is set when a bundle that includes a LazyValue is unparceled. But it is used - // when apply method is called. Between these 2 events, the bundle's ClassLoader could have - // changed. Let the bundle be a ClassLoaderProvider allows the bundle provides its current - // ClassLoader at the time apply method is called. - @NonNull - private final ClassLoaderProvider mLoaderProvider; + @Nullable private final ClassLoader mLoader; @Nullable private Object mObject; /** @@ -4712,13 +4702,12 @@ public final class Parcel { */ @Nullable private volatile Parcel mSource; - LazyValue(Parcel source, int position, int length, int type, - @NonNull ClassLoaderProvider loaderProvider) { + LazyValue(Parcel source, int position, int length, int type, @Nullable ClassLoader loader) { mSource = requireNonNull(source); mPosition = position; mLength = length; mType = type; - mLoaderProvider = loaderProvider; + mLoader = loader; } @Override @@ -4731,8 +4720,7 @@ public final class Parcel { int restore = source.dataPosition(); try { source.setDataPosition(mPosition); - mObject = source.readValue(mLoaderProvider.getClassLoader(), clazz, - itemTypes); + mObject = source.readValue(mLoader, clazz, itemTypes); } finally { source.setDataPosition(restore); } @@ -4805,8 +4793,7 @@ public final class Parcel { return Objects.equals(mObject, value.mObject); } // Better safely fail here since this could mean we get different objects. - if (!Objects.equals(mLoaderProvider.getClassLoader(), - value.mLoaderProvider.getClassLoader())) { + if (!Objects.equals(mLoader, value.mLoader)) { return false; } // Otherwise compare metadata prior to comparing payload. @@ -4820,24 +4807,10 @@ public final class Parcel { @Override public int hashCode() { // Accessing mSource first to provide memory barrier for mObject - return Objects.hash(mSource == null, mObject, mLoaderProvider.getClassLoader(), mType, - mLength); + return Objects.hash(mSource == null, mObject, mLoader, mType, mLength); } } - /** - * Provides a ClassLoader. - * @hide - */ - public interface ClassLoaderProvider { - /** - * Returns a ClassLoader. - * - * @return ClassLoader - */ - ClassLoader getClassLoader(); - } - /** Same as {@link #readValue(ClassLoader, Class, Class[])} without any item types. */ private <T> T readValue(int type, @Nullable ClassLoader loader, @Nullable Class<T> clazz) { // Avoids allocating Class[0] array @@ -5578,8 +5551,8 @@ public final class Parcel { } private void readArrayMapInternal(@NonNull ArrayMap<? super String, Object> outVal, - int size, @Nullable ClassLoaderProvider loaderProvider) { - readArrayMap(outVal, size, /* sorted */ true, /* lazy */ false, loaderProvider, null); + int size, @Nullable ClassLoader loader) { + readArrayMap(outVal, size, /* sorted */ true, /* lazy */ false, loader, null); } /** @@ -5593,12 +5566,11 @@ public final class Parcel { * @hide */ void readArrayMap(ArrayMap<? super String, Object> map, int size, boolean sorted, - boolean lazy, @Nullable ClassLoaderProvider loaderProvider, int[] lazyValueCount) { + boolean lazy, @Nullable ClassLoader loader, int[] lazyValueCount) { ensureWithinMemoryLimit(SIZE_COMPLEX_TYPE, size); while (size > 0) { String key = readString(); - Object value = (lazy) ? readLazyValue(loaderProvider) : readValue( - getClassLoader(loaderProvider)); + Object value = (lazy) ? readLazyValue(loader) : readValue(loader); if (value instanceof LazyValue) { lazyValueCount[0]++; } @@ -5619,12 +5591,12 @@ public final class Parcel { */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public void readArrayMap(@NonNull ArrayMap<? super String, Object> outVal, - @Nullable ClassLoaderProvider loaderProvider) { + @Nullable ClassLoader loader) { final int N = readInt(); if (N < 0) { return; } - readArrayMapInternal(outVal, N, loaderProvider); + readArrayMapInternal(outVal, N, loader); } /** diff --git a/core/java/android/os/UidBatteryConsumer.java b/core/java/android/os/UidBatteryConsumer.java index 976bfe41ba45..62d5015af914 100644 --- a/core/java/android/os/UidBatteryConsumer.java +++ b/core/java/android/os/UidBatteryConsumer.java @@ -147,7 +147,7 @@ public final class UidBatteryConsumer extends BatteryConsumer { for (int screenState = 0; screenState < SCREEN_STATE_COUNT; screenState++) { if (mData.layout.screenStateDataIncluded - && screenState == POWER_STATE_UNSPECIFIED) { + && screenState == SCREEN_STATE_UNSPECIFIED) { continue; } diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 967f55ce7a88..6c21dbf126bb 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -2910,10 +2910,18 @@ public class UserManager { * <p>Currently, on most form factors the first human user on the device will be the main user; * in the future, the concept may be transferable, so a different user (or even no user at all) * may be designated the main user instead. On other form factors there might not be a main + * user. In the future, the concept may be removed, i.e. typical future devices may have no main * user. * * <p>Note that this will not be the system user on devices for which * {@link #isHeadlessSystemUserMode()} returns true. + * + * <p>NB: Features should ideally not limit functionality to the main user. Ideally, they + * should either work for all users or for all admin users. If a feature should only work for + * select users, its determination of which user should be done intelligently or be + * customizable. Not all devices support a main user, and the idea of singling out one user as + * special is contrary to overall multiuser goals. + * * @hide */ @SystemApi @@ -2930,6 +2938,12 @@ public class UserManager { /** * Returns the designated "main user" of the device, or {@code null} if there is no main user. * + * <p>NB: Features should ideally not limit functionality to the main user. Ideally, they + * should either work for all users or for all admin users. If a feature should only work for + * select users, its determination of which user should be done intelligently or be + * customizable. Not all devices support a main user, and the idea of singling out one user as + * special is contrary to overall multiuser goals. + * * @see #isMainUser() * @hide */ diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 2e231e3957c6..65c857a51b29 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -11008,6 +11008,21 @@ public final class Settings { @Readable public static final String SHOW_NOTIFICATION_SNOOZE = "show_notification_snooze"; + /** + * Controls whether dual shade is enabled. This splits notifications and quick settings to + * have their own independently expandable/collapsible panels, appearing on either side of + * the large screen (including unfolded device) or sharing a space on a narrow screen + * (including a folded device). Both panels will now cover the screen only partially + * (wrapping their content), so a running app or the lockscreen will remain visible in the + * background. + * <p> + * Type: int (0 for false, 1 for true) + * + * @hide + */ + @android.provider.Settings.Readable + public static final String DUAL_SHADE = "dual_shade"; + /** * 1 if it is allowed to remove the primary GAIA account. 0 by default. * @hide @@ -13788,6 +13803,16 @@ public final class Settings { = "enable_freeform_support"; /** + * Whether to override the availability of the desktop experiences features on the + * device. With desktop experiences enabled, secondary displays can be used to run + * apps, in desktop mode by default. Otherwise they can only be used for mirroring. + * @hide + */ + @Readable + public static final String DEVELOPMENT_OVERRIDE_DESKTOP_EXPERIENCE_FEATURES = + "override_desktop_experience_features"; + + /** * Whether to override the availability of the desktop mode on the main display of the * device. If on, users can make move an app to the desktop, allowing a freeform windowing * experience. diff --git a/core/java/android/service/notification/Adjustment.java b/core/java/android/service/notification/Adjustment.java index 073f512a85fb..09ebc9f030c2 100644 --- a/core/java/android/service/notification/Adjustment.java +++ b/core/java/android/service/notification/Adjustment.java @@ -21,6 +21,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.StringDef; import android.annotation.SystemApi; +import android.annotation.TestApi; import android.app.Notification; import android.os.Build; import android.os.Bundle; @@ -223,6 +224,14 @@ public final class Adjustment implements Parcelable { public static final int TYPE_CONTENT_RECOMMENDATION = 4; /** + * Data type: String, the classification type of this notification. The OS may display + * notifications differently depending on the type, and may change the alerting level of the + * notification. + */ + @FlaggedApi(android.app.Flags.FLAG_NM_SUMMARIZATION) + public static final String KEY_SUMMARIZATION = "key_summarization"; + + /** * Create a notification adjustment. * * @param pkg The package of the notification. diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java index 72569075c2ed..f23006584621 100644 --- a/core/java/android/service/notification/NotificationListenerService.java +++ b/core/java/android/service/notification/NotificationListenerService.java @@ -23,6 +23,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SdkConstant; import android.annotation.SystemApi; +import android.annotation.TestApi; import android.annotation.UiThread; import android.app.ActivityManager; import android.app.INotificationManager; @@ -56,6 +57,7 @@ import android.os.Parcelable; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; +import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; import android.widget.RemoteViews; @@ -1824,6 +1826,7 @@ public abstract class NotificationListenerService extends Service { private int mProposedImportance; // Sensitive info detected by the notification assistant private boolean mSensitiveContent; + private String mSummarization; private static final int PARCEL_VERSION = 2; @@ -1864,6 +1867,7 @@ public abstract class NotificationListenerService extends Service { out.writeBoolean(mIsBubble); out.writeInt(mProposedImportance); out.writeBoolean(mSensitiveContent); + out.writeString(mSummarization); } /** @hide */ @@ -1904,6 +1908,7 @@ public abstract class NotificationListenerService extends Service { mIsBubble = in.readBoolean(); mProposedImportance = in.readInt(); mSensitiveContent = in.readBoolean(); + mSummarization = in.readString(); } @@ -2180,6 +2185,16 @@ public abstract class NotificationListenerService extends Service { } /** + * Returns a summary of the content in the notification, or potentially of the current + * notification and related notifications (for example, if this is provided for a group + * summary notification it may be summarizing all the child notifications). + */ + @FlaggedApi(android.app.Flags.FLAG_NM_SUMMARIZATION) + public @Nullable String getSummarization() { + return mSummarization; + } + + /** * Returns the intended transition to ranking passed by {@link NotificationAssistantService} * @hide */ @@ -2201,7 +2216,7 @@ public abstract class NotificationListenerService extends Service { ArrayList<CharSequence> smartReplies, boolean canBubble, boolean isTextChanged, boolean isConversation, ShortcutInfo shortcutInfo, int rankingAdjustment, boolean isBubble, int proposedImportance, - boolean sensitiveContent) { + boolean sensitiveContent, String summarization) { mKey = key; mRank = rank; mIsAmbient = importance < NotificationManager.IMPORTANCE_LOW; @@ -2229,6 +2244,7 @@ public abstract class NotificationListenerService extends Service { mIsBubble = isBubble; mProposedImportance = proposedImportance; mSensitiveContent = sensitiveContent; + mSummarization = TextUtils.nullIfEmpty(summarization); } /** @@ -2271,7 +2287,8 @@ public abstract class NotificationListenerService extends Service { other.mRankingAdjustment, other.mIsBubble, other.mProposedImportance, - other.mSensitiveContent); + other.mSensitiveContent, + other.mSummarization); } /** @@ -2332,7 +2349,8 @@ public abstract class NotificationListenerService extends Service { && Objects.equals(mRankingAdjustment, other.mRankingAdjustment) && Objects.equals(mIsBubble, other.mIsBubble) && Objects.equals(mProposedImportance, other.mProposedImportance) - && Objects.equals(mSensitiveContent, other.mSensitiveContent); + && Objects.equals(mSensitiveContent, other.mSensitiveContent) + && Objects.equals(mSummarization, other.mSummarization); } } diff --git a/core/java/android/service/notification/StatusBarNotification.java b/core/java/android/service/notification/StatusBarNotification.java index 105fa3ffd4cd..79957f411597 100644 --- a/core/java/android/service/notification/StatusBarNotification.java +++ b/core/java/android/service/notification/StatusBarNotification.java @@ -18,6 +18,8 @@ package android.service.notification; import static android.text.TextUtils.formatSimple; +import static com.android.window.flags.Flags.enablePerDisplayPackageContextCacheInStatusbarNotif; + import android.annotation.NonNull; import android.app.Notification; import android.app.NotificationManager; @@ -37,9 +39,9 @@ import android.util.ArrayMap; import com.android.internal.logging.InstanceId; import com.android.internal.logging.nano.MetricsProto; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import static com.android.window.flags.Flags.enablePerDisplayPackageContextCacheInStatusbarNotif; import java.util.ArrayList; +import java.util.Collections; import java.util.Map; /** @@ -81,7 +83,8 @@ public class StatusBarNotification implements Parcelable { @Deprecated private Context mContext; // used for inflation & icon expansion // Maps display id to context used for remote view content inflation and status bar icon. - private final Map<Integer, Context> mContextForDisplayId = new ArrayMap<>(); + private final Map<Integer, Context> mContextForDisplayId = + Collections.synchronizedMap(new ArrayMap<>()); /** @hide */ public StatusBarNotification(String pkg, String opPkg, int id, diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index cb498503f201..a5d52957c40e 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -121,6 +121,7 @@ public class StaticLayout extends Layout { b.mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE; b.mJustificationMode = Layout.JUSTIFICATION_MODE_NONE; b.mLineBreakConfig = LineBreakConfig.NONE; + b.mUseBoundsForWidth = false; b.mMinimumFontMetrics = null; return b; } diff --git a/core/java/android/text/style/TtsSpan.java b/core/java/android/text/style/TtsSpan.java index a337ba2a57fb..e0d4ec1ca826 100644 --- a/core/java/android/text/style/TtsSpan.java +++ b/core/java/android/text/style/TtsSpan.java @@ -108,11 +108,13 @@ public class TtsSpan implements ParcelableSpan { /** * The text associated with this span is a time, consisting of a number of - * hours and minutes, specified with {@link #ARG_HOURS} and - * {@link #ARG_MINUTES}. + * hours, minutes, and seconds specified with {@link #ARG_HOURS}, {@link #ARG_MINUTES}, and + * {@link #ARG_SECONDS}. * Also accepts the arguments {@link #ARG_GENDER}, * {@link #ARG_ANIMACY}, {@link #ARG_MULTIPLICITY} and - * {@link #ARG_CASE}. + * {@link #ARG_CASE}. This is different from {@link #TYPE_DURATION}. This should be used to + * convey a particular moment in time, such as a clock time, while {@link #TYPE_DURATION} should + * be used to convey an interval of time. */ public static final String TYPE_TIME = "android.type.time"; @@ -310,16 +312,18 @@ public class TtsSpan implements ParcelableSpan { public static final String ARG_UNIT = "android.arg.unit"; /** - * Argument used to specify the hours of a time. The hours should be - * provided as an integer in the range from 0 up to and including 24. - * Can be used with {@link #TYPE_TIME}. + * Argument used to specify the hours of a time or duration. The hours should be + * provided as an integer in the range from 0 up to and including 24 for + * {@link #TYPE_TIME}. + * Can be used with {@link #TYPE_TIME} or {@link #TYPE_DURATION}. */ public static final String ARG_HOURS = "android.arg.hours"; /** - * Argument used to specify the minutes of a time. The minutes should be - * provided as an integer in the range from 0 up to and including 59. - * Can be used with {@link #TYPE_TIME}. + * Argument used to specify the minutes of a time or duration. The minutes should be + * provided as an integer in the range from 0 up to and including 59 for + * {@link #TYPE_TIME}. + * Can be used with {@link #TYPE_TIME} or {@link #TYPE_DURATION}. */ public static final String ARG_MINUTES = "android.arg.minutes"; diff --git a/core/java/android/view/InsetsSourceControl.java b/core/java/android/view/InsetsSourceControl.java index 7f2f0e8863df..cfb4835a13f7 100644 --- a/core/java/android/view/InsetsSourceControl.java +++ b/core/java/android/view/InsetsSourceControl.java @@ -194,7 +194,7 @@ public class InsetsSourceControl implements Parcelable { } public void release(Consumer<SurfaceControl> surfaceReleaseConsumer) { - if (mLeash != null) { + if (mLeash != null && mLeash.isValid()) { surfaceReleaseConsumer.accept(mLeash); } } diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 7c75d7b30037..0e329c2859db 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -44,7 +44,6 @@ import android.app.Activity; import android.app.ActivityOptions; import android.app.ActivityThread; import android.app.Application; -import android.app.LoadedApk; import android.app.PendingIntent; import android.app.RemoteInput; import android.appwidget.AppWidgetHostView; @@ -8484,8 +8483,14 @@ public class RemoteViews implements Parcelable, Filter { return context; } try { - LoadedApk.checkAndUpdateApkPaths(mApplication); - Context applicationContext = context.createApplicationContext(mApplication, + // Use PackageManager as the source of truth for application information, rather + // than the parceled ApplicationInfo provided by the app. + ApplicationInfo sanitizedApplication = + context.getPackageManager().getApplicationInfoAsUser( + mApplication.packageName, 0, + UserHandle.getUserId(mApplication.uid)); + Context applicationContext = context.createApplicationContext( + sanitizedApplication, Context.CONTEXT_RESTRICTED); // Get the correct apk paths while maintaining the current context's configuration. return applicationContext.createConfigurationContext( diff --git a/core/java/android/window/DesktopExperienceFlags.java b/core/java/android/window/DesktopExperienceFlags.java new file mode 100644 index 000000000000..0d1bb77ae8a2 --- /dev/null +++ b/core/java/android/window/DesktopExperienceFlags.java @@ -0,0 +1,124 @@ +/* + * 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 android.window; + +import static com.android.server.display.feature.flags.Flags.enableDisplayContentModeManagement; + +import android.annotation.Nullable; +import android.os.SystemProperties; +import android.util.Log; + +import com.android.window.flags.Flags; + +import java.util.function.BooleanSupplier; + +/** + * Checks Desktop Experience flag state. + * + * <p>This enum provides a centralized way to control the behavior of flags related to desktop + * experience features which are aiming for developer preview before their release. It allows + * developer option to override the default behavior of these flags. + * + * <p>The flags here will be controlled by the {@code + * persist.wm.debug.desktop_experience_devopts} system property. + * + * <p>NOTE: Flags should only be added to this enum when they have received Product and UX alignment + * that the feature is ready for developer preview, otherwise just do a flag check. + * + * @hide + */ +public enum DesktopExperienceFlags { + ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT(() -> enableDisplayContentModeManagement(), true); + + /** + * Flag class, to be used in case the enum cannot be used because the flag is not accessible. + * + * <p>This class will still use the process-wide cache. + */ + public static class DesktopExperienceFlag { + // Function called to obtain aconfig flag value. + private final BooleanSupplier mFlagFunction; + // Whether the flag state should be affected by developer option. + private final boolean mShouldOverrideByDevOption; + + public DesktopExperienceFlag(BooleanSupplier flagFunction, boolean shouldOverrideByDevOption) { + this.mFlagFunction = flagFunction; + this.mShouldOverrideByDevOption = shouldOverrideByDevOption; + } + + /** + * Determines state of flag based on the actual flag and desktop experience developer option + * overrides. + */ + public boolean isTrue() { + return isFlagTrue(mFlagFunction, mShouldOverrideByDevOption); + } + } + + private static final String TAG = "DesktopExperienceFlags"; + // Function called to obtain aconfig flag value. + private final BooleanSupplier mFlagFunction; + // Whether the flag state should be affected by developer option. + private final boolean mShouldOverrideByDevOption; + + // Local cache for toggle override, which is initialized once on its first access. It needs to + // be refreshed only on reboots as overridden state is expected to take effect on reboots. + @Nullable private static Boolean sCachedToggleOverride; + + public static final String SYSTEM_PROPERTY_NAME = "persist.wm.debug.desktop_experience_devopts"; + + DesktopExperienceFlags(BooleanSupplier flagFunction, boolean shouldOverrideByDevOption) { + this.mFlagFunction = flagFunction; + this.mShouldOverrideByDevOption = shouldOverrideByDevOption; + } + + /** + * Determines state of flag based on the actual flag and desktop experience developer option + * overrides. + */ + public boolean isTrue() { + return isFlagTrue(mFlagFunction, mShouldOverrideByDevOption); + } + + private static boolean isFlagTrue( + BooleanSupplier flagFunction, boolean shouldOverrideByDevOption) { + if (shouldOverrideByDevOption + && Flags.showDesktopExperienceDevOption() + && getToggleOverride()) { + return true; + } + return flagFunction.getAsBoolean(); + } + + private static boolean getToggleOverride() { + // If cached, return it + if (sCachedToggleOverride != null) { + return sCachedToggleOverride; + } + + // Otherwise, fetch and cache it + boolean override = getToggleOverrideFromSystem(); + sCachedToggleOverride = override; + Log.d(TAG, "Toggle override initialized to: " + override); + return override; + } + + /** Returns the {@link ToggleOverride} from the system property.. */ + private static boolean getToggleOverrideFromSystem() { + return SystemProperties.getBoolean(SYSTEM_PROPERTY_NAME, false); + } +} diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java index be69d3da3874..d1e3a2d953ef 100644 --- a/core/java/android/window/DesktopModeFlags.java +++ b/core/java/android/window/DesktopModeFlags.java @@ -20,12 +20,13 @@ import android.annotation.Nullable; import android.app.ActivityThread; import android.app.Application; import android.content.ContentResolver; +import android.os.SystemProperties; import android.provider.Settings; import android.util.Log; import com.android.window.flags.Flags; -import java.util.function.Supplier; +import java.util.function.BooleanSupplier; /** * Checks desktop mode flag state. @@ -80,19 +81,54 @@ public enum DesktopModeFlags { ENABLE_DESKTOP_WINDOWING_PERSISTENCE(Flags::enableDesktopWindowingPersistence, false), ENABLE_HANDLE_INPUT_FIX(Flags::enableHandleInputFix, true), ENABLE_DESKTOP_WINDOWING_ENTER_TRANSITIONS_BUGFIX( - Flags::enableDesktopWindowingEnterTransitionBugfix, false), + Flags::enableDesktopWindowingEnterTransitionBugfix, true), ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX( - Flags::enableDesktopWindowingExitTransitionsBugfix, false), + Flags::enableDesktopWindowingExitTransitionsBugfix, true), ENABLE_DESKTOP_APP_LAUNCH_ALTTAB_TRANSITIONS_BUGFIX( - Flags::enableDesktopAppLaunchAlttabTransitionsBugfix, false), + Flags::enableDesktopAppLaunchAlttabTransitionsBugfix, true), ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX( - Flags::enableDesktopAppLaunchTransitionsBugfix, false), + Flags::enableDesktopAppLaunchTransitionsBugfix, true), INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC( - Flags::includeTopTransparentFullscreenTaskInDesktopHeuristic, true); + Flags::includeTopTransparentFullscreenTaskInDesktopHeuristic, true), + ENABLE_DESKTOP_WINDOWING_HSUM(Flags::enableDesktopWindowingHsum, true), + ENABLE_MINIMIZE_BUTTON(Flags::enableMinimizeButton, true), + ENABLE_RESIZING_METRICS(Flags::enableResizingMetrics, true), + ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS(Flags::enableTaskResizingKeyboardShortcuts, true), + ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER( + Flags::enableDesktopWallpaperActivityForSystemUser, true), + ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX( + Flags::enableDesktopRecentsTransitionsCornersBugfix, false), + ENABLE_DESKTOP_SYSTEM_DIALOGS_TRANSITIONS(Flags::enableDesktopSystemDialogsTransitions, true); - private static final String TAG = "DesktopModeFlagsUtil"; + /** + * Flag class, to be used in case the enum cannot be used because the flag is not accessible. + * + * <p> This class will still use the process-wide cache. + */ + public static class DesktopModeFlag { + // Function called to obtain aconfig flag value. + private final BooleanSupplier mFlagFunction; + // Whether the flag state should be affected by developer option. + private final boolean mShouldOverrideByDevOption; + + public DesktopModeFlag(BooleanSupplier flagFunction, boolean shouldOverrideByDevOption) { + this.mFlagFunction = flagFunction; + this.mShouldOverrideByDevOption = shouldOverrideByDevOption; + } + + /** + * Determines state of flag based on the actual flag and desktop mode developer option + * overrides. + */ + public boolean isTrue() { + return isFlagTrue(mFlagFunction, mShouldOverrideByDevOption); + } + + } + + private static final String TAG = "DesktopModeFlags"; // Function called to obtain aconfig flag value. - private final Supplier<Boolean> mFlagFunction; + private final BooleanSupplier mFlagFunction; // Whether the flag state should be affected by developer option. private final boolean mShouldOverrideByDevOption; @@ -100,7 +136,9 @@ public enum DesktopModeFlags { // be refreshed only on reboots as overridden state is expected to take effect on reboots. private static ToggleOverride sCachedToggleOverride; - DesktopModeFlags(Supplier<Boolean> flagFunction, boolean shouldOverrideByDevOption) { + public static final String SYSTEM_PROPERTY_NAME = "persist.wm.debug.desktop_experience_devopts"; + + DesktopModeFlags(BooleanSupplier flagFunction, boolean shouldOverrideByDevOption) { this.mFlagFunction = flagFunction; this.mShouldOverrideByDevOption = shouldOverrideByDevOption; } @@ -110,24 +148,42 @@ public enum DesktopModeFlags { * overrides. */ public boolean isTrue() { - Application application = ActivityThread.currentApplication(); - if (!Flags.showDesktopWindowingDevOption() - || !mShouldOverrideByDevOption - || application == null) { - return mFlagFunction.get(); - } else { + return isFlagTrue(mFlagFunction, mShouldOverrideByDevOption); + } + + private static boolean isFlagTrue(BooleanSupplier flagFunction, + boolean shouldOverrideByDevOption) { + if (!shouldOverrideByDevOption) return flagFunction.getAsBoolean(); + if (Flags.showDesktopExperienceDevOption()) { + return switch (getToggleOverride(null)) { + case OVERRIDE_UNSET, OVERRIDE_OFF -> flagFunction.getAsBoolean(); + case OVERRIDE_ON -> true; + }; + } + if (Flags.showDesktopWindowingDevOption()) { + Application application = ActivityThread.currentApplication(); + if (application == null) { + Log.w(TAG, "Could not get the current application."); + return flagFunction.getAsBoolean(); + } + ContentResolver contentResolver = application.getContentResolver(); + if (contentResolver == null) { + Log.w(TAG, "Could not get the content resolver for the application."); + return flagFunction.getAsBoolean(); + } boolean shouldToggleBeEnabledByDefault = Flags.enableDesktopWindowingMode(); - return switch (getToggleOverride(application.getContentResolver())) { - case OVERRIDE_UNSET -> mFlagFunction.get(); + return switch (getToggleOverride(contentResolver)) { + case OVERRIDE_UNSET -> flagFunction.getAsBoolean(); // When toggle override matches its default state, don't override flags. This // helps users reset their feature overrides. - case OVERRIDE_OFF -> !shouldToggleBeEnabledByDefault && mFlagFunction.get(); - case OVERRIDE_ON -> shouldToggleBeEnabledByDefault ? mFlagFunction.get() : true; + case OVERRIDE_OFF -> !shouldToggleBeEnabledByDefault && flagFunction.getAsBoolean(); + case OVERRIDE_ON -> !shouldToggleBeEnabledByDefault || flagFunction.getAsBoolean(); }; } + return flagFunction.getAsBoolean(); } - private ToggleOverride getToggleOverride(ContentResolver contentResolver) { + private static ToggleOverride getToggleOverride(@Nullable ContentResolver contentResolver) { // If cached, return it if (sCachedToggleOverride != null) { return sCachedToggleOverride; @@ -143,12 +199,21 @@ public enum DesktopModeFlags { /** * Returns {@link ToggleOverride} from Settings.Global set by toggle. */ - private ToggleOverride getToggleOverrideFromSystem(ContentResolver contentResolver) { - int settingValue = Settings.Global.getInt( - contentResolver, - Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, - ToggleOverride.OVERRIDE_UNSET.getSetting() - ); + private static ToggleOverride getToggleOverrideFromSystem( + @Nullable ContentResolver contentResolver) { + int settingValue; + if (Flags.showDesktopExperienceDevOption()) { + settingValue = SystemProperties.getInt( + SYSTEM_PROPERTY_NAME, + ToggleOverride.OVERRIDE_UNSET.getSetting() + ); + } else { + settingValue = Settings.Global.getInt( + contentResolver, + Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, + ToggleOverride.OVERRIDE_UNSET.getSetting() + ); + } return ToggleOverride.fromSetting(settingValue, ToggleOverride.OVERRIDE_UNSET); } diff --git a/core/java/android/window/OWNERS b/core/java/android/window/OWNERS index 77c99b98cf4a..82d37244dc70 100644 --- a/core/java/android/window/OWNERS +++ b/core/java/android/window/OWNERS @@ -3,3 +3,4 @@ set noparent include /services/core/java/com/android/server/wm/OWNERS per-file DesktopModeFlags.java = file:/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS +per-file DesktopExperienceFlags.java = file:/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java index ce0ccd5c6d0d..68b5a261f507 100644 --- a/core/java/android/window/WindowContainerTransaction.java +++ b/core/java/android/window/WindowContainerTransaction.java @@ -112,6 +112,12 @@ public final class WindowContainerTransaction implements Parcelable { mTaskFragmentOrganizer = null; } + /* + * =========================================================================================== + * Window container properties + * =========================================================================================== + */ + /** * Resize a container. */ @@ -170,20 +176,6 @@ public final class WindowContainerTransaction implements Parcelable { } /** - * Notify {@link com.android.server.wm.PinnedTaskController} that the picture-in-picture task - * has finished the enter animation with the given bounds. - */ - @NonNull - public WindowContainerTransaction scheduleFinishEnterPip( - @NonNull WindowContainerToken container, @NonNull Rect bounds) { - final Change chg = getOrCreateChange(container.asBinder()); - chg.mPinnedBounds = new Rect(bounds); - chg.mChangeMask |= Change.CHANGE_PIP_CALLBACK; - - return this; - } - - /** * Send a SurfaceControl transaction to the server, which the server will apply in sync with * the next bounds change. As this uses deferred transaction and not BLAST it is only * able to sync with a single window, and the first visible window in this hierarchy of type @@ -204,36 +196,6 @@ public final class WindowContainerTransaction implements Parcelable { } /** - * Like {@link #setBoundsChangeTransaction} but instead queues up a setPosition/WindowCrop - * on a container's surface control. This is useful when a boundsChangeTransaction needs to be - * queued up on a Task that won't be organized until the end of this window-container - * transaction. - * - * This requires that, at the end of this transaction, `task` will be organized; otherwise - * the server will throw an IllegalArgumentException. - * - * WARNING: Use this carefully. Whatever is set here should match the expected bounds after - * the transaction completes since it will likely be replaced by it. This call is - * intended to pre-emptively set bounds on a surface in sync with a buffer when - * otherwise the new bounds and the new buffer would update on different frames. - * - * TODO(b/134365562): remove once TaskOrg drives full-screen or BLAST is enabled. - * - * @hide - */ - @NonNull - public WindowContainerTransaction setBoundsChangeTransaction( - @NonNull WindowContainerToken task, @NonNull Rect surfaceBounds) { - Change chg = getOrCreateChange(task.asBinder()); - if (chg.mBoundsChangeSurfaceBounds == null) { - chg.mBoundsChangeSurfaceBounds = new Rect(); - } - chg.mBoundsChangeSurfaceBounds.set(surfaceBounds); - chg.mChangeMask |= Change.CHANGE_BOUNDS_TRANSACTION_RECT; - return this; - } - - /** * Set the windowing mode of children of a given root task, without changing * the windowing mode of the Task itself. This can be used during transitions * for example to make the activity render it's fullscreen configuration @@ -381,22 +343,115 @@ public final class WindowContainerTransaction implements Parcelable { } /** - * Reparents a container into another one. The effect of a {@code null} parent can vary. For - * example, reparenting a stack to {@code null} will reparent it to its display. + * Sets whether a container is being drag-resized. + * When {@code true}, the client will reuse a single (larger) surface size to avoid + * continuous allocations on every size change. * - * @param onTop When {@code true}, the child goes to the top of parent; otherwise it goes to - * the bottom. + * @param container WindowContainerToken of the task that changed its drag resizing state + * @hide */ @NonNull - public WindowContainerTransaction reparent(@NonNull WindowContainerToken child, - @Nullable WindowContainerToken parent, boolean onTop) { - mHierarchyOps.add(HierarchyOp.createForReparent(child.asBinder(), - parent == null ? null : parent.asBinder(), - onTop)); + public WindowContainerTransaction setDragResizing(@NonNull WindowContainerToken container, + boolean dragResizing) { + final Change change = getOrCreateChange(container.asBinder()); + change.mChangeMask |= Change.CHANGE_DRAG_RESIZING; + change.mDragResizing = dragResizing; return this; } /** + * Sets/removes the always on top flag for this {@code windowContainer}. See + * {@link com.android.server.wm.ConfigurationContainer#setAlwaysOnTop(boolean)}. + * Please note that this method is only intended to be used for a + * {@link com.android.server.wm.Task} or {@link com.android.server.wm.DisplayArea}. + * + * <p> + * Setting always on top to {@code True} will also make the {@code windowContainer} to move + * to the top. + * </p> + * <p> + * Setting always on top to {@code False} will make this {@code windowContainer} to move + * below the other always on top sibling containers. + * </p> + * + * @param windowContainer the container which the flag need to be updated for. + * @param alwaysOnTop denotes whether or not always on top flag should be set. + * @hide + */ + @NonNull + public WindowContainerTransaction setAlwaysOnTop( + @NonNull WindowContainerToken windowContainer, boolean alwaysOnTop) { + final HierarchyOp hierarchyOp = + new HierarchyOp.Builder( + HierarchyOp.HIERARCHY_OP_TYPE_SET_ALWAYS_ON_TOP) + .setContainer(windowContainer.asBinder()) + .setAlwaysOnTop(alwaysOnTop) + .build(); + mHierarchyOps.add(hierarchyOp); + return this; + } + + /** + * Sets/removes the reparent leaf task flag for this {@code windowContainer}. + * When this is set, the server side will try to reparent the leaf task to task display area + * if there is an existing activity in history during the activity launch. This operation only + * support on the organized root task. + * @hide + */ + @NonNull + public WindowContainerTransaction setReparentLeafTaskIfRelaunch( + @NonNull WindowContainerToken windowContainer, boolean reparentLeafTaskIfRelaunch) { + final HierarchyOp hierarchyOp = + new HierarchyOp.Builder( + HierarchyOp.HIERARCHY_OP_TYPE_SET_REPARENT_LEAF_TASK_IF_RELAUNCH) + .setContainer(windowContainer.asBinder()) + .setReparentLeafTaskIfRelaunch(reparentLeafTaskIfRelaunch) + .build(); + mHierarchyOps.add(hierarchyOp); + return this; + } + + /** + * Defers client-facing configuration changes for activities in `container` until the end of + * the transition animation. The configuration will still be applied to the WMCore hierarchy + * at the normal time (beginning); so, special consideration must be made for this in the + * animation. + * + * @param container WindowContainerToken who's children should defer config notification. + * @hide + */ + @NonNull + public WindowContainerTransaction deferConfigToTransitionEnd( + @NonNull WindowContainerToken container) { + final Change change = getOrCreateChange(container.asBinder()); + change.mConfigAtTransitionEnd = true; + return this; + } + + /** + * Sets the task as trimmable or not. This can be used to prevent the task from being trimmed by + * recents. This attribute is set to true on task creation by default. + * + * @param isTrimmableFromRecents When {@code true}, task is set as trimmable from recents. + * @hide + */ + @NonNull + public WindowContainerTransaction setTaskTrimmableFromRecents( + @NonNull WindowContainerToken container, + boolean isTrimmableFromRecents) { + mHierarchyOps.add( + HierarchyOp.createForSetTaskTrimmableFromRecents(container.asBinder(), + isTrimmableFromRecents)); + return this; + } + + /* + * =========================================================================================== + * Hierarchy updates (create/destroy/reorder/reparent containers) + * =========================================================================================== + */ + + /** * Reorders a container within its parent. * * @param onTop When {@code true}, the child goes to the top of parent; otherwise it goes to @@ -425,6 +480,22 @@ public final class WindowContainerTransaction implements Parcelable { } /** + * Reparents a container into another one. The effect of a {@code null} parent can vary. For + * example, reparenting a stack to {@code null} will reparent it to its display. + * + * @param onTop When {@code true}, the child goes to the top of parent; otherwise it goes to + * the bottom. + */ + @NonNull + public WindowContainerTransaction reparent(@NonNull WindowContainerToken child, + @Nullable WindowContainerToken parent, boolean onTop) { + mHierarchyOps.add(HierarchyOp.createForReparent(child.asBinder(), + parent == null ? null : parent.asBinder(), + onTop)); + return this; + } + + /** * Reparent's all children tasks or the top task of {@param currentParent} in the specified * {@param windowingMode} and {@param activityType} to {@param newParent} in their current * z-order. @@ -478,6 +549,116 @@ public final class WindowContainerTransaction implements Parcelable { } /** + * Finds and removes a task and its children using its container token. The task is removed + * from recents. + * + * If the task is a root task, its leaves are removed but the root task is not. Use + * {@link #removeRootTask(WindowContainerToken)} to remove the root task. + * + * @param containerToken ContainerToken of Task to be removed + */ + @NonNull + public WindowContainerTransaction removeTask(@NonNull WindowContainerToken containerToken) { + mHierarchyOps.add(HierarchyOp.createForRemoveTask(containerToken.asBinder())); + return this; + } + + /** + * Finds and removes a root task created by an organizer and its leaves using its container + * token. + * + * @param containerToken ContainerToken of the root task to be removed + * @hide + */ + @NonNull + public WindowContainerTransaction removeRootTask(@NonNull WindowContainerToken containerToken) { + mHierarchyOps.add(HierarchyOp.createForRemoveRootTask(containerToken.asBinder())); + return this; + } + + /** + * If `container` was brought to front as a transient-launch (eg. recents), this will reorder + * the container back to where it was prior to the transient-launch. This way if a transient + * launch is "aborted", the z-ordering of containers in WM should be restored to before the + * launch. + * @hide + */ + @NonNull + public WindowContainerTransaction restoreTransientOrder( + @NonNull WindowContainerToken container) { + final HierarchyOp hierarchyOp = + new HierarchyOp.Builder(HierarchyOp.HIERARCHY_OP_TYPE_RESTORE_TRANSIENT_ORDER) + .setContainer(container.asBinder()) + .build(); + mHierarchyOps.add(hierarchyOp); + return this; + } + + /** + * Restore the back navigation target from visible to invisible for canceling gesture animation. + * @hide + */ + @NonNull + public WindowContainerTransaction restoreBackNavi() { + final HierarchyOp hierarchyOp = + new HierarchyOp.Builder(HierarchyOp.HIERARCHY_OP_TYPE_RESTORE_BACK_NAVIGATION) + .build(); + mHierarchyOps.add(hierarchyOp); + return this; + } + + /* + * =========================================================================================== + * Activity launch + * =========================================================================================== + */ + + /** + * Starts a task by id. The task is expected to already exist (eg. as a recent task). + * @param taskId Id of task to start. + * @param options bundle containing ActivityOptions for the task's top activity. + * @hide + */ + @NonNull + public WindowContainerTransaction startTask(int taskId, @Nullable Bundle options) { + mHierarchyOps.add(HierarchyOp.createForTaskLaunch(taskId, options)); + return this; + } + + /** + * Sends a pending intent in sync. + * @param sender The PendingIntent sender. + * @param intent The fillIn intent to patch over the sender's base intent. + * @param options bundle containing ActivityOptions for the task's top activity. + * @hide + */ + @NonNull + public WindowContainerTransaction sendPendingIntent(@Nullable PendingIntent sender, + @Nullable Intent intent, @Nullable Bundle options) { + mHierarchyOps.add(new HierarchyOp.Builder(HierarchyOp.HIERARCHY_OP_TYPE_PENDING_INTENT) + .setLaunchOptions(options) + .setPendingIntent(sender) + .setActivityIntent(intent) + .build()); + return this; + } + + /** + * Starts activity(s) from a shortcut. + * @param callingPackage The package launching the shortcut. + * @param shortcutInfo Information about the shortcut to start + * @param options bundle containing ActivityOptions for the task's top activity. + * @hide + */ + @NonNull + public WindowContainerTransaction startShortcut(@NonNull String callingPackage, + @NonNull ShortcutInfo shortcutInfo, @Nullable Bundle options) { + mHierarchyOps.add(HierarchyOp.createForStartShortcut( + callingPackage, shortcutInfo, options)); + return this; + } + + /** * Sets whether a container should be the launch root for the specified windowing mode and * activity type. This currently only applies to Task containers created by organizer. */ @@ -491,6 +672,12 @@ public final class WindowContainerTransaction implements Parcelable { return this; } + /* + * =========================================================================================== + * Multitasking + * =========================================================================================== + */ + /** * Sets two containers adjacent to each other. Containers below two visible adjacent roots will * be made invisible. This currently only applies to TaskFragment containers created by @@ -599,93 +786,162 @@ public final class WindowContainerTransaction implements Parcelable { return this; } + /* + * =========================================================================================== + * PIP + * =========================================================================================== + */ + /** - * Starts a task by id. The task is expected to already exist (eg. as a recent task). - * @param taskId Id of task to start. - * @param options bundle containing ActivityOptions for the task's top activity. + * Moves the PiP activity of a parent task to a pinned root task. + * @param parentToken the parent task of the PiP activity + * @param bounds the entry bounds * @hide */ @NonNull - public WindowContainerTransaction startTask(int taskId, @Nullable Bundle options) { - mHierarchyOps.add(HierarchyOp.createForTaskLaunch(taskId, options)); + public WindowContainerTransaction movePipActivityToPinnedRootTask( + @NonNull WindowContainerToken parentToken, @NonNull Rect bounds) { + mHierarchyOps.add(new HierarchyOp + .Builder(HierarchyOp.HIERARCHY_OP_TYPE_MOVE_PIP_ACTIVITY_TO_PINNED_TASK) + .setContainer(parentToken.asBinder()) + .setBounds(bounds) + .build()); return this; } /** - * Finds and removes a task and its children using its container token. The task is removed - * from recents. - * - * If the task is a root task, its leaves are removed but the root task is not. Use - * {@link #removeRootTask(WindowContainerToken)} to remove the root task. - * - * @param containerToken ContainerToken of Task to be removed + * Notify {@link com.android.server.wm.PinnedTaskController} that the picture-in-picture task + * has finished the enter animation with the given bounds. */ @NonNull - public WindowContainerTransaction removeTask(@NonNull WindowContainerToken containerToken) { - mHierarchyOps.add(HierarchyOp.createForRemoveTask(containerToken.asBinder())); + public WindowContainerTransaction scheduleFinishEnterPip( + @NonNull WindowContainerToken container, @NonNull Rect bounds) { + final Change chg = getOrCreateChange(container.asBinder()); + chg.mPinnedBounds = new Rect(bounds); + chg.mChangeMask |= Change.CHANGE_PIP_CALLBACK; + return this; } + /* + * =========================================================================================== + * Insets + * =========================================================================================== + */ + /** - * Finds and removes a root task created by an organizer and its leaves using its container - * token. + * Adds a given {@code Rect} as an insets source frame on the {@code receiver}. * - * @param containerToken ContainerToken of the root task to be removed + * @param receiver The window container that the insets source is added to. + * @param owner The owner of the insets source. An insets source can only be modified by its + * owner. + * @param index An owner might add multiple insets sources with the same type. + * This identifies them. + * @param type The {@link InsetsType} of the insets source. + * @param frame The rectangle area of the insets source. + * @param boundingRects The bounding rects within this inset, relative to the |frame|. * @hide */ @NonNull - public WindowContainerTransaction removeRootTask(@NonNull WindowContainerToken containerToken) { - mHierarchyOps.add(HierarchyOp.createForRemoveRootTask(containerToken.asBinder())); + public WindowContainerTransaction addInsetsSource( + @NonNull WindowContainerToken receiver, + @Nullable IBinder owner, int index, @InsetsType int type, @Nullable Rect frame, + @Nullable Rect[] boundingRects, @InsetsSource.Flags int flags) { + final HierarchyOp hierarchyOp = + new HierarchyOp.Builder(HierarchyOp.HIERARCHY_OP_TYPE_ADD_INSETS_FRAME_PROVIDER) + .setContainer(receiver.asBinder()) + .setInsetsFrameProvider(new InsetsFrameProvider(owner, index, type) + .setSource(InsetsFrameProvider.SOURCE_ARBITRARY_RECTANGLE) + .setArbitraryRectangle(frame) + .setBoundingRects(boundingRects) + .setFlags(flags)) + .setInsetsFrameOwner(owner) + .build(); + mHierarchyOps.add(hierarchyOp); return this; } /** - * Sets whether a container is being drag-resized. - * When {@code true}, the client will reuse a single (larger) surface size to avoid - * continuous allocations on every size change. + * Removes the insets source from the {@code receiver}. * - * @param container WindowContainerToken of the task that changed its drag resizing state + * @param receiver The window container that the insets source was added to. + * @param owner The owner of the insets source. An insets source can only be modified by its + * owner. + * @param index An owner might add multiple insets sources with the same type. + * This identifies them. + * @param type The {@link InsetsType} of the insets source. * @hide */ @NonNull - public WindowContainerTransaction setDragResizing(@NonNull WindowContainerToken container, - boolean dragResizing) { - final Change change = getOrCreateChange(container.asBinder()); - change.mChangeMask |= Change.CHANGE_DRAG_RESIZING; - change.mDragResizing = dragResizing; + public WindowContainerTransaction removeInsetsSource(@NonNull WindowContainerToken receiver, + @Nullable IBinder owner, int index, @InsetsType int type) { + final HierarchyOp hierarchyOp = + new HierarchyOp.Builder(HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_INSETS_FRAME_PROVIDER) + .setContainer(receiver.asBinder()) + .setInsetsFrameProvider(new InsetsFrameProvider(owner, index, type)) + .setInsetsFrameOwner(owner) + .build(); + mHierarchyOps.add(hierarchyOp); return this; } + /* + * =========================================================================================== + * Keyguard + * =========================================================================================== + */ + /** - * Sends a pending intent in sync. - * @param sender The PendingIntent sender. - * @param intent The fillIn intent to patch over the sender's base intent. - * @param options bundle containing ActivityOptions for the task's top activity. + * Adds a {@link KeyguardState} to apply to the given displays. + * * @hide */ @NonNull - public WindowContainerTransaction sendPendingIntent(@Nullable PendingIntent sender, - @Nullable Intent intent, @Nullable Bundle options) { - mHierarchyOps.add(new HierarchyOp.Builder(HierarchyOp.HIERARCHY_OP_TYPE_PENDING_INTENT) - .setLaunchOptions(options) - .setPendingIntent(sender) - .setActivityIntent(intent) - .build()); + public WindowContainerTransaction addKeyguardState(@NonNull KeyguardState keyguardState) { + Objects.requireNonNull(keyguardState); + final HierarchyOp hierarchyOp = + new HierarchyOp.Builder( + HierarchyOp.HIERARCHY_OP_TYPE_SET_KEYGUARD_STATE) + .setKeyguardState(keyguardState) + .build(); + mHierarchyOps.add(hierarchyOp); return this; } + /* + * =========================================================================================== + * Task fragments + * =========================================================================================== + */ + /** - * Starts activity(s) from a shortcut. - * @param callingPackage The package launching the shortcut. - * @param shortcutInfo Information about the shortcut to start - * @param options bundle containing ActivityOptions for the task's top activity. + * Sets the {@link TaskFragmentOrganizer} that applies this {@link WindowContainerTransaction}. + * When this is set, the server side will not check for the permission of + * {@link android.Manifest.permission#MANAGE_ACTIVITY_TASKS}, but will ensure this WCT only + * contains operations that are allowed for this organizer, such as modifying TaskFragments that + * are organized by this organizer. * @hide */ @NonNull - public WindowContainerTransaction startShortcut(@NonNull String callingPackage, - @NonNull ShortcutInfo shortcutInfo, @Nullable Bundle options) { - mHierarchyOps.add(HierarchyOp.createForStartShortcut( - callingPackage, shortcutInfo, options)); + public WindowContainerTransaction setTaskFragmentOrganizer( + @NonNull ITaskFragmentOrganizer organizer) { + mTaskFragmentOrganizer = organizer; + return this; + } + + /** + * When this {@link WindowContainerTransaction} failed to finish on the server side, it will + * trigger callback with this {@param errorCallbackToken}. + * @param errorCallbackToken client provided token that will be passed back as parameter in + * the callback if there is an error on the server side. + * @see ITaskFragmentOrganizer#onTaskFragmentError + */ + @NonNull + public WindowContainerTransaction setErrorCallbackToken(@NonNull IBinder errorCallbackToken) { + if (mErrorCallbackToken != null) { + throw new IllegalStateException("Can't set multiple error token for one transaction."); + } + mErrorCallbackToken = errorCallbackToken; return this; } @@ -793,93 +1049,6 @@ public final class WindowContainerTransaction implements Parcelable { } /** - * If `container` was brought to front as a transient-launch (eg. recents), this will reorder - * the container back to where it was prior to the transient-launch. This way if a transient - * launch is "aborted", the z-ordering of containers in WM should be restored to before the - * launch. - * @hide - */ - @NonNull - public WindowContainerTransaction restoreTransientOrder( - @NonNull WindowContainerToken container) { - final HierarchyOp hierarchyOp = - new HierarchyOp.Builder(HierarchyOp.HIERARCHY_OP_TYPE_RESTORE_TRANSIENT_ORDER) - .setContainer(container.asBinder()) - .build(); - mHierarchyOps.add(hierarchyOp); - return this; - } - - /** - * Restore the back navigation target from visible to invisible for canceling gesture animation. - * @hide - */ - @NonNull - public WindowContainerTransaction restoreBackNavi() { - final HierarchyOp hierarchyOp = - new HierarchyOp.Builder(HierarchyOp.HIERARCHY_OP_TYPE_RESTORE_BACK_NAVIGATION) - .build(); - mHierarchyOps.add(hierarchyOp); - return this; - } - - /** - * Adds a given {@code Rect} as an insets source frame on the {@code receiver}. - * - * @param receiver The window container that the insets source is added to. - * @param owner The owner of the insets source. An insets source can only be modified by its - * owner. - * @param index An owner might add multiple insets sources with the same type. - * This identifies them. - * @param type The {@link InsetsType} of the insets source. - * @param frame The rectangle area of the insets source. - * @param boundingRects The bounding rects within this inset, relative to the |frame|. - * @hide - */ - @NonNull - public WindowContainerTransaction addInsetsSource( - @NonNull WindowContainerToken receiver, - @Nullable IBinder owner, int index, @InsetsType int type, @Nullable Rect frame, - @Nullable Rect[] boundingRects, @InsetsSource.Flags int flags) { - final HierarchyOp hierarchyOp = - new HierarchyOp.Builder(HierarchyOp.HIERARCHY_OP_TYPE_ADD_INSETS_FRAME_PROVIDER) - .setContainer(receiver.asBinder()) - .setInsetsFrameProvider(new InsetsFrameProvider(owner, index, type) - .setSource(InsetsFrameProvider.SOURCE_ARBITRARY_RECTANGLE) - .setArbitraryRectangle(frame) - .setBoundingRects(boundingRects) - .setFlags(flags)) - .setInsetsFrameOwner(owner) - .build(); - mHierarchyOps.add(hierarchyOp); - return this; - } - - /** - * Removes the insets source from the {@code receiver}. - * - * @param receiver The window container that the insets source was added to. - * @param owner The owner of the insets source. An insets source can only be modified by its - * owner. - * @param index An owner might add multiple insets sources with the same type. - * This identifies them. - * @param type The {@link InsetsType} of the insets source. - * @hide - */ - @NonNull - public WindowContainerTransaction removeInsetsSource(@NonNull WindowContainerToken receiver, - @Nullable IBinder owner, int index, @InsetsType int type) { - final HierarchyOp hierarchyOp = - new HierarchyOp.Builder(HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_INSETS_FRAME_PROVIDER) - .setContainer(receiver.asBinder()) - .setInsetsFrameProvider(new InsetsFrameProvider(owner, index, type)) - .setInsetsFrameOwner(owner) - .build(); - mHierarchyOps.add(hierarchyOp); - return this; - } - - /** * Requests focus on the top running Activity in the given TaskFragment. This will only take * effect if there is no focus, or if the current focus is in the same Task as the requested * TaskFragment. @@ -961,157 +1130,6 @@ public final class WindowContainerTransaction implements Parcelable { } /** - * Adds a {@link KeyguardState} to apply to the given displays. - * - * @hide - */ - @NonNull - public WindowContainerTransaction addKeyguardState(@NonNull KeyguardState keyguardState) { - Objects.requireNonNull(keyguardState); - final HierarchyOp hierarchyOp = - new HierarchyOp.Builder( - HierarchyOp.HIERARCHY_OP_TYPE_SET_KEYGUARD_STATE) - .setKeyguardState(keyguardState) - .build(); - mHierarchyOps.add(hierarchyOp); - return this; - } - - /** - * Sets/removes the always on top flag for this {@code windowContainer}. See - * {@link com.android.server.wm.ConfigurationContainer#setAlwaysOnTop(boolean)}. - * Please note that this method is only intended to be used for a - * {@link com.android.server.wm.Task} or {@link com.android.server.wm.DisplayArea}. - * - * <p> - * Setting always on top to {@code True} will also make the {@code windowContainer} to move - * to the top. - * </p> - * <p> - * Setting always on top to {@code False} will make this {@code windowContainer} to move - * below the other always on top sibling containers. - * </p> - * - * @param windowContainer the container which the flag need to be updated for. - * @param alwaysOnTop denotes whether or not always on top flag should be set. - * @hide - */ - @NonNull - public WindowContainerTransaction setAlwaysOnTop( - @NonNull WindowContainerToken windowContainer, boolean alwaysOnTop) { - final HierarchyOp hierarchyOp = - new HierarchyOp.Builder( - HierarchyOp.HIERARCHY_OP_TYPE_SET_ALWAYS_ON_TOP) - .setContainer(windowContainer.asBinder()) - .setAlwaysOnTop(alwaysOnTop) - .build(); - mHierarchyOps.add(hierarchyOp); - return this; - } - - /** - * When this {@link WindowContainerTransaction} failed to finish on the server side, it will - * trigger callback with this {@param errorCallbackToken}. - * @param errorCallbackToken client provided token that will be passed back as parameter in - * the callback if there is an error on the server side. - * @see ITaskFragmentOrganizer#onTaskFragmentError - */ - @NonNull - public WindowContainerTransaction setErrorCallbackToken(@NonNull IBinder errorCallbackToken) { - if (mErrorCallbackToken != null) { - throw new IllegalStateException("Can't set multiple error token for one transaction."); - } - mErrorCallbackToken = errorCallbackToken; - return this; - } - - /** - * Sets the {@link TaskFragmentOrganizer} that applies this {@link WindowContainerTransaction}. - * When this is set, the server side will not check for the permission of - * {@link android.Manifest.permission#MANAGE_ACTIVITY_TASKS}, but will ensure this WCT only - * contains operations that are allowed for this organizer, such as modifying TaskFragments that - * are organized by this organizer. - * @hide - */ - @NonNull - public WindowContainerTransaction setTaskFragmentOrganizer( - @NonNull ITaskFragmentOrganizer organizer) { - mTaskFragmentOrganizer = organizer; - return this; - } - - /** - * Sets/removes the reparent leaf task flag for this {@code windowContainer}. - * When this is set, the server side will try to reparent the leaf task to task display area - * if there is an existing activity in history during the activity launch. This operation only - * support on the organized root task. - * @hide - */ - @NonNull - public WindowContainerTransaction setReparentLeafTaskIfRelaunch( - @NonNull WindowContainerToken windowContainer, boolean reparentLeafTaskIfRelaunch) { - final HierarchyOp hierarchyOp = - new HierarchyOp.Builder( - HierarchyOp.HIERARCHY_OP_TYPE_SET_REPARENT_LEAF_TASK_IF_RELAUNCH) - .setContainer(windowContainer.asBinder()) - .setReparentLeafTaskIfRelaunch(reparentLeafTaskIfRelaunch) - .build(); - mHierarchyOps.add(hierarchyOp); - return this; - } - - /** - * Moves the PiP activity of a parent task to a pinned root task. - * @param parentToken the parent task of the PiP activity - * @param bounds the entry bounds - * @hide - */ - @NonNull - public WindowContainerTransaction movePipActivityToPinnedRootTask( - @NonNull WindowContainerToken parentToken, @NonNull Rect bounds) { - mHierarchyOps.add(new HierarchyOp - .Builder(HierarchyOp.HIERARCHY_OP_TYPE_MOVE_PIP_ACTIVITY_TO_PINNED_TASK) - .setContainer(parentToken.asBinder()) - .setBounds(bounds) - .build()); - return this; - } - - /** - * Defers client-facing configuration changes for activities in `container` until the end of - * the transition animation. The configuration will still be applied to the WMCore hierarchy - * at the normal time (beginning); so, special consideration must be made for this in the - * animation. - * - * @param container WindowContainerToken who's children should defer config notification. - * @hide - */ - @NonNull - public WindowContainerTransaction deferConfigToTransitionEnd( - @NonNull WindowContainerToken container) { - final Change change = getOrCreateChange(container.asBinder()); - change.mConfigAtTransitionEnd = true; - return this; - } - - /** - * Sets the task as trimmable or not. This can be used to prevent the task from being trimmed by - * recents. This attribute is set to true on task creation by default. - * - * @param isTrimmableFromRecents When {@code true}, task is set as trimmable from recents. - * @hide - */ - @NonNull - public WindowContainerTransaction setTaskTrimmableFromRecents( - @NonNull WindowContainerToken container, - boolean isTrimmableFromRecents) { - mHierarchyOps.add( - HierarchyOp.createForSetTaskTrimmableFromRecents(container.asBinder(), - isTrimmableFromRecents)); - return this; - } - - /** * Merges another WCT into this one. * @param transfer When true, this will transfer everything from other potentially leaving * other in an unusable state. When false, other is left alone, but @@ -1206,7 +1224,7 @@ public final class WindowContainerTransaction implements Parcelable { @NonNull public static final Creator<WindowContainerTransaction> CREATOR = - new Creator<WindowContainerTransaction>() { + new Creator<>() { @Override public WindowContainerTransaction createFromParcel(@NonNull Parcel in) { return new WindowContainerTransaction(in); @@ -1227,19 +1245,17 @@ public final class WindowContainerTransaction implements Parcelable { public static final int CHANGE_BOUNDS_TRANSACTION = 1 << 1; public static final int CHANGE_PIP_CALLBACK = 1 << 2; public static final int CHANGE_HIDDEN = 1 << 3; - public static final int CHANGE_BOUNDS_TRANSACTION_RECT = 1 << 4; - public static final int CHANGE_IGNORE_ORIENTATION_REQUEST = 1 << 5; - public static final int CHANGE_FORCE_NO_PIP = 1 << 6; - public static final int CHANGE_FORCE_TRANSLUCENT = 1 << 7; - public static final int CHANGE_DRAG_RESIZING = 1 << 8; - public static final int CHANGE_RELATIVE_BOUNDS = 1 << 9; + public static final int CHANGE_IGNORE_ORIENTATION_REQUEST = 1 << 4; + public static final int CHANGE_FORCE_NO_PIP = 1 << 5; + public static final int CHANGE_FORCE_TRANSLUCENT = 1 << 6; + public static final int CHANGE_DRAG_RESIZING = 1 << 7; + public static final int CHANGE_RELATIVE_BOUNDS = 1 << 8; @IntDef(flag = true, prefix = { "CHANGE_" }, value = { CHANGE_FOCUSABLE, CHANGE_BOUNDS_TRANSACTION, CHANGE_PIP_CALLBACK, CHANGE_HIDDEN, - CHANGE_BOUNDS_TRANSACTION_RECT, CHANGE_IGNORE_ORIENTATION_REQUEST, CHANGE_FORCE_NO_PIP, CHANGE_FORCE_TRANSLUCENT, @@ -1262,7 +1278,6 @@ public final class WindowContainerTransaction implements Parcelable { private Rect mPinnedBounds = null; private SurfaceControl.Transaction mBoundsChangeTransaction = null; - private Rect mBoundsChangeSurfaceBounds = null; @Nullable private Rect mRelativeBounds = null; private boolean mConfigAtTransitionEnd = false; @@ -1290,10 +1305,6 @@ public final class WindowContainerTransaction implements Parcelable { mBoundsChangeTransaction = SurfaceControl.Transaction.CREATOR.createFromParcel(in); } - if ((mChangeMask & Change.CHANGE_BOUNDS_TRANSACTION_RECT) != 0) { - mBoundsChangeSurfaceBounds = new Rect(); - mBoundsChangeSurfaceBounds.readFromParcel(in); - } if ((mChangeMask & Change.CHANGE_RELATIVE_BOUNDS) != 0) { mRelativeBounds = new Rect(); mRelativeBounds.readFromParcel(in); @@ -1342,10 +1353,6 @@ public final class WindowContainerTransaction implements Parcelable { if (other.mWindowingMode >= WINDOWING_MODE_UNDEFINED) { mWindowingMode = other.mWindowingMode; } - if (other.mBoundsChangeSurfaceBounds != null) { - mBoundsChangeSurfaceBounds = transfer ? other.mBoundsChangeSurfaceBounds - : new Rect(other.mBoundsChangeSurfaceBounds); - } if (other.mRelativeBounds != null) { mRelativeBounds = transfer ? other.mRelativeBounds @@ -1446,11 +1453,6 @@ public final class WindowContainerTransaction implements Parcelable { } @Nullable - public Rect getBoundsChangeSurfaceBounds() { - return mBoundsChangeSurfaceBounds; - } - - @Nullable public Rect getRelativeBounds() { return mRelativeBounds; } @@ -1529,9 +1531,6 @@ public final class WindowContainerTransaction implements Parcelable { if (mBoundsChangeTransaction != null) { mBoundsChangeTransaction.writeToParcel(dest, flags); } - if (mBoundsChangeSurfaceBounds != null) { - mBoundsChangeSurfaceBounds.writeToParcel(dest, flags); - } if (mRelativeBounds != null) { mRelativeBounds.writeToParcel(dest, flags); } diff --git a/core/java/android/window/flags/device_state_auto_rotate_setting.aconfig b/core/java/android/window/flags/device_state_auto_rotate_setting.aconfig new file mode 100644 index 000000000000..bb66989b9946 --- /dev/null +++ b/core/java/android/window/flags/device_state_auto_rotate_setting.aconfig @@ -0,0 +1,22 @@ +package: "com.android.window.flags" +container: "system" + +flag { + name: "enable_device_state_auto_rotate_setting_logging" + namespace: "windowing_frontend" + description: "Enable device state auto rotate setting logging" + bug: "391147112" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "enable_device_state_auto_rotate_setting_refactor" + namespace: "windowing_frontend" + description: "Enable refactored device state auto rotate setting logic" + bug: "350946537" + metadata { + purpose: PURPOSE_BUGFIX + } +}
\ No newline at end of file diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index b4e7675402b9..222088e8a8b9 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -555,4 +555,41 @@ flag { metadata { purpose: PURPOSE_BUGFIX } -}
\ No newline at end of file +} + +flag { + name: "show_desktop_experience_dev_option" + namespace: "lse_desktop_experience" + description: "Replace the freeform windowing dev options with a desktop experience one." + bug: "389092752" +} + +flag { + name: "enable_quickswitch_desktop_split_bugfix" + namespace: "lse_desktop_experience" + description: "Enables splitting QuickSwitch between fullscreen apps and Desktop workspaces." + bug: "345296916" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "enable_desktop_windowing_exit_by_minimize_transition_bugfix" + namespace: "lse_desktop_experience" + description: "Enables exit desktop windowing by minimize transition & motion polish changes" + bug: "390161102" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "enable_start_launch_transition_from_taskbar_bugfix" + namespace: "lse_desktop_experience" + description: "Enables starting a launch transition directly from the taskbar if desktop tasks are visible." + bug: "361366053" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/window/flags/responsible_apis.aconfig b/core/java/android/window/flags/responsible_apis.aconfig index 4b5adfcc2c9b..36219812c002 100644 --- a/core/java/android/window/flags/responsible_apis.aconfig +++ b/core/java/android/window/flags/responsible_apis.aconfig @@ -81,3 +81,10 @@ flag { description: "Strict mode violation triggered by grace period usage" bug: "384807495" } + +flag { + name: "bal_clear_allowlist_duration" + namespace: "responsible_apis" + description: "Clear the allowlist duration when clearAllowBgActivityStarts is called" + bug: "322159724" +} diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java index dc440e36ca0d..f49c5f1c2b0f 100644 --- a/core/java/com/android/internal/os/BatteryStatsHistory.java +++ b/core/java/com/android/internal/os/BatteryStatsHistory.java @@ -84,7 +84,7 @@ public class BatteryStatsHistory { private static final String TAG = "BatteryStatsHistory"; // Current on-disk Parcel version. Must be updated when the format of the parcelable changes - private static final int VERSION = 211; + private static final int VERSION = 212; private static final String HISTORY_DIR = "battery-history"; private static final String FILE_SUFFIX = ".bh"; @@ -211,6 +211,8 @@ public class BatteryStatsHistory { private final MonotonicClock mMonotonicClock; // Monotonic time when we started writing to the history buffer private long mHistoryBufferStartTime; + // Monotonic time when the last event was written to the history buffer + private long mHistoryMonotonicEndTime; // Monotonically increasing size of written history private long mMonotonicHistorySize; private final ArraySet<PowerStats.Descriptor> mWrittenPowerStatsDescriptors = new ArraySet<>(); @@ -423,13 +425,22 @@ public class BatteryStatsHistory { return file; } - void writeToParcel(Parcel out, boolean useBlobs) { + void writeToParcel(Parcel out, boolean useBlobs, + long preferredEarliestIncludedTimestampMs) { Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.writeToParcel"); lock(); try { final long start = SystemClock.uptimeMillis(); - out.writeInt(mHistoryFiles.size() - 1); for (int i = 0; i < mHistoryFiles.size() - 1; i++) { + long monotonicEndTime = Long.MAX_VALUE; + if (i < mHistoryFiles.size() - 1) { + monotonicEndTime = mHistoryFiles.get(i + 1).monotonicTimeMs; + } + + if (monotonicEndTime < preferredEarliestIncludedTimestampMs) { + continue; + } + AtomicFile file = mHistoryFiles.get(i).atomicFile; byte[] raw = new byte[0]; try { @@ -437,6 +448,8 @@ public class BatteryStatsHistory { } catch (Exception e) { Slog.e(TAG, "Error reading file " + file.getBaseFile().getPath(), e); } + + out.writeBoolean(true); if (useBlobs) { out.writeBlob(raw); } else { @@ -444,6 +457,7 @@ public class BatteryStatsHistory { out.writeByteArray(raw); } } + out.writeBoolean(false); if (DEBUG) { Slog.d(TAG, "writeToParcel duration ms:" + (SystemClock.uptimeMillis() - start)); @@ -634,6 +648,7 @@ public class BatteryStatsHistory { mWritableHistory = writableHistory; if (mWritableHistory != null) { mMutable = false; + mHistoryMonotonicEndTime = mWritableHistory.mHistoryMonotonicEndTime; } if (historyBuffer != null) { @@ -937,6 +952,8 @@ public class BatteryStatsHistory { } // skip monotonic time field. p.readLong(); + // skip monotonic end time field + p.readLong(); // skip monotonic size field p.readLong(); @@ -996,6 +1013,8 @@ public class BatteryStatsHistory { } // skip monotonic time field. out.readLong(); + // skip monotonic end time field + out.readLong(); // skip monotonic size field out.readLong(); return true; @@ -1024,6 +1043,7 @@ public class BatteryStatsHistory { p.setDataPosition(0); p.readInt(); // Skip the version field long monotonicTime = p.readLong(); + p.readLong(); // Skip monotonic end time field p.readLong(); // Skip monotonic size field p.setDataPosition(pos); return monotonicTime; @@ -1086,7 +1106,10 @@ public class BatteryStatsHistory { public void writeToParcel(Parcel out) { synchronized (this) { writeHistoryBuffer(out); - writeToParcel(out, false /* useBlobs */); + /* useBlobs */ + if (mHistoryDir != null) { + mHistoryDir.writeToParcel(out, false /* useBlobs */, 0); + } } } @@ -1096,16 +1119,13 @@ public class BatteryStatsHistory { * * @param out the output parcel */ - public void writeToBatteryUsageStatsParcel(Parcel out) { + public void writeToBatteryUsageStatsParcel(Parcel out, long preferredHistoryDurationMs) { synchronized (this) { out.writeBlob(mHistoryBuffer.marshall()); - writeToParcel(out, true /* useBlobs */); - } - } - - private void writeToParcel(Parcel out, boolean useBlobs) { - if (mHistoryDir != null) { - mHistoryDir.writeToParcel(out, useBlobs); + if (mHistoryDir != null) { + mHistoryDir.writeToParcel(out, true /* useBlobs */, + mHistoryMonotonicEndTime - preferredHistoryDurationMs); + } } } @@ -1166,8 +1186,7 @@ public class BatteryStatsHistory { private void readFromParcel(Parcel in, boolean useBlobs) { final long start = SystemClock.uptimeMillis(); mHistoryParcels = new ArrayList<>(); - final int count = in.readInt(); - for (int i = 0; i < count; i++) { + while (in.readBoolean()) { byte[] temp = useBlobs ? in.readBlob() : in.createByteArray(); if (temp == null || temp.length == 0) { continue; @@ -2081,6 +2100,8 @@ public class BatteryStatsHistory { */ @GuardedBy("this") private void writeHistoryDelta(Parcel dest, HistoryItem cur, HistoryItem last) { + mHistoryMonotonicEndTime = cur.time; + if (last == null || cur.cmd != HistoryItem.CMD_UPDATE) { dest.writeInt(BatteryStatsHistory.DELTA_TIME_ABS); cur.writeToParcel(dest, 0); @@ -2396,6 +2417,7 @@ public class BatteryStatsHistory { } mHistoryBufferStartTime = in.readLong(); + mHistoryMonotonicEndTime = in.readLong(); mMonotonicHistorySize = in.readLong(); mHistoryBuffer.setDataSize(0); @@ -2424,6 +2446,7 @@ public class BatteryStatsHistory { private void writeHistoryBuffer(Parcel out) { out.writeInt(BatteryStatsHistory.VERSION); out.writeLong(mHistoryBufferStartTime); + out.writeLong(mHistoryMonotonicEndTime); out.writeLong(mMonotonicHistorySize); out.writeInt(mHistoryBuffer.dataSize()); if (DEBUG) { diff --git a/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java index e0a77d2be724..1f9df3cc842a 100644 --- a/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java @@ -28,6 +28,7 @@ import com.android.internal.protolog.common.IProtoLogGroup; import java.io.FileInputStream; import java.io.FileNotFoundException; +import java.io.IOException; import java.util.ArrayList; public class ProcessedPerfettoProtoLogImpl extends PerfettoProtoLogImpl { @@ -161,15 +162,39 @@ public class ProcessedPerfettoProtoLogImpl extends PerfettoProtoLogImpl { messageString = message.getMessage(mViewerConfigReader); if (messageString == null) { - throw new RuntimeException("Failed to decode message for logcat. " - + "Message hash (" + message.getMessageHash() + ") either not available in " - + "viewerConfig file (" + mViewerConfigFilePath + ") or " - + "not loaded into memory from file before decoding."); + // Either we failed to load the config for this log message from the viewer config file + // into memory, or the message hash is simply not available in the viewer config file. + // We want to confirm that the message hash is not available in the viewer config file + // before throwing an exception. + throw new RuntimeException(getReasonForFailureToGetMessageString(message)); } return messageString; } + private String getReasonForFailureToGetMessageString(Message message) { + if (message.getMessageHash() == null) { + return "Trying to get message from null message hash"; + } + + try { + if (mViewerConfigReader.messageHashIsAvailableInFile(message.getMessageHash())) { + return "Failed to decode message for logcat logging. " + + "Message hash (" + message.getMessageHash() + ") is not available in " + + "viewerConfig file (" + mViewerConfigFilePath + "). This might be due " + + "to the viewer config file and the executing code being out of sync."; + } else { + return "Failed to decode message for logcat. " + + "Message hash (" + message.getMessageHash() + ") was available in the " + + "viewerConfig file (" + mViewerConfigFilePath + ") but wasn't loaded " + + "into memory from file before decoding! This is likely a bug."; + } + } catch (IOException e) { + return "Failed to get string message to log but could not identify the root cause due " + + "to an IO error in reading the viewer config file."; + } + } + private void loadLogcatGroupsViewerConfig(@NonNull IProtoLogGroup[] protoLogGroups) { final var groupsLoggingToLogcat = new ArrayList<String>(); for (IProtoLogGroup protoLogGroup : protoLogGroups) { diff --git a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java index 524f64225084..f77179949fbf 100644 --- a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java +++ b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java @@ -100,6 +100,36 @@ public class ProtoLogViewerConfigReader { } } + /** + * Return whether or not the viewer config file contains a message with the specified hash. + * @param messageHash The hash message we are looking for in the viewer config file + * @return True iff the message with message hash is contained in the viewer config. + * @throws IOException if there was an issue reading the viewer config file. + */ + public boolean messageHashIsAvailableInFile(long messageHash) + throws IOException { + try (var pisWrapper = mViewerConfigInputStreamProvider.getInputStream()) { + final var pis = pisWrapper.get(); + while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + if (pis.getFieldNumber() == (int) MESSAGES) { + final long inMessageToken = pis.start(MESSAGES); + + while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + if (pis.getFieldNumber() == (int) MESSAGE_ID) { + if (pis.readLong(MESSAGE_ID) == messageHash) { + return true; + } + } + } + + pis.end(inMessageToken); + } + } + } + + return false; + } + @NonNull private Map<Long, String> loadViewerConfigMappingForGroup(@NonNull String group) throws IOException { diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index ec3975205542..72cb9d1a20ac 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -51,6 +51,14 @@ oneway interface IStatusBar void showWirelessChargingAnimation(int batteryLevel); + /** + * Sets the new IME window status. + * + * @param displayId The id of the display to which the IME is bound. + * @param vis The IME window visibility. + * @param backDisposition The IME back disposition mode. + * @param showImeSwitcher Whether the IME Switcher button should be shown. + */ void setImeWindowStatus(int displayId, int vis, int backDisposition, boolean showImeSwitcher); void setWindowState(int display, int window, int state); diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl index ec0954d5590a..1fa1e0bbc69a 100644 --- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl @@ -61,6 +61,14 @@ interface IStatusBarService void setIconVisibility(String slot, boolean visible); @UnsupportedAppUsage void removeIcon(String slot); + /** + * Sets the new IME window status. + * + * @param displayId The id of the display to which the IME is bound. + * @param vis The IME window visibility. + * @param backDisposition The IME back disposition mode. + * @param showImeSwitcher Whether the IME Switcher button should be shown. + */ void setImeWindowStatus(int displayId, int vis, int backDisposition, boolean showImeSwitcher); void expandSettingsPanel(String subPanel); diff --git a/core/java/com/android/internal/widget/ConversationLayout.java b/core/java/com/android/internal/widget/ConversationLayout.java index b3ab5d3cd258..04ce9bcd7afd 100644 --- a/core/java/com/android/internal/widget/ConversationLayout.java +++ b/core/java/com/android/internal/widget/ConversationLayout.java @@ -105,6 +105,7 @@ public class ConversationLayout extends FrameLayout private ArrayList<MessagingGroup> mAddedGroups = new ArrayList<>(); private Person mUser; private CharSequence mNameReplacement; + private CharSequence mSummarizedContent; private boolean mIsCollapsed; private ImageResolver mImageResolver; private CachingIconView mConversationIconView; @@ -397,7 +398,7 @@ public class ConversationLayout extends FrameLayout * * @param isCollapsed is it collapsed */ - @RemotableViewMethod + @RemotableViewMethod(asyncImpl = "setIsCollapsedAsync") public void setIsCollapsed(boolean isCollapsed) { mIsCollapsed = isCollapsed; mMessagingLinearLayout.setMaxDisplayedLines(isCollapsed ? 1 : Integer.MAX_VALUE); @@ -406,6 +407,15 @@ public class ConversationLayout extends FrameLayout } /** + * setDataAsync needs to do different stuff for the collapsed vs expanded view, so store the + * collapsed state early. + */ + public Runnable setIsCollapsedAsync(boolean isCollapsed) { + mIsCollapsed = isCollapsed; + return () -> setIsCollapsed(isCollapsed); + } + + /** * Set conversation data * * @param extras Bundle contains conversation data @@ -439,8 +449,16 @@ public class ConversationLayout extends FrameLayout extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false); int unreadCount = extras.getInt(Notification.EXTRA_CONVERSATION_UNREAD_MESSAGE_COUNT); - final List<MessagingMessage> newMessagingMessages = - createMessages(newMessages, /* isHistoric= */false, usePrecomputedText); + List<MessagingMessage> newMessagingMessages; + mSummarizedContent = extras.getCharSequence(Notification.EXTRA_SUMMARIZED_CONTENT); + if (mSummarizedContent != null && mIsCollapsed) { + Notification.MessagingStyle.Message summary = + new Notification.MessagingStyle.Message(mSummarizedContent, 0, ""); + newMessagingMessages = createMessages(List.of(summary), false, usePrecomputedText); + } else { + newMessagingMessages = + createMessages(newMessages, /* isHistoric= */false, usePrecomputedText); + } final List<MessagingMessage> newHistoricMessagingMessages = createMessages(newHistoricMessages, /* isHistoric= */true, usePrecomputedText); @@ -463,7 +481,7 @@ public class ConversationLayout extends FrameLayout return new MessagingData(user, showSpinner, unreadCount, newHistoricMessagingMessages, newMessagingMessages, groups, senders, - conversationHeaderData); + conversationHeaderData, mSummarizedContent); } /** @@ -1622,6 +1640,9 @@ public class ConversationLayout extends FrameLayout @Nullable public CharSequence getConversationText() { + if (mSummarizedContent != null) { + return mSummarizedContent; + } if (mMessages.isEmpty()) { return null; } diff --git a/core/java/com/android/internal/widget/MessagingData.java b/core/java/com/android/internal/widget/MessagingData.java index fb1f28fb8ef3..cb5041efd10f 100644 --- a/core/java/com/android/internal/widget/MessagingData.java +++ b/core/java/com/android/internal/widget/MessagingData.java @@ -32,6 +32,7 @@ final class MessagingData { private final List<List<MessagingMessage>> mGroups; private final List<Person> mSenders; private final int mUnreadCount; + private final CharSequence mSummarization; private ConversationHeaderData mConversationHeaderData; @@ -41,8 +42,7 @@ final class MessagingData { List<Person> senders) { this(user, showSpinner, /* unreadCount= */0, historicMessagingMessages, newMessagingMessages, - groups, - senders, null); + groups, senders, null, null); } MessagingData(Person user, boolean showSpinner, @@ -51,7 +51,8 @@ final class MessagingData { List<MessagingMessage> newMessagingMessages, List<List<MessagingMessage>> groups, List<Person> senders, - @Nullable ConversationHeaderData conversationHeaderData) { + @Nullable ConversationHeaderData conversationHeaderData, + CharSequence summarization) { mUser = user; mShowSpinner = showSpinner; mUnreadCount = unreadCount; @@ -60,6 +61,7 @@ final class MessagingData { mGroups = groups; mSenders = senders; mConversationHeaderData = conversationHeaderData; + mSummarization = summarization; } public Person getUser() { @@ -94,4 +96,9 @@ final class MessagingData { public ConversationHeaderData getConversationHeaderData() { return mConversationHeaderData; } + + @Nullable + public CharSequence getSummarization() { + return mSummarization; + } } diff --git a/core/java/com/android/internal/widget/MessagingMessage.java b/core/java/com/android/internal/widget/MessagingMessage.java index a59ee77cc693..c7f22836dd93 100644 --- a/core/java/com/android/internal/widget/MessagingMessage.java +++ b/core/java/com/android/internal/widget/MessagingMessage.java @@ -24,7 +24,7 @@ import java.util.ArrayList; import java.util.Objects; /** - * A message of a {@link MessagingLayout}. + * A message or summary of a {@link MessagingLayout}. */ public interface MessagingMessage extends MessagingLinearLayout.MessagingChild { diff --git a/core/java/com/android/internal/widget/NotificationProgressDrawable.java b/core/java/com/android/internal/widget/NotificationProgressDrawable.java index 4ece81c24edc..30dcc67d9ce5 100644 --- a/core/java/com/android/internal/widget/NotificationProgressDrawable.java +++ b/core/java/com/android/internal/widget/NotificationProgressDrawable.java @@ -132,6 +132,8 @@ public final class NotificationProgressDrawable extends Drawable { final float centerY = (float) getBounds().centerY(); final int numParts = mParts.size(); + final float pointTop = Math.round(centerY - pointRadius); + final float pointBottom = Math.round(centerY + pointRadius); for (int iPart = 0; iPart < numParts; iPart++) { final DrawablePart part = mParts.get(iPart); final float start = left + part.mStart; @@ -146,12 +148,13 @@ public final class NotificationProgressDrawable extends Drawable { mFillPaint.setColor(segment.mColor); - mSegRectF.set(start, centerY - radiusY, end, centerY + radiusY); + mSegRectF.set(Math.round(start), Math.round(centerY - radiusY), Math.round(end), + Math.round(centerY + radiusY)); canvas.drawRoundRect(mSegRectF, cornerRadius, cornerRadius, mFillPaint); } else if (part instanceof DrawablePoint point) { // TODO: b/367804171 - actually use a vector asset for the default point // rather than drawing it as a box? - mPointRectF.set(start, centerY - pointRadius, end, centerY + pointRadius); + mPointRectF.set(Math.round(start), pointTop, Math.round(end), pointBottom); final float inset = mState.mPointRectInset; final float cornerRadius = mState.mPointRectCornerRadius; mPointRectF.inset(inset, inset); diff --git a/core/java/com/android/internal/widget/remotecompose/accessibility/CoreDocumentAccessibility.java b/core/java/com/android/internal/widget/remotecompose/accessibility/CoreDocumentAccessibility.java index 2cd4f0362306..52d51539867d 100644 --- a/core/java/com/android/internal/widget/remotecompose/accessibility/CoreDocumentAccessibility.java +++ b/core/java/com/android/internal/widget/remotecompose/accessibility/CoreDocumentAccessibility.java @@ -21,6 +21,7 @@ import android.os.Bundle; import com.android.internal.widget.remotecompose.core.CoreDocument; import com.android.internal.widget.remotecompose.core.Operation; +import com.android.internal.widget.remotecompose.core.RemoteContext; import com.android.internal.widget.remotecompose.core.operations.layout.ClickModifierOperation; import com.android.internal.widget.remotecompose.core.operations.layout.Component; import com.android.internal.widget.remotecompose.core.operations.layout.LayoutComponent; @@ -45,9 +46,11 @@ import java.util.stream.Stream; */ public class CoreDocumentAccessibility implements RemoteComposeDocumentAccessibility { private final CoreDocument mDocument; + private final RemoteContext mRemoteContext; - public CoreDocumentAccessibility(CoreDocument document) { + public CoreDocumentAccessibility(CoreDocument document, RemoteContext remoteContext) { this.mDocument = document; + this.mRemoteContext = remoteContext; } @Nullable @@ -95,7 +98,7 @@ public class CoreDocumentAccessibility implements RemoteComposeDocumentAccessibi @Override public boolean performAction(Component component, int action, Bundle arguments) { if (action == ACTION_CLICK) { - mDocument.performClick(component.getComponentId()); + mDocument.performClick(mRemoteContext, component.getComponentId()); return true; } else { return false; diff --git a/core/java/com/android/internal/widget/remotecompose/accessibility/PlatformRemoteComposeAccessibilityRegistrar.java b/core/java/com/android/internal/widget/remotecompose/accessibility/PlatformRemoteComposeAccessibilityRegistrar.java index 010253e9cb95..975383ee36b4 100644 --- a/core/java/com/android/internal/widget/remotecompose/accessibility/PlatformRemoteComposeAccessibilityRegistrar.java +++ b/core/java/com/android/internal/widget/remotecompose/accessibility/PlatformRemoteComposeAccessibilityRegistrar.java @@ -19,6 +19,7 @@ import android.annotation.NonNull; import android.view.View; import com.android.internal.widget.remotecompose.core.CoreDocument; +import com.android.internal.widget.remotecompose.core.RemoteContextAware; /** * Trivial wrapper for calling setAccessibilityDelegate on a View. This exists primarily because the @@ -31,7 +32,8 @@ public class PlatformRemoteComposeAccessibilityRegistrar View player, @NonNull CoreDocument coreDocument) { return new PlatformRemoteComposeTouchHelper( player, - new CoreDocumentAccessibility(coreDocument), + new CoreDocumentAccessibility( + coreDocument, ((RemoteContextAware) player).getRemoteContext()), new AndroidPlatformSemanticNodeApplier()); } diff --git a/core/java/com/android/internal/widget/remotecompose/accessibility/PlatformRemoteComposeTouchHelper.java b/core/java/com/android/internal/widget/remotecompose/accessibility/PlatformRemoteComposeTouchHelper.java index 43118a0800fb..c8474b19058f 100644 --- a/core/java/com/android/internal/widget/remotecompose/accessibility/PlatformRemoteComposeTouchHelper.java +++ b/core/java/com/android/internal/widget/remotecompose/accessibility/PlatformRemoteComposeTouchHelper.java @@ -28,6 +28,7 @@ import android.view.accessibility.AccessibilityNodeInfo; import com.android.internal.widget.ExploreByTouchHelper; import com.android.internal.widget.remotecompose.core.CoreDocument; +import com.android.internal.widget.remotecompose.core.RemoteContextAware; import com.android.internal.widget.remotecompose.core.operations.layout.Component; import com.android.internal.widget.remotecompose.core.semantics.AccessibilitySemantics; import com.android.internal.widget.remotecompose.core.semantics.AccessibleComponent.Mode; @@ -55,7 +56,8 @@ public class PlatformRemoteComposeTouchHelper extends ExploreByTouchHelper { View player, @NonNull CoreDocument coreDocument) { return new PlatformRemoteComposeTouchHelper( player, - new CoreDocumentAccessibility(coreDocument), + new CoreDocumentAccessibility( + coreDocument, ((RemoteContextAware) player).getRemoteContext()), new AndroidPlatformSemanticNodeApplier()); } diff --git a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java index f5f4e4332d28..0cfaf5592d6f 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java +++ b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java @@ -62,7 +62,7 @@ public class CoreDocument { // We also keep a more fine-grained BUILD number, exposed as // ID_API_LEVEL = DOCUMENT_API_LEVEL + BUILD - static final float BUILD = 0.3f; + static final float BUILD = 0.4f; @NonNull ArrayList<Operation> mOperations = new ArrayList<>(); @@ -860,16 +860,22 @@ public class CoreDocument { * * @param id the click area id */ - public void performClick(int id) { + public void performClick(@NonNull RemoteContext context, int id) { for (ClickAreaRepresentation clickArea : mClickAreas) { if (clickArea.mId == id) { warnClickListeners(clickArea); return; } } + for (IdActionCallback listener : mIdActionListeners) { listener.onAction(id, ""); } + + Component component = getComponent(id); + if (component != null) { + component.onClick(context, this, -1, -1); + } } /** Warn click listeners when a click area is activated */ diff --git a/core/java/com/android/internal/widget/remotecompose/core/Operations.java b/core/java/com/android/internal/widget/remotecompose/core/Operations.java index 0b6a3c415e4a..3760af2f7460 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/Operations.java +++ b/core/java/com/android/internal/widget/remotecompose/core/Operations.java @@ -18,6 +18,7 @@ package com.android.internal.widget.remotecompose.core; import android.annotation.NonNull; import com.android.internal.widget.remotecompose.core.operations.BitmapData; +import com.android.internal.widget.remotecompose.core.operations.BitmapFontData; import com.android.internal.widget.remotecompose.core.operations.ClickArea; import com.android.internal.widget.remotecompose.core.operations.ClipPath; import com.android.internal.widget.remotecompose.core.operations.ClipRect; @@ -30,6 +31,7 @@ import com.android.internal.widget.remotecompose.core.operations.DataMapIds; import com.android.internal.widget.remotecompose.core.operations.DataMapLookup; import com.android.internal.widget.remotecompose.core.operations.DrawArc; import com.android.internal.widget.remotecompose.core.operations.DrawBitmap; +import com.android.internal.widget.remotecompose.core.operations.DrawBitmapFontText; import com.android.internal.widget.remotecompose.core.operations.DrawBitmapInt; import com.android.internal.widget.remotecompose.core.operations.DrawBitmapScaled; import com.android.internal.widget.remotecompose.core.operations.DrawCircle; @@ -45,6 +47,8 @@ import com.android.internal.widget.remotecompose.core.operations.DrawTextOnPath; import com.android.internal.widget.remotecompose.core.operations.DrawTweenPath; import com.android.internal.widget.remotecompose.core.operations.FloatConstant; import com.android.internal.widget.remotecompose.core.operations.FloatExpression; +import com.android.internal.widget.remotecompose.core.operations.FloatFunctionCall; +import com.android.internal.widget.remotecompose.core.operations.FloatFunctionDefine; import com.android.internal.widget.remotecompose.core.operations.Header; import com.android.internal.widget.remotecompose.core.operations.IntegerExpression; import com.android.internal.widget.remotecompose.core.operations.MatrixRestore; @@ -147,12 +151,14 @@ public class Operations { public static final int DATA_BITMAP = 101; public static final int DATA_SHADER = 45; public static final int DATA_TEXT = 102; + public static final int DATA_BITMAP_FONT = 167; ///////////////////////////// ===================== public static final int CLIP_PATH = 38; public static final int CLIP_RECT = 39; public static final int PAINT_VALUES = 40; public static final int DRAW_RECT = 42; + public static final int DRAW_BITMAP_FONT_TEXT_RUN = 48; public static final int DRAW_TEXT_RUN = 43; public static final int DRAW_CIRCLE = 46; public static final int DRAW_LINE = 47; @@ -196,11 +202,13 @@ public class Operations { public static final int PATH_TWEEN = 158; public static final int PATH_CREATE = 159; public static final int PATH_ADD = 160; - public static final int PARTICLE_CREATE = 161; + public static final int PARTICLE_DEFINE = 161; public static final int PARTICLE_PROCESS = 162; public static final int PARTICLE_LOOP = 163; public static final int IMPULSE_START = 164; public static final int IMPULSE_PROCESS = 165; + public static final int FUNCTION_CALL = 166; + public static final int FUNCTION_DEFINE = 168; ///////////////////////////////////////// ====================== @@ -276,6 +284,7 @@ public class Operations { map.put(HEADER, Header::read); map.put(DRAW_BITMAP_INT, DrawBitmapInt::read); map.put(DATA_BITMAP, BitmapData::read); + map.put(DATA_BITMAP_FONT, BitmapFontData::read); map.put(DATA_TEXT, TextData::read); map.put(THEME, Theme::read); map.put(CLICK_AREA, ClickArea::read); @@ -292,6 +301,7 @@ public class Operations { map.put(DRAW_ROUND_RECT, DrawRoundRect::read); map.put(DRAW_TEXT_ON_PATH, DrawTextOnPath::read); map.put(DRAW_TEXT_RUN, DrawText::read); + map.put(DRAW_BITMAP_FONT_TEXT_RUN, DrawBitmapFontText::read); map.put(DRAW_TWEEN_PATH, DrawTweenPath::read); map.put(DATA_PATH, PathData::read); map.put(PAINT_VALUES, PaintData::read); @@ -389,8 +399,10 @@ public class Operations { map.put(PATH_ADD, PathAppend::read); map.put(IMPULSE_START, ImpulseOperation::read); map.put(IMPULSE_PROCESS, ImpulseProcess::read); - map.put(PARTICLE_CREATE, ParticlesCreate::read); + map.put(PARTICLE_DEFINE, ParticlesCreate::read); map.put(PARTICLE_LOOP, ParticlesLoop::read); + map.put(FUNCTION_CALL, FloatFunctionCall::read); + map.put(FUNCTION_DEFINE, FloatFunctionDefine::read); map.put(ACCESSIBILITY_SEMANTICS, CoreSemantics::read); // map.put(ACCESSIBILITY_CUSTOM_ACTION, CoreSemantics::read); diff --git a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java index 1cb8fefde80c..f83ecef1074d 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java +++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java @@ -21,6 +21,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import com.android.internal.widget.remotecompose.core.operations.BitmapData; +import com.android.internal.widget.remotecompose.core.operations.BitmapFontData; import com.android.internal.widget.remotecompose.core.operations.ClickArea; import com.android.internal.widget.remotecompose.core.operations.ClipPath; import com.android.internal.widget.remotecompose.core.operations.ClipRect; @@ -33,6 +34,7 @@ import com.android.internal.widget.remotecompose.core.operations.DataMapIds; import com.android.internal.widget.remotecompose.core.operations.DataMapLookup; import com.android.internal.widget.remotecompose.core.operations.DrawArc; import com.android.internal.widget.remotecompose.core.operations.DrawBitmap; +import com.android.internal.widget.remotecompose.core.operations.DrawBitmapFontText; import com.android.internal.widget.remotecompose.core.operations.DrawBitmapInt; import com.android.internal.widget.remotecompose.core.operations.DrawBitmapScaled; import com.android.internal.widget.remotecompose.core.operations.DrawCircle; @@ -48,6 +50,8 @@ import com.android.internal.widget.remotecompose.core.operations.DrawTextOnPath; import com.android.internal.widget.remotecompose.core.operations.DrawTweenPath; import com.android.internal.widget.remotecompose.core.operations.FloatConstant; import com.android.internal.widget.remotecompose.core.operations.FloatExpression; +import com.android.internal.widget.remotecompose.core.operations.FloatFunctionCall; +import com.android.internal.widget.remotecompose.core.operations.FloatFunctionDefine; import com.android.internal.widget.remotecompose.core.operations.Header; import com.android.internal.widget.remotecompose.core.operations.IntegerExpression; import com.android.internal.widget.remotecompose.core.operations.MatrixRestore; @@ -557,6 +561,18 @@ public class RemoteComposeBuffer { } /** + * Records a bitmap font and returns an ID. + * + * @param glyphs The glyphs that define the bitmap font + * @return id of the BitmapFont + */ + public int addBitmapFont(BitmapFontData.Glyph[] glyphs) { + int id = mRemoteComposeState.nextId(); + BitmapFontData.apply(mBuffer, id, glyphs); + return id; + } + + /** * This defines the name of the bitmap given the id. * * @param id of the Bitmap @@ -825,6 +841,22 @@ public class RemoteComposeBuffer { } /** + * Draw the text with a bitmap font, with origin at (x,y). The origin is interpreted based on + * the Align setting in the paint. + * + * @param textId The text to be drawn + * @param bitmapFontId The id of the bitmap font to draw with + * @param start The index of the first character in text to draw + * @param end (end - 1) is the index of the last character in text to draw + * @param x The x-coordinate of the origin of the text being drawn + * @param y The y-coordinate of the baseline of the text being drawn + */ + public void addDrawBitmapFontTextRun( + int textId, int bitmapFontId, int start, int end, float x, float y) { + DrawBitmapFontText.apply(mBuffer, textId, bitmapFontId, start, end, x, y); + } + + /** * Draw a text on canvas at relative to position (x, y), offset panX and panY. <br> * The panning factors (panX, panY) mapped to the resulting bounding box of the text, in such a * way that a panning factor of (0.0, 0.0) would center the text at (x, y) @@ -1060,6 +1092,14 @@ public class RemoteComposeBuffer { return "v1.0"; } + /** + * Initialize a buffer from a file + * + * @param path the file path + * @param remoteComposeState the associated state + * @return the RemoteComposeBuffer + * @throws IOException + */ @NonNull public static RemoteComposeBuffer fromFile( @NonNull String path, @NonNull RemoteComposeState remoteComposeState) @@ -1134,11 +1174,24 @@ public class RemoteComposeBuffer { } } + /** + * Read the content of the file into the buffer + * + * @param file a target file + * @param buffer a RemoteComposeBuffer + * @throws IOException + */ static void read(@NonNull File file, @NonNull RemoteComposeBuffer buffer) throws IOException { FileInputStream fd = new FileInputStream(file); read(fd, buffer); } + /** + * Initialize a buffer from an input stream + * + * @param fd the input stream + * @param buffer a RemoteComposeBuffer + */ public static void read(@NonNull InputStream fd, @NonNull RemoteComposeBuffer buffer) { try { byte[] bytes = readAllBytes(fd); @@ -1150,6 +1203,13 @@ public class RemoteComposeBuffer { } } + /** + * Load a byte buffer from the input stream + * + * @param is the input stream + * @return a byte buffer containing the input stream content + * @throws IOException + */ private static byte[] readAllBytes(@NonNull InputStream is) throws IOException { byte[] buff = new byte[32 * 1024]; // moderate size buff to start int red = 0; @@ -1684,7 +1744,27 @@ public class RemoteComposeBuffer { * @return id of the color (color ids are short) */ public short addColorExpression(int alpha, float hue, float sat, float value) { - ColorExpression c = new ColorExpression(0, alpha, hue, sat, value); + ColorExpression c = + new ColorExpression(0, ColorExpression.HSV_MODE, alpha, hue, sat, value); + short id = (short) mRemoteComposeState.cacheData(c); + c.mId = id; + c.write(mBuffer); + return id; + } + + /** + * Color calculated by Alpha, Red, Green and Blue. (as floats they can be variables used to + * create color transitions) + * + * @param alpha the alpha value of the color + * @param red the red component of the color + * @param green the green component of the color + * @param blue the blue component of the color + * @return id of the color (color ids are short) + */ + public short addColorExpression(float alpha, float red, float green, float blue) { + ColorExpression c = + new ColorExpression(0, ColorExpression.ARGB_MODE, alpha, red, green, blue); short id = (short) mRemoteComposeState.cacheData(c); c.mId = id; c.write(mBuffer); @@ -2179,10 +2259,21 @@ public class RemoteComposeBuffer { textAlign); } + /** + * Returns the next available id for the given type + * + * @param type the type of the value + * @return a unique id + */ public int createID(int type) { return mRemoteComposeState.nextId(type); } + /** + * Returns the next available id + * + * @return a unique id + */ public int nextId() { return mRemoteComposeState.nextId(); } @@ -2243,4 +2334,27 @@ public class RemoteComposeBuffer { public void addParticleLoopEnd() { ContainerEnd.apply(mBuffer); } + + /** + * @param fid The id of the function + * @param args The arguments of the function + */ + public void defineFloatFunction(int fid, int[] args) { + FloatFunctionDefine.apply(mBuffer, fid, args); + } + + /** end the definition of the function */ + public void addEndFloatFunctionDef() { + ContainerEnd.apply(mBuffer); + } + + /** + * add a function call + * + * @param id the id of the function to call + * @param args the arguments of the function + */ + public void callFloatFunction(int id, float[] args) { + FloatFunctionCall.apply(mBuffer, id, args); + } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromNotificationsShadeToQuickSettingsShadeTransition.kt b/core/java/com/android/internal/widget/remotecompose/core/RemoteContextAware.java index 9a7f99f3247b..bf9a8c02d525 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromNotificationsShadeToQuickSettingsShadeTransition.kt +++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteContextAware.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 The Android Open Source Project + * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,17 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.android.internal.widget.remotecompose.core; -package com.android.systemui.scene.ui.composable.transitions - -import androidx.compose.animation.core.tween -import com.android.compose.animation.scene.TransitionBuilder -import kotlin.time.Duration.Companion.milliseconds +/** + * This interface defines a contract for objects that are aware of a {@link RemoteContext}. + * + * <p>PlayerViews should implement to provide access to the RemoteContext. + */ +public interface RemoteContextAware { -fun TransitionBuilder.notificationsShadeToQuickSettingsShadeTransition( - durationScale: Double = 1.0 -) { - spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) + /** + * Returns the remote context + * + * @return a RemoteContext + */ + RemoteContext getRemoteContext(); } - -private val DefaultDuration = 300.milliseconds diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/BitmapFontData.java b/core/java/com/android/internal/widget/remotecompose/core/operations/BitmapFontData.java new file mode 100644 index 000000000000..cbd30dc21caf --- /dev/null +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/BitmapFontData.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.widget.remotecompose.core.operations; + +import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT_ARRAY; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import com.android.internal.widget.remotecompose.core.Operation; +import com.android.internal.widget.remotecompose.core.Operations; +import com.android.internal.widget.remotecompose.core.RemoteContext; +import com.android.internal.widget.remotecompose.core.WireBuffer; +import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; +import com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation; + +import java.util.Arrays; +import java.util.List; + +/** Operation to deal with bitmap font data. */ +public class BitmapFontData extends Operation { + private static final int OP_CODE = Operations.DATA_BITMAP_FONT; + private static final String CLASS_NAME = "BitmapFontData"; + + int mId; + + // Sorted in order of decreasing mChars length. + @NonNull Glyph[] mFontGlyphs; + + /** + * A bitmap font is comprised of a collection of Glyphs. Note each Glyph has its own bitmap + * rather than using a texture atlas. + */ + public static class Glyph { + /** The character(s) this glyph represents. */ + public String mChars; + + /** The id of the bitmap for this glyph, or -1 for space. */ + public int mBitmapId; + + /** The margin in pixels to the left of the glyph bitmap. */ + public short mMarginLeft; + + /** The margin in pixels above of the glyph bitmap. */ + public short mMarginTop; + + /** The margin in pixels to the right of the glyph bitmap. */ + public short mMarginRight; + + /** The margin in pixels below the glyph bitmap. */ + public short mMarginBottom; + + public short mBitmapWidth; + public short mBitmapHeight; + + public Glyph() {} + + public Glyph( + String chars, + int bitmapId, + short marginLeft, + short marginTop, + short marginRight, + short marginBottom, + short width, + short height) { + mChars = chars; + mBitmapId = bitmapId; + mMarginLeft = marginLeft; + mMarginTop = marginTop; + mMarginRight = marginRight; + mMarginBottom = marginBottom; + mBitmapWidth = width; + mBitmapHeight = height; + } + } + + /** + * create a bitmap font structure. + * + * @param id the id of the bitmap font + * @param fontGlyphs the glyphs that define the bitmap font + */ + public BitmapFontData(int id, @NonNull Glyph[] fontGlyphs) { + mId = id; + mFontGlyphs = fontGlyphs; + + // Sort in order of decreasing mChars length. + Arrays.sort(mFontGlyphs, (o1, o2) -> o2.mChars.length() - o1.mChars.length()); + } + + @Override + public void write(@NonNull WireBuffer buffer) { + apply(buffer, mId, mFontGlyphs); + } + + @NonNull + @Override + public String toString() { + return "BITMAP FONT DATA " + mId; + } + + /** + * The name of the class + * + * @return the name + */ + @NonNull + public static String name() { + return CLASS_NAME; + } + + /** + * The OP_CODE for this command + * + * @return the opcode + */ + public static int id() { + return OP_CODE; + } + + /** + * Add the image to the document + * + * @param buffer document to write to + * @param id the id the bitmap font will be stored under + * @param glyphs glyph metadata + */ + public static void apply(@NonNull WireBuffer buffer, int id, @NonNull Glyph[] glyphs) { + buffer.start(OP_CODE); + buffer.writeInt(id); + buffer.writeInt(glyphs.length); + for (Glyph element : glyphs) { + buffer.writeUTF8(element.mChars); + buffer.writeInt(element.mBitmapId); + buffer.writeShort(element.mMarginLeft); + buffer.writeShort(element.mMarginTop); + buffer.writeShort(element.mMarginRight); + buffer.writeShort(element.mMarginBottom); + buffer.writeShort(element.mBitmapWidth); + buffer.writeShort(element.mBitmapHeight); + } + } + + /** + * Read this operation and add it to the list of operations + * + * @param buffer the buffer to read + * @param operations the list of operations that will be added to + */ + public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) { + int id = buffer.readInt(); + int numGlyphElements = buffer.readInt(); + Glyph[] glyphs = new Glyph[numGlyphElements]; + for (int i = 0; i < numGlyphElements; i++) { + glyphs[i] = new Glyph(); + glyphs[i].mChars = buffer.readUTF8(); + glyphs[i].mBitmapId = buffer.readInt(); + glyphs[i].mMarginLeft = (short) buffer.readShort(); + glyphs[i].mMarginTop = (short) buffer.readShort(); + glyphs[i].mMarginRight = (short) buffer.readShort(); + glyphs[i].mMarginBottom = (short) buffer.readShort(); + glyphs[i].mBitmapWidth = (short) buffer.readShort(); + glyphs[i].mBitmapHeight = (short) buffer.readShort(); + } + + operations.add(new BitmapFontData(id, glyphs)); + } + + /** + * Populate the documentation with a description of this operation + * + * @param doc to append the description to. + */ + public static void documentation(@NonNull DocumentationBuilder doc) { + doc.operation("Data Operations", OP_CODE, CLASS_NAME) + .description("Bitmap font data") + .field(DocumentedOperation.INT, "id", "id of bitmap font data") + .field(INT_ARRAY, "glyphNodes", "list used to greedily convert strings into glyphs") + .field(INT_ARRAY, "glyphElements", ""); + } + + @Override + public void apply(@NonNull RemoteContext context) { + context.putObject(mId, this); + } + + @NonNull + @Override + public String deepToString(@NonNull String indent) { + return indent + toString(); + } + + /** Finds the largest glyph matching the string at the specified offset, or returns null. */ + @Nullable + public Glyph lookupGlyph(String string, int offset) { + // Since mFontGlyphs is sorted on decreasing size, it will match the longest items first. + // It is expected that the mFontGlyphs array will be fairly small. + for (Glyph glyph : mFontGlyphs) { + if (string.startsWith(glyph.mChars, offset)) { + return glyph; + } + } + return null; + } +} diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/ColorExpression.java b/core/java/com/android/internal/widget/remotecompose/core/operations/ColorExpression.java index b385ecd9e5f7..73f99ccb4405 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/ColorExpression.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/ColorExpression.java @@ -38,7 +38,13 @@ public class ColorExpression extends Operation implements VariableSupport { private static final int OP_CODE = Operations.COLOR_EXPRESSIONS; private static final String CLASS_NAME = "ColorExpression"; public int mId; + + /** + * Mode of the color expression 0 = two colors and a tween 1 = color1 is a colorID. 2 color2 is + * a colorID. 3 = color1 & color2 are ids 4 = H S V mode 5 = RGB mode 6 = ARGB mode + */ int mMode; + public int mColor1; public int mColor2; public float mTween = 0.0f; @@ -51,11 +57,49 @@ public class ColorExpression extends Operation implements VariableSupport { public float mOutValue = 0; public int mAlpha = 0xFF; // only used in hsv mode + private float mArgbAlpha = 0.0f; + private float mArgbRed = 0.0f; + private float mArgbGreen = 0.0f; + private float mArgbBlue = 0.0f; + + private float mOutArgbAlpha = 0.0f; + private float mOutArgbRed = 0.0f; + private float mOutArgbGreen = 0.0f; + private float mOutArgbBlue = 0.0f; + public float mOutTween = 0.0f; public int mOutColor1; public int mOutColor2; - public static final int HSV_MODE = 4; + /** COLOR_COLOR_INTERPOLATE */ + public static final byte COLOR_COLOR_INTERPOLATE = 0; + + /** COLOR_ID_INTERPOLATE */ + public static final byte ID_COLOR_INTERPOLATE = 1; + + /** ID_COLOR_INTERPOLATE */ + public static final byte COLOR_ID_INTERPOLATE = 2; + + /** ID_ID_INTERPOLATE */ + public static final byte ID_ID_INTERPOLATE = 3; + + /** H S V mode */ + public static final byte HSV_MODE = 4; + + /** ARGB mode */ + public static final byte ARGB_MODE = 5; + + /** ARGB mode with a being an id */ + public static final byte IDARGB_MODE = 6; + + /** + * Create a new ColorExpression object + * + * @param id the id of the color + * @param hue the hue of the color + * @param sat the saturation of the color + * @param value the value of the color + */ public ColorExpression(int id, float hue, float sat, float value) { mMode = HSV_MODE; mAlpha = 0xFF; @@ -67,7 +111,21 @@ public class ColorExpression extends Operation implements VariableSupport { mTween = value; } - public ColorExpression(int id, int alpha, float hue, float sat, float value) { + /** + * Create a new ColorExpression object based on HSV + * + * @param id id of the color + * @param mode the mode of the color + * @param alpha the alpha of the color + * @param hue the hue of the color + * @param sat the saturation of the color + * @param value the value (brightness) of the color + */ + public ColorExpression(int id, byte mode, int alpha, float hue, float sat, float value) { + if (mode != HSV_MODE) { + throw new RuntimeException("Invalid mode " + mode); + } + mId = id; mMode = HSV_MODE; mAlpha = alpha; mOutHue = mHue = hue; @@ -78,6 +136,15 @@ public class ColorExpression extends Operation implements VariableSupport { mTween = value; } + /** + * Create a new ColorExpression object based interpolationg two colors + * + * @param id the id of the color + * @param mode the type of mode (are colors ids or actual values) + * @param color1 the first color to use + * @param color2 the second color to use + * @param tween the value to use to interpolate between the two colors + */ public ColorExpression(int id, int mode, int color1, int color2, float tween) { this.mId = id; this.mMode = mode & 0xFF; @@ -95,6 +162,28 @@ public class ColorExpression extends Operation implements VariableSupport { this.mOutColor2 = color2; } + /** + * Create a new ColorExpression object based on ARGB + * + * @param id the id of the color + * @param mode the mode must be ARGB_MODE + * @param alpha the alpha value of the color + * @param red the red of component the color + * @param green the greej component of the color + * @param blue the blue of component the color + */ + public ColorExpression(int id, byte mode, float alpha, float red, float green, float blue) { + if (mode != ARGB_MODE) { + throw new RuntimeException("Invalid mode " + mode); + } + mMode = ARGB_MODE; + mId = id; + mOutArgbAlpha = mArgbAlpha = alpha; + mOutArgbRed = mArgbRed = red; + mOutArgbGreen = mArgbGreen = green; + mOutArgbBlue = mArgbBlue = blue; + } + @Override public void updateVariables(@NonNull RemoteContext context) { if (mMode == 4) { @@ -108,6 +197,20 @@ public class ColorExpression extends Operation implements VariableSupport { mOutValue = context.getFloat(Utils.idFromNan(mValue)); } } + if (mMode == ARGB_MODE) { + if (Float.isNaN(mArgbAlpha)) { + mOutArgbAlpha = context.getFloat(Utils.idFromNan(mArgbAlpha)); + } + if (Float.isNaN(mArgbRed)) { + mOutArgbRed = context.getFloat(Utils.idFromNan(mArgbRed)); + } + if (Float.isNaN(mArgbGreen)) { + mOutArgbGreen = context.getFloat(Utils.idFromNan(mArgbGreen)); + } + if (Float.isNaN(mArgbBlue)) { + mOutArgbBlue = context.getFloat(Utils.idFromNan(mArgbBlue)); + } + } if (Float.isNaN(mTween)) { mOutTween = context.getFloat(Utils.idFromNan(mTween)); } @@ -146,13 +249,21 @@ public class ColorExpression extends Operation implements VariableSupport { @Override public void apply(@NonNull RemoteContext context) { - if (mMode == 4) { + if (mMode == HSV_MODE) { context.loadColor( mId, (mAlpha << 24) | (0xFFFFFF & Utils.hsvToRgb(mOutHue, mOutSat, mOutValue))); return; } + if (mMode == ARGB_MODE) { + context.loadColor( + mId, Utils.toARGB(mOutArgbAlpha, mOutArgbRed, mOutArgbGreen, mOutArgbBlue)); + return; + } if (mOutTween == 0.0) { - context.loadColor(mId, mColor1); + if ((mMode & 1) == 1) { + mOutColor1 = context.getColor(mColor1); + } + context.loadColor(mId, mOutColor1); } else { if ((mMode & 1) == 1) { mOutColor1 = context.getColor(mColor1); @@ -167,14 +278,36 @@ public class ColorExpression extends Operation implements VariableSupport { @Override public void write(@NonNull WireBuffer buffer) { - int mode = mMode | (mAlpha << 16); - apply(buffer, mId, mode, mColor1, mColor2, mTween); + int mode; + switch (mMode) { + case ARGB_MODE: + apply(buffer, mId, mArgbAlpha, mArgbRed, mArgbGreen, mArgbBlue); + break; + + case HSV_MODE: + mOutValue = mValue; + mColor1 = Float.floatToRawIntBits(mHue); + mColor2 = Float.floatToRawIntBits(mSat); + mode = mMode | (mAlpha << 16); + apply(buffer, mId, mode, mColor1, mColor2, mTween); + + break; + case COLOR_ID_INTERPOLATE: + case ID_COLOR_INTERPOLATE: + case ID_ID_INTERPOLATE: + case COLOR_COLOR_INTERPOLATE: + apply(buffer, mId, mMode, mColor1, mColor2, mTween); + + break; + default: + throw new RuntimeException("Invalid mode "); + } } @NonNull @Override public String toString() { - if (mMode == 4) { + if (mMode == HSV_MODE) { return "ColorExpression[" + mId + "] = hsv (" @@ -185,7 +318,20 @@ public class ColorExpression extends Operation implements VariableSupport { + Utils.floatToString(mValue) + ")"; } - + Utils.log(" ColorExpression toString" + mId + " " + mMode); + if (mMode == ARGB_MODE) { + return "ColorExpression[" + + mId + + "] = rgb (" + + Utils.floatToString(mArgbAlpha) + + ", " + + Utils.floatToString(mArgbRed) + + ", " + + Utils.floatToString(mArgbGreen) + + ", " + + Utils.floatToString(mArgbRed) + + ")"; + } String c1 = (mMode & 1) == 1 ? "[" + mColor1 + "]" : Utils.colorInt(mColor1); String c2 = (mMode & 2) == 2 ? "[" + mColor2 + "]" : Utils.colorInt(mColor2); return "ColorExpression[" @@ -230,12 +376,38 @@ public class ColorExpression extends Operation implements VariableSupport { */ public static void apply( @NonNull WireBuffer buffer, int id, int mode, int color1, int color2, float tween) { + apply(buffer, id, mode, color1, color2, Float.floatToRawIntBits(tween)); + } + + /** + * Call to write a ColorExpression object on the buffer + * + * @param buffer + * @param id of the ColorExpression object + * @param alpha + * @param red + * @param green + * @param blue + */ + public static void apply( + @NonNull WireBuffer buffer, int id, float alpha, float red, float green, float blue) { + int param1 = (Float.isNaN(alpha)) ? IDARGB_MODE : ARGB_MODE; + param1 |= + (Float.isNaN(alpha)) ? Utils.idFromNan(alpha) << 16 : ((int) (alpha * 1024)) << 16; + int param2 = Float.floatToRawIntBits(red); + int param3 = Float.floatToRawIntBits(green); + int param4 = Float.floatToRawIntBits(blue); + apply(buffer, id, param1, param2, param3, param4); + } + + private static void apply( + @NonNull WireBuffer buffer, int id, int param1, int param2, int param3, int param4) { buffer.start(OP_CODE); buffer.writeInt(id); - buffer.writeInt(mode); - buffer.writeInt(color1); - buffer.writeInt(color2); - buffer.writeFloat(tween); + buffer.writeInt(param1); + buffer.writeInt(param2); + buffer.writeInt(param3); + buffer.writeInt(param4); } /** @@ -246,12 +418,48 @@ public class ColorExpression extends Operation implements VariableSupport { */ public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) { int id = buffer.readInt(); - int mode = buffer.readInt(); - int color1 = buffer.readInt(); - int color2 = buffer.readInt(); - float tween = buffer.readFloat(); - - operations.add(new ColorExpression(id, mode, color1, color2, tween)); + int param1 = buffer.readInt(); + int param2 = buffer.readInt(); + int param3 = buffer.readInt(); + int param4 = buffer.readInt(); + int mode = param1 & 0xFF; + float alpha; + float red; + float green; + float blue; + switch (mode) { + case IDARGB_MODE: + alpha = Utils.asNan(param1 >> 16); + red = Float.intBitsToFloat(param2); + green = Float.intBitsToFloat(param3); + blue = Float.intBitsToFloat(param4); + operations.add(new ColorExpression(id, (byte) ARGB_MODE, alpha, red, green, blue)); + break; + case ARGB_MODE: + alpha = (param1 >> 16) / 1024.0f; + red = Float.intBitsToFloat(param2); + green = Float.intBitsToFloat(param3); + blue = Float.intBitsToFloat(param4); + operations.add(new ColorExpression(id, (byte) ARGB_MODE, alpha, red, green, blue)); + break; + case HSV_MODE: + alpha = (param1 >> 16) / 1024.0f; + float hue = Float.intBitsToFloat(param2); + float sat = Float.intBitsToFloat(param3); + float value = Float.intBitsToFloat(param4); + operations.add(new ColorExpression(id, HSV_MODE, (param1 >> 16), hue, sat, value)); + break; + case COLOR_ID_INTERPOLATE: + case ID_COLOR_INTERPOLATE: + case ID_ID_INTERPOLATE: + case COLOR_COLOR_INTERPOLATE: + operations.add( + new ColorExpression( + id, mode, param2, param3, Float.intBitsToFloat(param4))); + break; + default: + throw new RuntimeException("Invalid mode " + mode); + } } /** diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase2.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase2.java index 7f1ba6f94065..411353bd3509 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase2.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase2.java @@ -31,8 +31,8 @@ import java.util.List; /** Base class for commands that take 3 float */ public abstract class DrawBase2 extends PaintOperation implements VariableSupport { @NonNull protected String mName = "DrawRectBase"; - protected float mV1; - protected float mV2; + float mV1; + float mV2; float mValue1; float mValue2; diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBitmapFontText.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBitmapFontText.java new file mode 100644 index 000000000000..258988e8b00a --- /dev/null +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBitmapFontText.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.widget.remotecompose.core.operations; + +import static com.android.internal.widget.remotecompose.core.operations.Utils.floatToString; + +import android.annotation.NonNull; + +import com.android.internal.widget.remotecompose.core.Operation; +import com.android.internal.widget.remotecompose.core.Operations; +import com.android.internal.widget.remotecompose.core.PaintContext; +import com.android.internal.widget.remotecompose.core.PaintOperation; +import com.android.internal.widget.remotecompose.core.RemoteContext; +import com.android.internal.widget.remotecompose.core.VariableSupport; +import com.android.internal.widget.remotecompose.core.WireBuffer; +import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; +import com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation; + +import java.util.List; + +/** Draw Text */ +public class DrawBitmapFontText extends PaintOperation implements VariableSupport { + private static final int OP_CODE = Operations.DRAW_BITMAP_FONT_TEXT_RUN; + private static final String CLASS_NAME = "DrawBitmapFontText"; + int mTextID; + int mBitmapFontID; + int mStart; + int mEnd; + float mX; + float mY; + float mOutX; + float mOutY; + + public DrawBitmapFontText(int textID, int bitmapFontID, int start, int end, float x, float y) { + mTextID = textID; + mBitmapFontID = bitmapFontID; + mStart = start; + mEnd = end; + mOutX = mX = x; + mOutY = mY = y; + } + + @Override + public void updateVariables(@NonNull RemoteContext context) { + mOutX = Float.isNaN(mX) ? context.getFloat(Utils.idFromNan(mX)) : mX; + mOutY = Float.isNaN(mY) ? context.getFloat(Utils.idFromNan(mY)) : mY; + } + + @Override + public void registerListening(@NonNull RemoteContext context) { + if (Float.isNaN(mX)) { + context.listensTo(Utils.idFromNan(mX), this); + } + if (Float.isNaN(mY)) { + context.listensTo(Utils.idFromNan(mY), this); + } + } + + @Override + public void write(@NonNull WireBuffer buffer) { + apply(buffer, mTextID, mBitmapFontID, mStart, mEnd, mX, mY); + } + + @NonNull + @Override + public String toString() { + return "DrawBitmapFontText [" + + mTextID + + "] " + + mBitmapFontID + + ", " + + mStart + + ", " + + mEnd + + ", " + + floatToString(mX, mOutX) + + ", " + + floatToString(mY, mOutY); + } + + /** + * Read this operation and add it to the list of operations + * + * @param buffer the buffer to read + * @param operations the list of operations that will be added to + */ + public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) { + int text = buffer.readInt(); + int bitmapFont = buffer.readInt(); + int start = buffer.readInt(); + int end = buffer.readInt(); + float x = buffer.readFloat(); + float y = buffer.readFloat(); + DrawBitmapFontText op = new DrawBitmapFontText(text, bitmapFont, start, end, x, y); + + operations.add(op); + } + + /** + * The name of the class + * + * @return the name + */ + @NonNull + public static String name() { + return CLASS_NAME; + } + + /** + * The OP_CODE for this command + * + * @return the opcode + */ + public static int id() { + return OP_CODE; + } + + /** + * Writes out the operation to the buffer + * + * @param buffer write the command to the buffer + * @param textID id of the text + * @param bitmapFontID id of the bitmap font + * @param start Start position + * @param end end position + * @param x position of where to draw + * @param y position of where to draw + */ + public static void apply( + @NonNull WireBuffer buffer, + int textID, + int bitmapFontID, + int start, + int end, + float x, + float y) { + buffer.start(Operations.DRAW_BITMAP_FONT_TEXT_RUN); + buffer.writeInt(textID); + buffer.writeInt(bitmapFontID); + buffer.writeInt(start); + buffer.writeInt(end); + buffer.writeFloat(x); + buffer.writeFloat(y); + } + + /** + * Populate the documentation with a description of this operation + * + * @param doc to append the description to. + */ + public static void documentation(@NonNull DocumentationBuilder doc) { + doc.operation("Draw Operations", id(), CLASS_NAME) + .description("Draw a run of bitmap font text, all in a single direction") + .field(DocumentedOperation.INT, "textId", "id of bitmap") + .field(DocumentedOperation.INT, "bitmapFontId", "id of the bitmap font") + .field( + DocumentedOperation.INT, + "start", + "The start of the text to render. -1=end of string") + .field(DocumentedOperation.INT, "end", "The end of the text to render") + .field( + DocumentedOperation.INT, + "contextStart", + "the index of the start of the shaping context") + .field( + DocumentedOperation.INT, + "contextEnd", + "the index of the end of the shaping context") + .field(DocumentedOperation.FLOAT, "x", "The x position at which to draw the text") + .field(DocumentedOperation.FLOAT, "y", "The y position at which to draw the text") + .field(DocumentedOperation.BOOLEAN, "RTL", "Whether the run is in RTL direction"); + } + + @Override + public void paint(@NonNull PaintContext context) { + RemoteContext remoteContext = context.getContext(); + String textToPaint = remoteContext.getText(mTextID); + if (textToPaint == null) { + return; + } + if (mEnd == -1) { + if (mStart != 0) { + textToPaint = textToPaint.substring(mStart); + } + } else if (mEnd > textToPaint.length()) { + textToPaint = textToPaint.substring(mStart); + } else { + textToPaint = textToPaint.substring(mStart, mEnd); + } + + BitmapFontData bitmapFont = (BitmapFontData) remoteContext.getObject(mBitmapFontID); + if (bitmapFont == null) { + return; + } + + float xPos = mX; + int pos = 0; + while (pos < textToPaint.length()) { + BitmapFontData.Glyph glyph = bitmapFont.lookupGlyph(textToPaint, pos); + if (glyph == null) { + pos++; + continue; + } + + pos += glyph.mChars.length(); + if (glyph.mBitmapId == -1) { + // Space is represented by a glyph of -1. + xPos += glyph.mMarginLeft + glyph.mMarginRight; + continue; + } + + xPos += glyph.mMarginLeft; + float xPos2 = xPos + glyph.mBitmapWidth; + context.drawBitmap( + glyph.mBitmapId, xPos, mY + glyph.mMarginTop, xPos2, mY + glyph.mBitmapHeight); + xPos = xPos2 + glyph.mMarginRight; + } + } +} diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/FloatFunctionCall.java b/core/java/com/android/internal/widget/remotecompose/core/operations/FloatFunctionCall.java new file mode 100644 index 000000000000..eccc00a18308 --- /dev/null +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/FloatFunctionCall.java @@ -0,0 +1,185 @@ +/* + * 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.internal.widget.remotecompose.core.operations; + +import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.FLOAT_ARRAY; +import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import com.android.internal.widget.remotecompose.core.Operation; +import com.android.internal.widget.remotecompose.core.Operations; +import com.android.internal.widget.remotecompose.core.PaintContext; +import com.android.internal.widget.remotecompose.core.PaintOperation; +import com.android.internal.widget.remotecompose.core.RemoteContext; +import com.android.internal.widget.remotecompose.core.VariableSupport; +import com.android.internal.widget.remotecompose.core.WireBuffer; +import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; +import com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation; +import com.android.internal.widget.remotecompose.core.operations.utilities.AnimatedFloatExpression; +import com.android.internal.widget.remotecompose.core.operations.utilities.NanMap; + +import java.util.ArrayList; +import java.util.List; + +/** This provides the command to call a floatfunction defined in floatfunction */ +public class FloatFunctionCall extends PaintOperation implements VariableSupport { + private static final int OP_CODE = Operations.FUNCTION_CALL; + private static final String CLASS_NAME = "FunctionCall"; + private final int mId; + private final float[] mArgs; + private final float[] mOutArgs; + + FloatFunctionDefine mFunction; + + @NonNull private ArrayList<Operation> mList = new ArrayList<>(); + + @NonNull AnimatedFloatExpression mExp = new AnimatedFloatExpression(); + + /** + * Create a new FloatFunctionCall operation + * + * @param id The function to call + * @param args the arguments to call it with + */ + public FloatFunctionCall(int id, float[] args) { + mId = id; + mArgs = args; + if (args != null) { + mOutArgs = new float[args.length]; + System.arraycopy(args, 0, mOutArgs, 0, args.length); + } else { + mOutArgs = null; + } + } + + @Override + public void updateVariables(@NonNull RemoteContext context) { + if (mOutArgs != null) { + for (int i = 0; i < mArgs.length; i++) { + float v = mArgs[i]; + mOutArgs[i] = + (Float.isNaN(v) + && !AnimatedFloatExpression.isMathOperator(v) + && !NanMap.isDataVariable(v)) + ? context.getFloat(Utils.idFromNan(v)) + : v; + } + } + } + + @Override + public void registerListening(@NonNull RemoteContext context) { + mFunction = (FloatFunctionDefine) context.getObject(mId); + if (mArgs != null) { + for (int i = 0; i < mArgs.length; i++) { + float v = mArgs[i]; + if (Float.isNaN(v) + && !AnimatedFloatExpression.isMathOperator(v) + && !NanMap.isDataVariable(v)) { + context.listensTo(Utils.idFromNan(v), this); + } + } + } + } + + @Override + public void write(@NonNull WireBuffer buffer) { + apply(buffer, mId, mArgs); + } + + @NonNull + @Override + public String toString() { + String str = "callFunction[" + Utils.idString(mId) + "] "; + for (int i = 0; i < mArgs.length; i++) { + str += ((i == 0) ? "" : " ,") + Utils.floatToString(mArgs[i], mOutArgs[i]); + } + return str; + } + + /** + * Write the operation on the buffer + * + * @param buffer the buffer to write to + * @param id the id of the function to call + * @param args the arguments to call the function with + */ + public static void apply(@NonNull WireBuffer buffer, int id, @Nullable float[] args) { + buffer.start(OP_CODE); + buffer.writeInt(id); + if (args != null) { + buffer.writeInt(args.length); + for (int i = 0; i < args.length; i++) { + buffer.writeFloat(args[i]); + } + } else { + buffer.writeInt(0); + } + } + + /** + * Read this operation and add it to the list of operations + * + * @param buffer the buffer to read + * @param operations the list of operations that will be added to + */ + public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) { + int id = buffer.readInt(); + int argLen = buffer.readInt(); + float[] args = null; + if (argLen > 0) { + args = new float[argLen]; + for (int i = 0; i < argLen; i++) { + args[i] = buffer.readFloat(); + } + } + + FloatFunctionCall data = new FloatFunctionCall(id, args); + operations.add(data); + } + + /** + * Populate the documentation with a description of this operation + * + * @param doc to append the description to. + */ + public static void documentation(@NonNull DocumentationBuilder doc) { + doc.operation("Data Operations", OP_CODE, CLASS_NAME) + .description("Command to call the function") + .field(DocumentedOperation.INT, "id", "id of function to call") + .field(INT, "argLen", "the number of Arguments") + .field(FLOAT_ARRAY, "values", "argLen", "array of float arguments"); + } + + @NonNull + @Override + public String deepToString(@NonNull String indent) { + return indent + toString(); + } + + @Override + public void paint(@NonNull PaintContext context) { + RemoteContext remoteContext = context.getContext(); + int[] args = mFunction.getArgs(); + for (int j = 0; j < mOutArgs.length; j++) { + remoteContext.loadFloat(args[j], mOutArgs[j]); + updateVariables(remoteContext); + } + mFunction.execute(remoteContext); + } +} diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/FloatFunctionDefine.java b/core/java/com/android/internal/widget/remotecompose/core/operations/FloatFunctionDefine.java new file mode 100644 index 000000000000..efd4eecb807e --- /dev/null +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/FloatFunctionDefine.java @@ -0,0 +1,168 @@ +/* + * 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.internal.widget.remotecompose.core.operations; + +import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.FLOAT_ARRAY; +import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT; + +import android.annotation.NonNull; + +import com.android.internal.widget.remotecompose.core.Operation; +import com.android.internal.widget.remotecompose.core.Operations; +import com.android.internal.widget.remotecompose.core.RemoteContext; +import com.android.internal.widget.remotecompose.core.VariableSupport; +import com.android.internal.widget.remotecompose.core.WireBuffer; +import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; +import com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation; +import com.android.internal.widget.remotecompose.core.operations.layout.Container; +import com.android.internal.widget.remotecompose.core.operations.utilities.AnimatedFloatExpression; + +import java.util.ArrayList; +import java.util.List; + +/** + * This defines a function Operator. It contains a collection of commands which are then executed by + * the FloatFunctionCall command + */ +public class FloatFunctionDefine extends Operation implements VariableSupport, Container { + private static final int OP_CODE = Operations.FUNCTION_DEFINE; + private static final String CLASS_NAME = "FunctionDefine"; + private final int mId; + private final int[] mFloatVarId; + @NonNull private ArrayList<Operation> mList = new ArrayList<>(); + + @NonNull AnimatedFloatExpression mExp = new AnimatedFloatExpression(); + + /** + * @param id The id of the function + * @param floatVarId the ids of the variables + */ + public FloatFunctionDefine(int id, int[] floatVarId) { + mId = id; + mFloatVarId = floatVarId; + } + + @NonNull + @Override + public ArrayList<Operation> getList() { + return mList; + } + + @Override + public void updateVariables(@NonNull RemoteContext context) {} + + @Override + public void registerListening(@NonNull RemoteContext context) { + context.putObject(mId, this); + } + + @Override + public void write(@NonNull WireBuffer buffer) { + apply(buffer, mId, mFloatVarId); + } + + @NonNull + @Override + public String toString() { + String str = "FloatFunctionDefine[" + Utils.idString(mId) + "] ("; + for (int j = 0; j < mFloatVarId.length; j++) { + str += "[" + mFloatVarId[j] + "] "; + } + str += ")"; + for (Operation operation : mList) { + str += " \n " + operation.toString(); + } + return str; + } + + /** + * Write the operation on the buffer + * + * @param buffer the buffer to write to + * @param id the id of the function + * @param varId the ids of the variables + */ + public static void apply(@NonNull WireBuffer buffer, int id, @NonNull int[] varId) { + buffer.start(OP_CODE); + buffer.writeInt(id); + buffer.writeInt(varId.length); + for (int i = 0; i < varId.length; i++) { + buffer.writeInt(varId[i]); + } + } + + /** + * Read this operation and add it to the list of operations + * + * @param buffer the buffer to read + * @param operations the list of operations that will be added to + */ + public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) { + int id = buffer.readInt(); + int varLen = buffer.readInt(); + int[] varId = new int[varLen]; + for (int i = 0; i < varId.length; i++) { + varId[i] = buffer.readInt(); + } + FloatFunctionDefine data = new FloatFunctionDefine(id, varId); + operations.add(data); + } + + /** + * Populate the documentation with a description of this operation + * + * @param doc to append the description to. + */ + public static void documentation(@NonNull DocumentationBuilder doc) { + doc.operation("Data Operations", OP_CODE, CLASS_NAME) + .description("Define a function") + .field(DocumentedOperation.INT, "id", "The reference of the function") + .field(INT, "varLen", "number of arguments to the function") + .field(FLOAT_ARRAY, "id", "varLen", "id equations"); + } + + @NonNull + @Override + public String deepToString(@NonNull String indent) { + return indent + toString(); + } + + /** + * @return the array of id's + */ + public int[] getArgs() { + return mFloatVarId; + } + + @Override + public void apply(@NonNull RemoteContext context) {} + + /** + * Execute the function by applying the list of operations + * + * @param context the current RemoteContext + */ + public void execute(@NonNull RemoteContext context) { + for (Operation op : mList) { + if (op instanceof VariableSupport) { + ((VariableSupport) op).updateVariables(context); + } + + context.incrementOpCount(); + op.apply(context); + } + } +} diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/ParticlesCreate.java b/core/java/com/android/internal/widget/remotecompose/core/operations/ParticlesCreate.java index 9e891c48c065..ee9e7a4045cb 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/ParticlesCreate.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/ParticlesCreate.java @@ -39,7 +39,7 @@ import java.util.List; * for constructing the particles */ public class ParticlesCreate extends Operation implements VariableSupport { - private static final int OP_CODE = Operations.PARTICLE_CREATE; + private static final int OP_CODE = Operations.PARTICLE_DEFINE; private static final String CLASS_NAME = "ParticlesCreate"; private final int mId; private final float[][] mEquations; diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/ParticlesLoop.java b/core/java/com/android/internal/widget/remotecompose/core/operations/ParticlesLoop.java index 791079070622..8d19c94df604 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/ParticlesLoop.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/ParticlesLoop.java @@ -38,7 +38,7 @@ import java.util.ArrayList; import java.util.List; /** - * This provides the mechinism to evolve the particles It consist of a restart equation and a list + * This provides the mechanism to evolve the particles It consist of a restart equation and a list * of equations particle restarts if restart equation > 0 */ public class ParticlesLoop extends PaintOperation implements VariableSupport, Container { @@ -159,10 +159,10 @@ public class ParticlesLoop extends PaintOperation implements VariableSupport, Co /** * Write the operation on the buffer * - * @param buffer - * @param id - * @param restart - * @param equations + * @param buffer the buffer to write to + * @param id the id of the particle system + * @param restart the restart equation + * @param equations the equations to evolve the particles */ public static void apply( @NonNull WireBuffer buffer, diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/Utils.java b/core/java/com/android/internal/widget/remotecompose/core/operations/Utils.java index bd68d5a8c180..5f505409e254 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/Utils.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/Utils.java @@ -289,4 +289,21 @@ public class Utils { } return 0; } + + /** + * Convert float alpha, red,g reen, blue to ARGB int + * + * @param alpha alpha value + * @param red red value + * @param green green value + * @param blue blue value + * @return ARGB int + */ + public static int toARGB(float alpha, float red, float green, float blue) { + int a = (int) (alpha * 255.0f + 0.5f); + int r = (int) (red * 255.0f + 0.5f); + int g = (int) (green * 255.0f + 0.5f); + int b = (int) (blue * 255.0f + 0.5f); + return (a << 24 | r << 16 | g << 8 | b); + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java index 8e733ce1d808..96a31aec7dc4 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java @@ -37,13 +37,15 @@ import com.android.internal.widget.remotecompose.core.operations.layout.measure. import com.android.internal.widget.remotecompose.core.operations.layout.measure.MeasurePass; import com.android.internal.widget.remotecompose.core.operations.paint.PaintBundle; import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; import java.util.ArrayList; import java.util.HashSet; /** Generic Component class */ public class Component extends PaintOperation - implements Container, Measurable, SerializableToString { + implements Container, Measurable, SerializableToString, Serializable { private static final boolean DEBUG = false; @@ -61,7 +63,7 @@ public class Component extends PaintOperation public boolean mNeedsMeasure = true; public boolean mNeedsRepaint = false; @Nullable public AnimateMeasure mAnimateMeasure; - @NonNull public AnimationSpec mAnimationSpec = new AnimationSpec(); + @NonNull public AnimationSpec mAnimationSpec = AnimationSpec.DEFAULT; public boolean mFirstLayout = true; @NonNull PaintBundle mPaint = new PaintBundle(); @NonNull protected HashSet<ComponentValue> mComponentValues = new HashSet<>(); @@ -318,6 +320,14 @@ public class Component extends PaintOperation } } + protected AnimationSpec getAnimationSpec() { + return mAnimationSpec; + } + + protected void setAnimationSpec(@NonNull AnimationSpec animationSpec) { + mAnimationSpec = animationSpec; + } + public enum Visibility { GONE, VISIBLE, @@ -501,16 +511,17 @@ public class Component extends PaintOperation * * @param context * @param document - * @param x - * @param y + * @param x x location on screen or -1 if unconditional click + * @param y y location on screen or -1 if unconditional click */ public void onClick( @NonNull RemoteContext context, @NonNull CoreDocument document, float x, float y) { - if (!contains(x, y)) { + boolean isUnconditional = x == -1 & y == -1; + if (!isUnconditional && !contains(x, y)) { return; } - float cx = x - getScrollX(); - float cy = y - getScrollY(); + float cx = isUnconditional ? -1 : x - getScrollX(); + float cy = isUnconditional ? -1 : y - getScrollY(); for (Operation op : mList) { if (op instanceof Component) { ((Component) op).onClick(context, document, cx, cy); @@ -1035,4 +1046,15 @@ public class Component extends PaintOperation } return null; } + + @Override + public void serialize(MapSerializer serializer) { + serializer.add("type", getSerializedName()); + serializer.add("id", mComponentId); + serializer.add("x", mX); + serializer.add("y", mY); + serializer.add("width", mWidth); + serializer.add("height", mHeight); + serializer.add("visibility", mVisibility); + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java index dcd334822010..c517e50f35d3 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java @@ -32,6 +32,7 @@ import com.android.internal.widget.remotecompose.core.operations.MatrixTranslate import com.android.internal.widget.remotecompose.core.operations.PaintData; import com.android.internal.widget.remotecompose.core.operations.TextData; import com.android.internal.widget.remotecompose.core.operations.TouchExpression; +import com.android.internal.widget.remotecompose.core.operations.layout.animation.AnimationSpec; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ComponentModifiers; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ComponentVisibilityOperation; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.DimensionModifierOperation; @@ -44,6 +45,7 @@ import com.android.internal.widget.remotecompose.core.operations.layout.modifier import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.WidthInModifierOperation; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.WidthModifierOperation; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ZIndexModifierOperation; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; import java.util.ArrayList; @@ -234,6 +236,8 @@ public class LayoutComponent extends Component { mZIndexModifier = (ZIndexModifierOperation) op; } else if (op instanceof GraphicsLayerModifierOperation) { mGraphicsLayerModifier = (GraphicsLayerModifierOperation) op; + } else if (op instanceof AnimationSpec) { + mAnimationSpec = (AnimationSpec) op; } else if (op instanceof ScrollDelegate) { ScrollDelegate scrollDelegate = (ScrollDelegate) op; if (scrollDelegate.handlesHorizontalScroll()) { @@ -256,6 +260,16 @@ public class LayoutComponent extends Component { if (heightInConstraints != null) { mHeightModifier.setHeightIn(heightInConstraints); } + + if (mAnimationSpec != AnimationSpec.DEFAULT) { + for (int i = 0; i < mChildrenComponents.size(); i++) { + Component c = mChildrenComponents.get(i); + if (c != null && c.getAnimationSpec() == AnimationSpec.DEFAULT) { + c.setAnimationSpec(mAnimationSpec); + } + } + } + setWidth(computeModifierDefinedWidth(null)); setHeight(computeModifierDefinedHeight(null)); } @@ -473,4 +487,14 @@ public class LayoutComponent extends Component { public ArrayList<Component> getChildrenComponents() { return mChildrenComponents; } + + @Override + public void serialize(MapSerializer serializer) { + super.serialize(serializer); + serializer.add("children", mChildrenComponents); + serializer.add("paddingLeft", mPaddingLeft); + serializer.add("paddingRight", mPaddingRight); + serializer.add("paddingTop", mPaddingTop); + serializer.add("paddingBottom", mPaddingBottom); + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/TouchCancelModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/TouchCancelModifierOperation.java index 4977a15e2dc1..a4e8f5c5f18e 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/TouchCancelModifierOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/TouchCancelModifierOperation.java @@ -94,6 +94,11 @@ public class TouchCancelModifierOperation extends ListActionsOperation implement return "TouchCancelModifier"; } + /** + * Write the operation on the buffer + * + * @param buffer a WireBuffer + */ public static void apply(WireBuffer buffer) { buffer.start(OP_CODE); } @@ -108,6 +113,11 @@ public class TouchCancelModifierOperation extends ListActionsOperation implement operations.add(new TouchCancelModifierOperation()); } + /** + * Add documentation for this operation + * + * @param doc a DocumentationBuilder + */ public static void documentation(DocumentationBuilder doc) { doc.operation("Modifier Operations", OP_CODE, name()) .description( diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/TouchDownModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/TouchDownModifierOperation.java index 8c51f2eac383..6191bf4e4ad2 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/TouchDownModifierOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/TouchDownModifierOperation.java @@ -96,14 +96,30 @@ public class TouchDownModifierOperation extends ListActionsOperation implements return "TouchModifier"; } + /** + * Write the operation to the buffer + * + * @param buffer a WireBuffer + */ public static void apply(WireBuffer buffer) { buffer.start(OP_CODE); } + /** + * Read the operation from the buffer + * + * @param buffer a WireBuffer + * @param operations the list of operations we read so far + */ public static void read(WireBuffer buffer, List<Operation> operations) { operations.add(new TouchDownModifierOperation()); } + /** + * Add documentation for this operation + * + * @param doc a DocumentationBuilder + */ public static void documentation(DocumentationBuilder doc) { doc.operation("Modifier Operations", OP_CODE, name()) .description( diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/TouchUpModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/TouchUpModifierOperation.java index a12c356f7c48..a7e423e67940 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/TouchUpModifierOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/TouchUpModifierOperation.java @@ -94,14 +94,30 @@ public class TouchUpModifierOperation extends ListActionsOperation implements To return "TouchUpModifier"; } + /** + * Write the operation to the buffer + * + * @param buffer a WireBuffer + */ public static void apply(WireBuffer buffer) { buffer.start(OP_CODE); } + /** + * Read the operation from the buffer + * + * @param buffer a WireBuffer + * @param operations the list of operations we read so far + */ public static void read(WireBuffer buffer, List<Operation> operations) { operations.add(new TouchUpModifierOperation()); } + /** + * Add documentation for this operation + * + * @param doc a DocumentationBuilder + */ public static void documentation(DocumentationBuilder doc) { doc.operation("Modifier Operations", OP_CODE, name()) .description( diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/AnimateMeasure.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/AnimateMeasure.java index d3b3e0e775f2..e5cd485967e8 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/AnimateMeasure.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/AnimateMeasure.java @@ -38,8 +38,8 @@ public class AnimateMeasure { private final @NonNull Component mComponent; private final @NonNull ComponentMeasure mOriginal; private final @NonNull ComponentMeasure mTarget; - private int mDuration; - private int mDurationVisibilityChange = mDuration; + private float mDuration; + private float mDurationVisibilityChange = mDuration; private @NonNull AnimationSpec.ANIMATION mEnterAnimation = AnimationSpec.ANIMATION.FADE_IN; private @NonNull AnimationSpec.ANIMATION mExitAnimation = AnimationSpec.ANIMATION.FADE_OUT; private int mMotionEasingType = GeneralEasing.CUBIC_STANDARD; @@ -64,8 +64,8 @@ public class AnimateMeasure { @NonNull Component component, @NonNull ComponentMeasure original, @NonNull ComponentMeasure target, - int duration, - int durationVisibilityChange, + float duration, + float durationVisibilityChange, @NonNull AnimationSpec.ANIMATION enterAnimation, @NonNull AnimationSpec.ANIMATION exitAnimation, int motionEasingType, @@ -94,6 +94,11 @@ public class AnimateMeasure { component.mVisibility = target.getVisibility(); } + /** + * Update the current bounds/visibility/etc given the current time + * + * @param currentTime the time we use to evaluate the animation + */ public void update(long currentTime) { long elapsed = currentTime - mStartTime; float motionProgress = elapsed / (float) mDuration; @@ -347,6 +352,11 @@ public class AnimateMeasure { return mOriginal.getH() * (1 - mP) + mTarget.getH() * mP; } + /** + * Returns the visibility for this measure + * + * @return the current visibility (possibly interpolated) + */ public float getVisibility() { if (mOriginal.getVisibility() == mTarget.getVisibility()) { return 1f; @@ -357,6 +367,12 @@ public class AnimateMeasure { } } + /** + * Set the target values from the given measure + * + * @param measure the target measure + * @param currentTime the current time + */ public void updateTarget(@NonNull ComponentMeasure measure, long currentTime) { mOriginal.setX(getX()); mOriginal.setY(getY()); diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/AnimationSpec.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/AnimationSpec.java index 6dff4a87088b..6e9de58e354a 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/AnimationSpec.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/AnimationSpec.java @@ -24,25 +24,28 @@ import com.android.internal.widget.remotecompose.core.Operations; import com.android.internal.widget.remotecompose.core.RemoteContext; import com.android.internal.widget.remotecompose.core.WireBuffer; import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; +import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ModifierOperation; +import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer; import com.android.internal.widget.remotecompose.core.operations.utilities.easing.GeneralEasing; import java.util.List; /** Basic component animation spec */ -public class AnimationSpec extends Operation { +public class AnimationSpec extends Operation implements ModifierOperation { + public static final AnimationSpec DEFAULT = new AnimationSpec(); int mAnimationId = -1; - int mMotionDuration = 300; + float mMotionDuration = 300; int mMotionEasingType = GeneralEasing.CUBIC_STANDARD; - int mVisibilityDuration = 300; + float mVisibilityDuration = 300; int mVisibilityEasingType = GeneralEasing.CUBIC_STANDARD; @NonNull ANIMATION mEnterAnimation = ANIMATION.FADE_IN; @NonNull ANIMATION mExitAnimation = ANIMATION.FADE_OUT; public AnimationSpec( int animationId, - int motionDuration, + float motionDuration, int motionEasingType, - int visibilityDuration, + float visibilityDuration, int visibilityEasingType, @NonNull ANIMATION enterAnimation, @NonNull ANIMATION exitAnimation) { @@ -70,7 +73,7 @@ public class AnimationSpec extends Operation { return mAnimationId; } - public int getMotionDuration() { + public float getMotionDuration() { return mMotionDuration; } @@ -78,7 +81,7 @@ public class AnimationSpec extends Operation { return mMotionEasingType; } - public int getVisibilityDuration() { + public float getVisibilityDuration() { return mVisibilityDuration; } @@ -102,6 +105,25 @@ public class AnimationSpec extends Operation { return "ANIMATION_SPEC (" + mMotionDuration + " ms)"; } + @Override + public void serializeToString(int indent, @NonNull StringSerializer serializer) { + serializer.append( + indent, + "ANIMATION_SPEC = [" + + getMotionDuration() + + ", " + + getMotionEasingType() + + ", " + + getVisibilityDuration() + + ", " + + getVisibilityEasingType() + + ", " + + getEnterAnimation() + + ", " + + getExitAnimation() + + "]"); + } + public enum ANIMATION { FADE_IN, FADE_OUT, @@ -156,10 +178,22 @@ public class AnimationSpec extends Operation { return Operations.ANIMATION_SPEC; } + /** + * Returns an int for the given ANIMATION + * + * @param animation an ANIMATION enum value + * @return a corresponding int value + */ public static int animationToInt(@NonNull ANIMATION animation) { return animation.ordinal(); } + /** + * Maps int value to the corresponding ANIMATION enum values + * + * @param value int value mapped to the enum + * @return the corresponding ANIMATION enum value + */ @NonNull public static ANIMATION intToAnimation(int value) { switch (value) { @@ -184,20 +218,32 @@ public class AnimationSpec extends Operation { } } + /** + * Write the operation to the buffer + * + * @param buffer a WireBuffer + * @param animationId the animation id + * @param motionDuration the duration of the motion animation + * @param motionEasingType the type of easing for the motion animation + * @param visibilityDuration the duration of the visibility animation + * @param visibilityEasingType the type of easing for the visibility animation + * @param enterAnimation the type of animation when "entering" (newly visible) + * @param exitAnimation the type of animation when "exiting" (newly gone) + */ public static void apply( @NonNull WireBuffer buffer, int animationId, - int motionDuration, + float motionDuration, int motionEasingType, - int visibilityDuration, + float visibilityDuration, int visibilityEasingType, @NonNull ANIMATION enterAnimation, @NonNull ANIMATION exitAnimation) { buffer.start(Operations.ANIMATION_SPEC); buffer.writeInt(animationId); - buffer.writeInt(motionDuration); + buffer.writeFloat(motionDuration); buffer.writeInt(motionEasingType); - buffer.writeInt(visibilityDuration); + buffer.writeFloat(visibilityDuration); buffer.writeInt(visibilityEasingType); buffer.writeInt(animationToInt(enterAnimation)); buffer.writeInt(animationToInt(exitAnimation)); @@ -211,9 +257,9 @@ public class AnimationSpec extends Operation { */ public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) { int animationId = buffer.readInt(); - int motionDuration = buffer.readInt(); + float motionDuration = buffer.readFloat(); int motionEasingType = buffer.readInt(); - int visibilityDuration = buffer.readInt(); + float visibilityDuration = buffer.readFloat(); int visibilityEasingType = buffer.readInt(); ANIMATION enterAnimation = intToAnimation(buffer.readInt()); ANIMATION exitAnimation = intToAnimation(buffer.readInt()); diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/ParticleAnimation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/ParticleAnimation.java index 64e2f004cb65..051579b02cee 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/ParticleAnimation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/animation/ParticleAnimation.java @@ -30,6 +30,15 @@ public class ParticleAnimation { @NonNull PaintBundle mPaint = new PaintBundle(); + /** + * Animate the particle animation + * + * @param context the current paint context + * @param component the target component + * @param start the component's measure at the end of the animation + * @param end the component's measure at the end of the animation + * @param progress the current animation progress + */ public void animate( @NonNull PaintContext context, @NonNull Component component, diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/BoxLayout.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/BoxLayout.java index a37f35f0c8d8..35d639e65385 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/BoxLayout.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/BoxLayout.java @@ -29,6 +29,7 @@ import com.android.internal.widget.remotecompose.core.operations.layout.Componen import com.android.internal.widget.remotecompose.core.operations.layout.measure.ComponentMeasure; import com.android.internal.widget.remotecompose.core.operations.layout.measure.MeasurePass; import com.android.internal.widget.remotecompose.core.operations.layout.measure.Size; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; import java.util.List; @@ -191,6 +192,15 @@ public class BoxLayout extends LayoutManager { return Operations.LAYOUT_BOX; } + /** + * Write the operation to the buffer + * + * @param buffer a WireBuffer + * @param componentId the component id + * @param animationId the component animation id + * @param horizontalPositioning the horizontal positioning rules + * @param verticalPositioning the vertical positioning rules + */ public static void apply( @NonNull WireBuffer buffer, int componentId, @@ -260,4 +270,28 @@ public class BoxLayout extends LayoutManager { public void write(@NonNull WireBuffer buffer) { apply(buffer, mComponentId, mAnimationId, mHorizontalPositioning, mVerticalPositioning); } + + @Override + public void serialize(MapSerializer serializer) { + super.serialize(serializer); + serializer.add("verticalPositioning", getPositioningString(mVerticalPositioning)); + serializer.add("horizontalPositioning", getPositioningString(mHorizontalPositioning)); + } + + private String getPositioningString(int pos) { + switch (pos) { + case START: + return "START"; + case CENTER: + return "CENTER"; + case END: + return "END"; + case TOP: + return "TOP"; + case BOTTOM: + return "BOTTOM"; + default: + return "NONE"; + } + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/CanvasLayout.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/CanvasLayout.java index 0091a47eebfb..8448132cbcc1 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/CanvasLayout.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/CanvasLayout.java @@ -28,6 +28,7 @@ import com.android.internal.widget.remotecompose.core.documentation.Documentatio import com.android.internal.widget.remotecompose.core.operations.layout.Component; import com.android.internal.widget.remotecompose.core.operations.layout.measure.ComponentMeasure; import com.android.internal.widget.remotecompose.core.operations.layout.measure.MeasurePass; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; import java.util.List; @@ -91,6 +92,13 @@ public class CanvasLayout extends BoxLayout { return Operations.LAYOUT_CANVAS; } + /** + * Write the operation to the buffer + * + * @param buffer a WireBuffer + * @param componentId the component id + * @param animationId the component animation id + */ public static void apply(@NonNull WireBuffer buffer, int componentId, int animationId) { buffer.start(Operations.LAYOUT_CANVAS); buffer.writeInt(componentId); @@ -142,4 +150,27 @@ public class CanvasLayout extends BoxLayout { public void write(@NonNull WireBuffer buffer) { apply(buffer, mComponentId, mAnimationId); } + + @Override + public void serialize(MapSerializer serializer) { + super.serialize(serializer); + serializer.add("", mHorizontalPositioning); + } + + private String getPositioningString(int pos) { + switch (pos) { + case START: + return "START"; + case CENTER: + return "CENTER"; + case END: + return "END"; + case TOP: + return "TOP"; + case BOTTOM: + return "BOTTOM"; + default: + return "NONE"; + } + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/ColumnLayout.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/ColumnLayout.java index 4d0cbefb0c92..47a55b6ed82a 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/ColumnLayout.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/ColumnLayout.java @@ -34,6 +34,7 @@ import com.android.internal.widget.remotecompose.core.operations.layout.measure. import com.android.internal.widget.remotecompose.core.operations.layout.measure.Size; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.HeightInModifierOperation; import com.android.internal.widget.remotecompose.core.operations.layout.utils.DebugLog; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; import java.util.List; @@ -218,6 +219,8 @@ public class ColumnLayout extends LayoutManager { boolean checkWeights = true; while (checkWeights) { checkWeights = false; + childrenWidth = 0f; + childrenHeight = 0f; boolean hasWeights = false; float totalWeights = 0f; for (Component child : mChildrenComponents) { @@ -477,4 +480,35 @@ public class ColumnLayout extends LayoutManager { mVerticalPositioning, mSpacedBy); } + + @Override + public void serialize(MapSerializer serializer) { + super.serialize(serializer); + serializer.add("verticalPositioning", getPositioningString(mVerticalPositioning)); + serializer.add("horizontalPositioning", getPositioningString(mHorizontalPositioning)); + serializer.add("spacedBy", mSpacedBy); + } + + private String getPositioningString(int pos) { + switch (pos) { + case START: + return "START"; + case CENTER: + return "CENTER"; + case END: + return "END"; + case TOP: + return "TOP"; + case BOTTOM: + return "BOTTOM"; + case SPACE_BETWEEN: + return "SPACE_BETWEEN"; + case SPACE_EVENLY: + return "SPACE_EVENLY"; + case SPACE_AROUND: + return "SPACE_AROUND"; + default: + return "NONE"; + } + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/RowLayout.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/RowLayout.java index 5b35c4c70702..e93cbd74b0b5 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/RowLayout.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/RowLayout.java @@ -34,6 +34,7 @@ import com.android.internal.widget.remotecompose.core.operations.layout.measure. import com.android.internal.widget.remotecompose.core.operations.layout.measure.Size; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.WidthInModifierOperation; import com.android.internal.widget.remotecompose.core.operations.layout.utils.DebugLog; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; import java.util.List; @@ -218,6 +219,8 @@ public class RowLayout extends LayoutManager { while (checkWeights) { checkWeights = false; + childrenWidth = 0f; + childrenHeight = 0f; boolean hasWeights = false; float totalWeights = 0f; for (Component child : mChildrenComponents) { @@ -481,4 +484,35 @@ public class RowLayout extends LayoutManager { mVerticalPositioning, mSpacedBy); } + + @Override + public void serialize(MapSerializer serializer) { + super.serialize(serializer); + serializer.add("verticalPositioning", getPositioningString(mVerticalPositioning)); + serializer.add("horizontalPositioning", getPositioningString(mHorizontalPositioning)); + serializer.add("spacedBy", mSpacedBy); + } + + private String getPositioningString(int pos) { + switch (pos) { + case START: + return "START"; + case CENTER: + return "CENTER"; + case END: + return "END"; + case TOP: + return "TOP"; + case BOTTOM: + return "BOTTOM"; + case SPACE_BETWEEN: + return "SPACE_BETWEEN"; + case SPACE_EVENLY: + return "SPACE_EVENLY"; + case SPACE_AROUND: + return "SPACE_AROUND"; + default: + return "NONE"; + } + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/StateLayout.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/StateLayout.java index 3044797b17c9..ee16bc2f4459 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/StateLayout.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/StateLayout.java @@ -30,6 +30,7 @@ import com.android.internal.widget.remotecompose.core.operations.layout.LayoutCo import com.android.internal.widget.remotecompose.core.operations.layout.measure.ComponentMeasure; import com.android.internal.widget.remotecompose.core.operations.layout.measure.MeasurePass; import com.android.internal.widget.remotecompose.core.operations.layout.measure.Size; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; import java.util.ArrayList; import java.util.HashMap; @@ -81,6 +82,7 @@ public class StateLayout extends LayoutManager { hideLayoutsOtherThan(currentLayoutIndex); } + /** Traverse the list of children and identify animated components across states */ public void findAnimatedComponents() { for (int i = 0; i < mChildrenComponents.size(); i++) { Component cs = mChildrenComponents.get(i); @@ -105,6 +107,10 @@ public class StateLayout extends LayoutManager { collapsePaintedComponents(); } + /** + * Traverse the list of components in different states, and if they are similar pick the first + * component for painting in all states. + */ public void collapsePaintedComponents() { int numStates = mChildrenComponents.size(); for (Integer id : statePaintedComponents.keySet()) { @@ -346,6 +352,11 @@ public class StateLayout extends LayoutManager { measuredLayoutIndex = currentLayoutIndex; } + /** + * Hides all layouts that are not the one with the given id + * + * @param idx the layout id + */ public void hideLayoutsOtherThan(int idx) { int index = 0; for (Component pane : mChildrenComponents) { @@ -360,6 +371,12 @@ public class StateLayout extends LayoutManager { } } + /** + * Returns the layout with the given id + * + * @param idx the component id + * @return the LayoutManager with the given id, or the first child of StateLayout if not found + */ public @NonNull LayoutManager getLayout(int idx) { int index = 0; for (Component pane : mChildrenComponents) { @@ -485,6 +502,7 @@ public class StateLayout extends LayoutManager { } } + /** Check if we are at the end of the transition, and if so handles it. */ public void checkEndOfTransition() { LayoutManager currentLayout = getLayout(measuredLayoutIndex); LayoutManager previousLayout = getLayout(previousLayoutIndex); @@ -536,10 +554,16 @@ public class StateLayout extends LayoutManager { return "STATE_LAYOUT"; } - // companion object { - // fun documentation(doc: OrigamiDocumentation) {} - // } - + /** + * write the operation to the buffer + * + * @param buffer the current buffer + * @param componentId the component id + * @param animationId the animation id if there's one, -1 otherwise. + * @param horizontalPositioning the horizontal positioning rule + * @param verticalPositioning the vertical positioning rule + * @param indexId the current index + */ public static void apply( @NonNull WireBuffer buffer, int componentId, @@ -570,4 +594,10 @@ public class StateLayout extends LayoutManager { operations.add( new StateLayout(null, componentId, animationId, 0f, 0f, 100f, 100f, indexId)); } + + @Override + public void serialize(MapSerializer serializer) { + super.serialize(serializer); + serializer.add("indexId", mIndexId); + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/TextLayout.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/TextLayout.java index 8157ea05ec45..e8e95db8141d 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/TextLayout.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/TextLayout.java @@ -34,6 +34,7 @@ import com.android.internal.widget.remotecompose.core.operations.layout.measure. import com.android.internal.widget.remotecompose.core.operations.paint.PaintBundle; import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer; import com.android.internal.widget.remotecompose.core.semantics.AccessibleComponent; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; import java.util.List; @@ -331,6 +332,20 @@ public class TextLayout extends LayoutManager implements VariableSupport, Access return Operations.LAYOUT_TEXT; } + /** + * Write the operation in the buffer + * + * @param buffer the WireBuffer we write on + * @param componentId the component id + * @param animationId the animation id (-1 if not set) + * @param textId the text id + * @param color the text color + * @param fontSize the font size + * @param fontStyle the font style + * @param fontWeight the font weight + * @param fontFamilyId the font family id + * @param textAlign the alignment rules + */ public static void apply( @NonNull WireBuffer buffer, int componentId, @@ -418,4 +433,16 @@ public class TextLayout extends LayoutManager implements VariableSupport, Access mFontFamilyId, mTextAlign); } + + @Override + public void serialize(MapSerializer serializer) { + super.serialize(serializer); + serializer.add("textId", mTextId); + serializer.add("color", mColor); + serializer.add("fontSize", mFontSize); + serializer.add("fontStyle", mFontStyle); + serializer.add("fontWeight", mFontWeight); + serializer.add("fontFamilyId", mFontFamilyId); + serializer.add("textAlign", mTextAlign); + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/ComponentMeasure.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/ComponentMeasure.java index 82f23cdcf766..11ed9f435070 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/ComponentMeasure.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/ComponentMeasure.java @@ -92,6 +92,11 @@ public class ComponentMeasure { component.mVisibility); } + /** + * Initialize this ComponentMeasure from another ComponentMeasure instance. + * + * @param m the ComponentMeasure to copy from + */ public void copyFrom(@NonNull ComponentMeasure m) { mX = m.mX; mY = m.mY; @@ -100,6 +105,12 @@ public class ComponentMeasure { mVisibility = m.mVisibility; } + /** + * Returns true if the ComponentMeasure passed is identical to us + * + * @param m the ComponentMeasure to check + * @return true if the passed ComponentMeasure is identical to ourself + */ public boolean same(@NonNull ComponentMeasure m) { return mX == m.mX && mY == m.mY && mW == m.mW && mH == m.mH && mVisibility == m.mVisibility; } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/MeasurePass.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/MeasurePass.java index 5cfb1b43cf15..b14f2d9f8a94 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/MeasurePass.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/measure/MeasurePass.java @@ -28,10 +28,17 @@ import java.util.HashMap; public class MeasurePass { @NonNull HashMap<Integer, ComponentMeasure> mList = new HashMap<>(); + /** Clear the MeasurePass */ public void clear() { mList.clear(); } + /** + * Add a ComponentMeasure to the MeasurePass + * + * @param measure the ComponentMeasure to add + * @throws Exception + */ public void add(@NonNull ComponentMeasure measure) throws Exception { if (measure.mId == -1) { throw new Exception("Component has no id!"); @@ -39,10 +46,22 @@ public class MeasurePass { mList.put(measure.mId, measure); } + /** + * Returns true if the current MeasurePass already contains a ComponentMeasure for the given id. + * + * @param id + * @return + */ public boolean contains(int id) { return mList.containsKey(id); } + /** + * return the ComponentMeasure associated with a given component + * + * @param c the Component + * @return the associated ComponentMeasure + */ public @NonNull ComponentMeasure get(@NonNull Component c) { if (!mList.containsKey(c.getComponentId())) { ComponentMeasure measure = @@ -54,6 +73,12 @@ public class MeasurePass { return mList.get(c.getComponentId()); } + /** + * Returns the ComponentMeasure associated with the id, creating one if none exists. + * + * @param id the component id + * @return the associated ComponentMeasure + */ public @NonNull ComponentMeasure get(int id) { if (!mList.containsKey(id)) { ComponentMeasure measure = diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BackgroundModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BackgroundModifierOperation.java index b4240d0e08a7..ac23db0ed599 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BackgroundModifierOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BackgroundModifierOperation.java @@ -130,6 +130,20 @@ public class BackgroundModifierOperation extends DecoratorModifierOperation { return OP_CODE; } + /** + * Write the operation to the buffer + * + * @param buffer the WireBuffer + * @param x x coordinate of the background rect + * @param y y coordinate of the background rect + * @param width width of the background rect + * @param height height of the background rect + * @param r red component of the background color + * @param g green component of the background color + * @param b blue component of the background color + * @param a alpha component of the background color + * @param shapeType the shape of the background (RECTANGLE=0, CIRCLE=1) + */ public static void apply( @NonNull WireBuffer buffer, float x, @@ -205,6 +219,6 @@ public class BackgroundModifierOperation extends DecoratorModifierOperation { .field(FLOAT, "g", "") .field(FLOAT, "b", "") .field(FLOAT, "a", "") - .field(FLOAT, "shapeType", ""); + .field(FLOAT, "shapeType", "0 for RECTANGLE, 1 for CIRCLE"); } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BorderModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BorderModifierOperation.java index df30d9f615e5..06c21bd49f33 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BorderModifierOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BorderModifierOperation.java @@ -176,6 +176,22 @@ public class BorderModifierOperation extends DecoratorModifierOperation { return OP_CODE; } + /** + * Write the operation to the buffer + * + * @param buffer the WireBuffer + * @param x x coordinate of the border rect + * @param y y coordinate of the border rect + * @param width width of the border rect + * @param height height of the border rect + * @param borderWidth the width of the border outline + * @param roundedCorner rounded corner value in pixels + * @param r red component of the border color + * @param g green component of the border color + * @param b blue component of the border color + * @param a alpha component of the border color + * @param shapeType the shape type (0 = RECTANGLE, 1 = CIRCLE) + */ public static void apply( @NonNull WireBuffer buffer, float x, diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ClipRectModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ClipRectModifierOperation.java index b27fb9200398..ce4449355434 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ClipRectModifierOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ClipRectModifierOperation.java @@ -76,6 +76,11 @@ public class ClipRectModifierOperation extends DecoratorModifierOperation { return OP_CODE; } + /** + * Write the operation to the buffer + * + * @param buffer the WireBuffer + */ public static void apply(@NonNull WireBuffer buffer) { buffer.start(OP_CODE); } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ComponentModifiers.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ComponentModifiers.java index a1609ace2138..dd27f8b6cfe6 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ComponentModifiers.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ComponentModifiers.java @@ -21,6 +21,7 @@ import com.android.internal.widget.remotecompose.core.CoreDocument; import com.android.internal.widget.remotecompose.core.PaintContext; import com.android.internal.widget.remotecompose.core.PaintOperation; import com.android.internal.widget.remotecompose.core.RemoteContext; +import com.android.internal.widget.remotecompose.core.SerializableToString; import com.android.internal.widget.remotecompose.core.VariableSupport; import com.android.internal.widget.remotecompose.core.WireBuffer; import com.android.internal.widget.remotecompose.core.operations.MatrixRestore; @@ -36,7 +37,7 @@ import java.util.ArrayList; /** Maintain a list of modifiers */ public class ComponentModifiers extends PaintOperation - implements DecoratorComponent, ClickHandler, TouchHandler { + implements DecoratorComponent, ClickHandler, TouchHandler, SerializableToString { @NonNull ArrayList<ModifierOperation> mList = new ArrayList<>(); @NonNull @@ -68,6 +69,7 @@ public class ComponentModifiers extends PaintOperation // nothing } + @Override public void serializeToString(int indent, @NonNull StringSerializer serializer) { serializer.append(indent, "MODIFIERS"); for (ModifierOperation m : mList) { @@ -75,10 +77,20 @@ public class ComponentModifiers extends PaintOperation } } + /** + * Add a ModifierOperation + * + * @param operation a ModifierOperation + */ public void add(@NonNull ModifierOperation operation) { mList.add(operation); } + /** + * Returns the size of the modifier list + * + * @return number of modifiers + */ public int size() { return mList.size(); } @@ -133,6 +145,11 @@ public class ComponentModifiers extends PaintOperation } } + /** + * Add the operations to this ComponentModifier + * + * @param operations list of ModifierOperation + */ public void addAll(@NonNull ArrayList<ModifierOperation> operations) { mList.addAll(operations); } @@ -197,6 +214,11 @@ public class ComponentModifiers extends PaintOperation } } + /** + * Returns true if we have a horizontal scroll modifier + * + * @return true if we have a horizontal scroll modifier, false otherwise + */ public boolean hasHorizontalScroll() { for (ModifierOperation op : mList) { if (op instanceof ScrollModifierOperation) { @@ -209,6 +231,11 @@ public class ComponentModifiers extends PaintOperation return false; } + /** + * Returns true if we have a vertical scroll modifier + * + * @return true if we have a vertical scroll modifier, false otherwise + */ public boolean hasVerticalScroll() { for (ModifierOperation op : mList) { if (op instanceof ScrollModifierOperation) { @@ -221,6 +248,12 @@ public class ComponentModifiers extends PaintOperation return false; } + /** + * Set the horizontal scroll dimension (if we have a scroll modifier) + * + * @param hostDimension the host component horizontal dimension + * @param contentDimension the content horizontal dimension + */ public void setHorizontalScrollDimension(float hostDimension, float contentDimension) { for (ModifierOperation op : mList) { if (op instanceof ScrollModifierOperation) { @@ -232,6 +265,12 @@ public class ComponentModifiers extends PaintOperation } } + /** + * Set the vertical scroll dimension (if we have a scroll modifier) + * + * @param hostDimension the host component vertical dimension + * @param contentDimension the content vertical dimension + */ public void setVerticalScrollDimension(float hostDimension, float contentDimension) { for (ModifierOperation op : mList) { if (op instanceof ScrollModifierOperation) { @@ -243,6 +282,11 @@ public class ComponentModifiers extends PaintOperation } } + /** + * Returns the horizontal scroll dimension if we have a scroll modifier + * + * @return the horizontal scroll dimension, or 0 if no scroll modifier + */ public float getHorizontalScrollDimension() { for (ModifierOperation op : mList) { if (op instanceof ScrollModifierOperation) { @@ -255,6 +299,11 @@ public class ComponentModifiers extends PaintOperation return 0f; } + /** + * Returns the vertical scroll dimension if we have a scroll modifier + * + * @return the vertical scroll dimension, or 0 if no scroll modifier + */ public float getVerticalScrollDimension() { for (ModifierOperation op : mList) { if (op instanceof ScrollModifierOperation) { diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ComponentVisibilityOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ComponentVisibilityOperation.java index c377b756ff38..dd22391c43ac 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ComponentVisibilityOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ComponentVisibilityOperation.java @@ -52,6 +52,11 @@ public class ComponentVisibilityOperation extends Operation return "ComponentVisibilityOperation(" + mVisibilityId + ")"; } + /** + * Returns the serialized name for this operation + * + * @return the serialized name + */ @NonNull public String serializedName() { return "COMPONENT_VISIBILITY"; @@ -74,6 +79,12 @@ public class ComponentVisibilityOperation extends Operation @Override public void write(@NonNull WireBuffer buffer) {} + /** + * Write the operation to the buffer + * + * @param buffer a WireBuffer + * @param valueId visibility value + */ public static void apply(@NonNull WireBuffer buffer, int valueId) { buffer.start(OP_CODE); buffer.writeInt(valueId); diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DimensionInModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DimensionInModifierOperation.java new file mode 100644 index 000000000000..7c9acfe8d2e6 --- /dev/null +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DimensionInModifierOperation.java @@ -0,0 +1,109 @@ +/* + * 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.internal.widget.remotecompose.core.operations.layout.modifiers; + +import android.annotation.NonNull; + +import com.android.internal.widget.remotecompose.core.Operation; +import com.android.internal.widget.remotecompose.core.RemoteContext; +import com.android.internal.widget.remotecompose.core.VariableSupport; +import com.android.internal.widget.remotecompose.core.WireBuffer; +import com.android.internal.widget.remotecompose.core.operations.Utils; +import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer; + +/** Helper class to set the min / max dimension on a component */ +public class DimensionInModifierOperation extends Operation + implements ModifierOperation, VariableSupport { + int mOpCode = -1; + + float mV1; + float mV2; + float mValue1; + float mValue2; + + public DimensionInModifierOperation(int opcode, float min, float max) { + mOpCode = opcode; + mValue1 = min; + mValue2 = max; + if (!Float.isNaN(mValue1)) { + mV1 = mValue1; + } + if (!Float.isNaN(mValue2)) { + mV2 = mValue2; + } + } + + @Override + public void updateVariables(@NonNull RemoteContext context) { + mV1 = Float.isNaN(mValue1) ? context.getFloat(Utils.idFromNan(mValue1)) : mValue1; + mV2 = Float.isNaN(mValue2) ? context.getFloat(Utils.idFromNan(mValue2)) : mValue2; + if (mV1 != -1) { + mV1 = mV1 * context.getDensity(); + } + if (mV2 != -1) { + mV2 = mV2 * context.getDensity(); + } + } + + @Override + public void registerListening(@NonNull RemoteContext context) { + if (Float.isNaN(mValue1)) { + context.listensTo(Utils.idFromNan(mValue1), this); + } + if (Float.isNaN(mValue2)) { + context.listensTo(Utils.idFromNan(mValue2), this); + } + } + + @Override + public void write(@NonNull WireBuffer buffer) { + // nothing + } + + @Override + public void apply(@NonNull RemoteContext context) { + // nothing + } + + @NonNull + @Override + public String deepToString(@NonNull String indent) { + return indent + toString(); + } + + /** + * Returns the min value + * + * @return minimum value + */ + public float getMin() { + return mV1; + } + + /** + * Returns the max value + * + * @return maximum value + */ + public float getMax() { + return mV2; + } + + @Override + public void serializeToString(int indent, @NonNull StringSerializer serializer) { + serializer.append(indent, "WIDTH_IN = [" + getMin() + ", " + getMax() + "]"); + } +} diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DimensionModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DimensionModifierOperation.java index b11deae3d196..88449c4a9016 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DimensionModifierOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DimensionModifierOperation.java @@ -104,6 +104,11 @@ public abstract class DimensionModifierOperation extends Operation } } + /** + * Returns true if the dimension is set using a weight + * + * @return true if using weight, false otherwise + */ public boolean hasWeight() { return mType == Type.WEIGHT; } @@ -136,6 +141,11 @@ public abstract class DimensionModifierOperation extends Operation mOutValue = mValue = value; } + /** + * Returns the serialized name for this operation + * + * @return the serialized name + */ @NonNull public String serializedName() { return "DIMENSION"; diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/GraphicsLayerModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/GraphicsLayerModifierOperation.java index 15c2f46093d2..dc5918037946 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/GraphicsLayerModifierOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/GraphicsLayerModifierOperation.java @@ -222,6 +222,26 @@ public class GraphicsLayerModifierOperation extends DecoratorModifierOperation { return OP_CODE; } + /** + * Write the operation to the buffer + * + * @param buffer a WireBuffer + * @param scaleX scaleX of the layer + * @param scaleY scaleY of the layer + * @param rotationX rotationX of the layer + * @param rotationY rotationY of the layer + * @param rotationZ rotationZ of the layer + * @param shadowElevation the shadow elevation + * @param transformOriginX the X origin of the transformations + * @param transformOriginY the Y origin of the transformations + * @param alpha the alpha of the layer + * @param cameraDistance the camera distance + * @param blendMode blending mode of the layer + * @param spotShadowColorId the spot shadow color id + * @param ambientShadowColorId the ambient shadow color id + * @param colorFilterId the color filter id + * @param renderEffectId the render effect id + */ public static void apply( WireBuffer buffer, float scaleX, @@ -257,6 +277,12 @@ public class GraphicsLayerModifierOperation extends DecoratorModifierOperation { buffer.writeInt(renderEffectId); } + /** + * Read the operation from the buffer + * + * @param buffer a WireBuffer + * @param operations the list of operations read so far + */ public static void read(WireBuffer buffer, List<Operation> operations) { float scaleX = buffer.readFloat(); float scaleY = buffer.readFloat(); @@ -292,6 +318,11 @@ public class GraphicsLayerModifierOperation extends DecoratorModifierOperation { renderEffectId)); } + /** + * Populate the documentation with a description of this operation + * + * @param doc to append the description to. + */ public static void documentation(DocumentationBuilder doc) { doc.operation("Modifier Operations", OP_CODE, CLASS_NAME) .description("define the GraphicsLayer Modifier") diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HeightInModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HeightInModifierOperation.java index c19bd2f6b7c0..cc32f2699dfe 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HeightInModifierOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HeightInModifierOperation.java @@ -19,47 +19,37 @@ import android.annotation.NonNull; import com.android.internal.widget.remotecompose.core.Operation; import com.android.internal.widget.remotecompose.core.Operations; -import com.android.internal.widget.remotecompose.core.PaintContext; import com.android.internal.widget.remotecompose.core.WireBuffer; import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; import com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation; -import com.android.internal.widget.remotecompose.core.operations.DrawBase2; import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer; import java.util.List; /** Set the min / max height dimension on a component */ -public class HeightInModifierOperation extends DrawBase2 implements ModifierOperation { +public class HeightInModifierOperation extends DimensionInModifierOperation { private static final int OP_CODE = Operations.MODIFIER_HEIGHT_IN; public static final String CLASS_NAME = "HeightInModifierOperation"; - /** - * Read this operation and add it to the list of operations - * - * @param buffer the buffer to read - * @param operations the list of operations that will be added to - */ - public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) { - Maker m = HeightInModifierOperation::new; - read(m, buffer, operations); + public HeightInModifierOperation(float min, float max) { + super(OP_CODE, min, max); } - /** - * Returns the min value - * - * @return minimum value - */ - public float getMin() { - return mV1; + @Override + public void write(@NonNull WireBuffer buffer) { + apply(buffer, getMin(), getMax()); } /** - * Returns the max value + * Read this operation and add it to the list of operations * - * @return maximum value + * @param buffer the buffer to read + * @param operations the list of operations that will be added to */ - public float getMax() { - return mV2; + public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) { + float v1 = buffer.readFloat(); + float v2 = buffer.readFloat(); + operations.add(new HeightInModifierOperation(v1, v2)); } /** @@ -81,11 +71,6 @@ public class HeightInModifierOperation extends DrawBase2 implements ModifierOper return CLASS_NAME; } - @Override - protected void write(@NonNull WireBuffer buffer, float v1, float v2) { - apply(buffer, v1, v2); - } - /** * Populate the documentation with a description of this operation * @@ -98,14 +83,6 @@ public class HeightInModifierOperation extends DrawBase2 implements ModifierOper .field(DocumentedOperation.FLOAT, "max", "The maximum height, -1 if not applied"); } - public HeightInModifierOperation(float min, float max) { - super(min, max); - mName = CLASS_NAME; - } - - @Override - public void paint(@NonNull PaintContext context) {} - /** * Writes out the HeightInModifier to the buffer * @@ -114,7 +91,9 @@ public class HeightInModifierOperation extends DrawBase2 implements ModifierOper * @param y1 start y of the DrawOval */ public static void apply(@NonNull WireBuffer buffer, float x1, float y1) { - write(buffer, OP_CODE, x1, y1); + buffer.start(OP_CODE); + buffer.writeFloat(x1); + buffer.writeFloat(y1); } @Override diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HeightModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HeightModifierOperation.java index 4b50a916b9cd..154740d5536c 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HeightModifierOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HeightModifierOperation.java @@ -52,6 +52,13 @@ public class HeightModifierOperation extends DimensionModifierOperation { return OP_CODE; } + /** + * Write the operation to the buffer + * + * @param buffer a WireBuffer + * @param type the type of dimension rule (DimensionModifierOperation.Type) + * @param value the value of the dimension + */ public static void apply(@NonNull WireBuffer buffer, int type, float value) { buffer.start(OP_CODE); buffer.writeInt(type); diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HostActionOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HostActionOperation.java index 2e9d6619d011..09e2228b847a 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HostActionOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HostActionOperation.java @@ -23,6 +23,7 @@ import com.android.internal.widget.remotecompose.core.CoreDocument; import com.android.internal.widget.remotecompose.core.Operation; import com.android.internal.widget.remotecompose.core.Operations; import com.android.internal.widget.remotecompose.core.RemoteContext; +import com.android.internal.widget.remotecompose.core.SerializableToString; import com.android.internal.widget.remotecompose.core.WireBuffer; import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; import com.android.internal.widget.remotecompose.core.operations.layout.ActionOperation; @@ -32,7 +33,8 @@ import com.android.internal.widget.remotecompose.core.operations.utilities.Strin import java.util.List; /** Capture a host action information. This can be triggered on eg. a click. */ -public class HostActionOperation extends Operation implements ActionOperation { +public class HostActionOperation extends Operation + implements ActionOperation, SerializableToString { private static final int OP_CODE = Operations.HOST_ACTION; int mActionId = -1; @@ -51,6 +53,11 @@ public class HostActionOperation extends Operation implements ActionOperation { return mActionId; } + /** + * Returns the serialized name for this operation + * + * @return the serialized name + */ @NonNull public String serializedName() { return "HOST_ACTION"; @@ -83,6 +90,12 @@ public class HostActionOperation extends Operation implements ActionOperation { context.runAction(mActionId, ""); } + /** + * Write the operation to the buffer + * + * @param buffer a WireBuffer + * @param actionId the action id + */ public static void apply(@NonNull WireBuffer buffer, int actionId) { buffer.start(OP_CODE); buffer.writeInt(actionId); diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HostNamedActionOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HostNamedActionOperation.java index 49ef58e0fe53..8a8809c653f8 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HostNamedActionOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HostNamedActionOperation.java @@ -57,6 +57,11 @@ public class HostNamedActionOperation extends Operation implements ActionOperati return "HostNamedActionOperation(" + mTextId + " : " + mValueId + ")"; } + /** + * Name used during serialization + * + * @return the serialized name for this operation + */ @NonNull public String serializedName() { return "HOST_NAMED_ACTION"; @@ -105,6 +110,14 @@ public class HostNamedActionOperation extends Operation implements ActionOperati context.runNamedAction(mTextId, value); } + /** + * Write the operation to the buffer + * + * @param buffer a WireBuffer + * @param textId the text id of the action + * @param type the type of the action + * @param valueId the value id associated with the action + */ public static void apply(@NonNull WireBuffer buffer, int textId, int type, int valueId) { buffer.start(OP_CODE); buffer.writeInt(textId); diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/MarqueeModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/MarqueeModifierOperation.java index 9588e99a65b6..4ad11d267406 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/MarqueeModifierOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/MarqueeModifierOperation.java @@ -110,6 +110,12 @@ public class MarqueeModifierOperation extends DecoratorModifierOperation impleme mVelocity); } + /** + * Serialize the string + * + * @param indent padding to display + * @param serializer append the string + */ // @Override public void serializeToString(int indent, StringSerializer serializer) { serializer.append(indent, "MARQUEE = [" + mIterations + "]"); @@ -153,14 +159,35 @@ public class MarqueeModifierOperation extends DecoratorModifierOperation impleme return "MarqueeModifierOperation(" + mIterations + ")"; } + /** + * Name of the operation + * + * @return name + */ public static String name() { return CLASS_NAME; } + /** + * id of the operation + * + * @return the operation id + */ public static int id() { return OP_CODE; } + /** + * Write the operation to the buffer + * + * @param buffer a WireBuffer + * @param iterations the number of iterations + * @param animationMode animation mode + * @param repeatDelayMillis repeat delay in ms + * @param initialDelayMillis initial delay before the marquee start in ms + * @param spacing the spacing between marquee + * @param velocity the velocity of the marquee animation + */ public static void apply( WireBuffer buffer, int iterations, @@ -178,6 +205,12 @@ public class MarqueeModifierOperation extends DecoratorModifierOperation impleme buffer.writeFloat(velocity); } + /** + * Read this operation and add it to the list of operations + * + * @param buffer the buffer to read + * @param operations the list of operations that will be added to + */ public static void read(WireBuffer buffer, List<Operation> operations) { int iterations = buffer.readInt(); int animationMode = buffer.readInt(); @@ -195,6 +228,11 @@ public class MarqueeModifierOperation extends DecoratorModifierOperation impleme velocity)); } + /** + * Populate the documentation with a description of this operation + * + * @param doc to append the description to. + */ public static void documentation(DocumentationBuilder doc) { doc.operation("Modifier Operations", OP_CODE, CLASS_NAME) .description("specify a Marquee Modifier") diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ModifierOperation.java index f8926fef56fa..a86fb2c1f6a5 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ModifierOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ModifierOperation.java @@ -22,5 +22,11 @@ import com.android.internal.widget.remotecompose.core.operations.utilities.Strin /** Represents a modifier */ public interface ModifierOperation extends OperationInterface { + /** + * Serialize the string + * + * @param indent padding to display + * @param serializer append the string + */ void serializeToString(int indent, @NonNull StringSerializer serializer); } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/OffsetModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/OffsetModifierOperation.java index 42719478faf0..2cd2728f0720 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/OffsetModifierOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/OffsetModifierOperation.java @@ -65,6 +65,12 @@ public class OffsetModifierOperation extends DecoratorModifierOperation { apply(buffer, mX, mY); } + /** + * Serialize the string + * + * @param indent padding to display + * @param serializer append the string + */ // @Override public void serializeToString(int indent, StringSerializer serializer) { serializer.append(indent, "OFFSET = [" + mX + ", " + mY + "]"); @@ -110,18 +116,36 @@ public class OffsetModifierOperation extends DecoratorModifierOperation { return OP_CODE; } + /** + * Write the operation to the buffer + * + * @param buffer a WireBuffer + * @param x x offset + * @param y y offset + */ public static void apply(WireBuffer buffer, float x, float y) { buffer.start(OP_CODE); buffer.writeFloat(x); buffer.writeFloat(y); } + /** + * Read this operation and add it to the list of operations + * + * @param buffer the buffer to read + * @param operations the list of operations that will be added to + */ public static void read(WireBuffer buffer, List<Operation> operations) { float x = buffer.readFloat(); float y = buffer.readFloat(); operations.add(new OffsetModifierOperation(x, y)); } + /** + * Populate the documentation with a description of this operation + * + * @param doc to append the description to. + */ public static void documentation(DocumentationBuilder doc) { doc.operation("Modifier Operations", OP_CODE, CLASS_NAME) .description("define the Offset Modifier") diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/PaddingModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/PaddingModifierOperation.java index bcfbdd68472f..3225d5c6f92c 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/PaddingModifierOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/PaddingModifierOperation.java @@ -132,6 +132,15 @@ public class PaddingModifierOperation extends Operation implements ModifierOpera return Operations.MODIFIER_PADDING; } + /** + * Write operation to the buffer + * + * @param buffer a WireBuffer + * @param left left padding + * @param top top padding + * @param right right padding + * @param bottom bottom padding + */ public static void apply( @NonNull WireBuffer buffer, float left, float top, float right, float bottom) { buffer.start(Operations.MODIFIER_PADDING); diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/RippleModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/RippleModifierOperation.java index fe074e4754e2..9787d9b4b399 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/RippleModifierOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/RippleModifierOperation.java @@ -143,19 +143,40 @@ public class RippleModifierOperation extends DecoratorModifierOperation implemen serializer.append(indent, "RIPPLE_MODIFIER"); } + /** + * The operation name + * + * @return operation name + */ @NonNull public static String name() { return "RippleModifier"; } + /** + * Write the operation to the buffer + * + * @param buffer a WireBuffer + */ public static void apply(@NonNull WireBuffer buffer) { buffer.start(OP_CODE); } + /** + * Read this operation and add it to the list of operations + * + * @param buffer the buffer to read + * @param operations the list of operations that will be added to + */ public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) { operations.add(new RippleModifierOperation()); } + /** + * Populate the documentation with a description of this operation + * + * @param doc to append the description to. + */ public static void documentation(@NonNull DocumentationBuilder doc) { doc.operation("Layout Operations", OP_CODE, name()) .description( diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ScrollModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ScrollModifierOperation.java index 8950579354b7..76b3373a52d9 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ScrollModifierOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ScrollModifierOperation.java @@ -135,6 +135,12 @@ public class ScrollModifierOperation extends ListActionsOperation apply(buffer, mDirection, mPositionExpression, mMax, mNotchMax); } + /** + * Serialize the string + * + * @param indent padding to display + * @param serializer append the string + */ // @Override public void serializeToString(int indent, StringSerializer serializer) { serializer.append(indent, "SCROLL = [" + mDirection + "]"); @@ -190,6 +196,15 @@ public class ScrollModifierOperation extends ListActionsOperation return OP_CODE; } + /** + * Write the operation to the buffer + * + * @param buffer a WireBuffer + * @param direction direction of the scroll (HORIZONTAL, VERTICAL) + * @param position the current position + * @param max the maximum position + * @param notchMax the maximum notch + */ public static void apply( WireBuffer buffer, int direction, float position, float max, float notchMax) { buffer.start(OP_CODE); @@ -199,6 +214,12 @@ public class ScrollModifierOperation extends ListActionsOperation buffer.writeFloat(notchMax); } + /** + * Read this operation and add it to the list of operations + * + * @param buffer the buffer to read + * @param operations the list of operations that will be added to + */ public static void read(WireBuffer buffer, List<Operation> operations) { int direction = buffer.readInt(); float position = buffer.readFloat(); @@ -207,6 +228,11 @@ public class ScrollModifierOperation extends ListActionsOperation operations.add(new ScrollModifierOperation(direction, position, max, notchMax)); } + /** + * Populate the documentation with a description of this operation + * + * @param doc to append the description to. + */ public static void documentation(DocumentationBuilder doc) { doc.operation("Modifier Operations", OP_CODE, CLASS_NAME) .description("define a Scroll Modifier") @@ -300,12 +326,24 @@ public class ScrollModifierOperation extends ListActionsOperation public void onTouchCancel( RemoteContext context, CoreDocument document, Component component, float x, float y) {} + /** + * Set the horizontal scroll dimension + * + * @param hostDimension the horizontal host dimension + * @param contentDimension the horizontal content dimension + */ public void setHorizontalScrollDimension(float hostDimension, float contentDimension) { mHostDimension = hostDimension; mContentDimension = contentDimension; mMaxScrollX = contentDimension - hostDimension; } + /** + * Set the vertical scroll dimension + * + * @param hostDimension the vertical host dimension + * @param contentDimension the vertical content dimension + */ public void setVerticalScrollDimension(float hostDimension, float contentDimension) { mHostDimension = hostDimension; mContentDimension = contentDimension; diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueFloatChangeActionOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueFloatChangeActionOperation.java index b6977a035c9e..d625900fcf2e 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueFloatChangeActionOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueFloatChangeActionOperation.java @@ -49,6 +49,11 @@ public class ValueFloatChangeActionOperation extends Operation implements Action return "ValueFloatChangeActionOperation(" + mTargetValueId + ")"; } + /** + * The name of the operation used during serialization + * + * @return the operation serialized name + */ public String serializedName() { return "VALUE_FLOAT_CHANGE"; } @@ -76,18 +81,36 @@ public class ValueFloatChangeActionOperation extends Operation implements Action context.overrideFloat(mTargetValueId, mValue); } + /** + * Write the operation to the buffer + * + * @param buffer a WireBuffer + * @param valueId the value id + * @param value the value to set + */ public static void apply(WireBuffer buffer, int valueId, float value) { buffer.start(OP_CODE); buffer.writeInt(valueId); buffer.writeFloat(value); } + /** + * Read this operation and add it to the list of operations + * + * @param buffer the buffer to read + * @param operations the list of operations that will be added to + */ public static void read(WireBuffer buffer, List<Operation> operations) { int valueId = buffer.readInt(); float value = buffer.readFloat(); operations.add(new ValueFloatChangeActionOperation(valueId, value)); } + /** + * Populate the documentation with a description of this operation + * + * @param doc to append the description to. + */ public static void documentation(DocumentationBuilder doc) { doc.operation("Layout Operations", OP_CODE, "ValueFloatChangeActionOperation") .description( diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueFloatExpressionChangeActionOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueFloatExpressionChangeActionOperation.java index 766271a70ce4..3f26c5e5575b 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueFloatExpressionChangeActionOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueFloatExpressionChangeActionOperation.java @@ -50,6 +50,11 @@ public class ValueFloatExpressionChangeActionOperation extends Operation return "ValueFloatExpressionChangeActionOperation(" + mTargetValueId + ")"; } + /** + * The name of the operation used during serialization + * + * @return the operation serialized name + */ @NonNull public String serializedName() { return "VALUE_FLOAT_EXPRESSION_CHANGE"; @@ -83,6 +88,13 @@ public class ValueFloatExpressionChangeActionOperation extends Operation document.evaluateFloatExpression(mValueExpressionId, mTargetValueId, context); } + /** + * Write the operation to the buffer + * + * @param buffer a WireBuffer + * @param valueId the value id + * @param value the value to set + */ public static void apply(@NonNull WireBuffer buffer, int valueId, int value) { buffer.start(OP_CODE); buffer.writeInt(valueId); diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueIntegerChangeActionOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueIntegerChangeActionOperation.java index 60166a7b2102..8c5bb6fdb268 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueIntegerChangeActionOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueIntegerChangeActionOperation.java @@ -49,6 +49,11 @@ public class ValueIntegerChangeActionOperation extends Operation implements Acti return "ValueChangeActionOperation(" + mTargetValueId + ")"; } + /** + * The name of the operation used during serialization + * + * @return the operation serialized name + */ @NonNull public String serializedName() { return "VALUE_INTEGER_CHANGE"; @@ -81,6 +86,13 @@ public class ValueIntegerChangeActionOperation extends Operation implements Acti context.overrideInteger(mTargetValueId, mValue); } + /** + * Write the operation to the buffer + * + * @param buffer a WireBuffer + * @param valueId the value id + * @param value the value to set + */ public static void apply(@NonNull WireBuffer buffer, int valueId, int value) { buffer.start(OP_CODE); buffer.writeInt(valueId); diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueIntegerExpressionChangeActionOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueIntegerExpressionChangeActionOperation.java index 502508058465..00c80f12aaba 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueIntegerExpressionChangeActionOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueIntegerExpressionChangeActionOperation.java @@ -50,6 +50,11 @@ public class ValueIntegerExpressionChangeActionOperation extends Operation return "ValueIntegerExpressionChangeActionOperation(" + mTargetValueId + ")"; } + /** + * The name of the operation used during serialization + * + * @return the operation serialized name + */ @NonNull public String serializedName() { return "VALUE_INTEGER_EXPRESSION_CHANGE"; @@ -83,6 +88,13 @@ public class ValueIntegerExpressionChangeActionOperation extends Operation document.evaluateIntExpression(mValueExpressionId, (int) mTargetValueId, context); } + /** + * Write the operation to the buffer + * + * @param buffer a WireBuffer + * @param valueId the long id pointing to an int value + * @param value the value to set (long id)` + */ public static void apply(@NonNull WireBuffer buffer, long valueId, long value) { buffer.start(OP_CODE); buffer.writeLong(valueId); diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueStringChangeActionOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueStringChangeActionOperation.java index 8093bb3c64ec..57e30d4126ba 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueStringChangeActionOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ValueStringChangeActionOperation.java @@ -53,6 +53,11 @@ public class ValueStringChangeActionOperation extends Operation implements Actio return mTargetValueId; } + /** + * The name of the operation used during serialization + * + * @return the operation serialized name + */ @NonNull public String serializedName() { return "VALUE_CHANGE"; @@ -85,6 +90,13 @@ public class ValueStringChangeActionOperation extends Operation implements Actio context.overrideText(mTargetValueId, mValueId); } + /** + * Write the operation to the buffer + * + * @param buffer a WireBuffer + * @param valueId the string id + * @param value the value to set (string id)` + */ public static void apply(@NonNull WireBuffer buffer, int valueId, int value) { buffer.start(OP_CODE); buffer.writeInt(valueId); diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/WidthInModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/WidthInModifierOperation.java index c3624e5b3d88..8c1ffbd2c500 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/WidthInModifierOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/WidthInModifierOperation.java @@ -19,47 +19,37 @@ import android.annotation.NonNull; import com.android.internal.widget.remotecompose.core.Operation; import com.android.internal.widget.remotecompose.core.Operations; -import com.android.internal.widget.remotecompose.core.PaintContext; import com.android.internal.widget.remotecompose.core.WireBuffer; import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; import com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation; -import com.android.internal.widget.remotecompose.core.operations.DrawBase2; import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer; import java.util.List; /** Set the min / max width dimension on a component */ -public class WidthInModifierOperation extends DrawBase2 implements ModifierOperation { +public class WidthInModifierOperation extends DimensionInModifierOperation { private static final int OP_CODE = Operations.MODIFIER_WIDTH_IN; public static final String CLASS_NAME = "WidthInModifierOperation"; - /** - * Read this operation and add it to the list of operations - * - * @param buffer the buffer to read - * @param operations the list of operations that will be added to - */ - public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) { - Maker m = WidthInModifierOperation::new; - read(m, buffer, operations); + public WidthInModifierOperation(float min, float max) { + super(OP_CODE, min, max); } - /** - * Returns the min value - * - * @return minimum value - */ - public float getMin() { - return mV1; + @Override + public void write(@NonNull WireBuffer buffer) { + apply(buffer, getMin(), getMax()); } /** - * Returns the max value + * Read this operation and add it to the list of operations * - * @return maximum value + * @param buffer the buffer to read + * @param operations the list of operations that will be added to */ - public float getMax() { - return mV2; + public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) { + float v1 = buffer.readFloat(); + float v2 = buffer.readFloat(); + operations.add(new WidthInModifierOperation(v1, v2)); } /** @@ -81,11 +71,6 @@ public class WidthInModifierOperation extends DrawBase2 implements ModifierOpera return CLASS_NAME; } - @Override - protected void write(@NonNull WireBuffer buffer, float v1, float v2) { - apply(buffer, v1, v2); - } - /** * Populate the documentation with a description of this operation * @@ -98,14 +83,6 @@ public class WidthInModifierOperation extends DrawBase2 implements ModifierOpera .field(DocumentedOperation.FLOAT, "max", "The maximum width, -1 if not applied"); } - public WidthInModifierOperation(float min, float max) { - super(min, max); - mName = CLASS_NAME; - } - - @Override - public void paint(@NonNull PaintContext context) {} - /** * Writes out the WidthInModifier to the buffer * @@ -114,7 +91,9 @@ public class WidthInModifierOperation extends DrawBase2 implements ModifierOpera * @param y1 start y of the DrawOval */ public static void apply(@NonNull WireBuffer buffer, float x1, float y1) { - write(buffer, OP_CODE, x1, y1); + buffer.start(OP_CODE); + buffer.writeFloat(x1); + buffer.writeFloat(y1); } @Override diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/WidthModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/WidthModifierOperation.java index 532027ab2087..687238e62bac 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/WidthModifierOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/WidthModifierOperation.java @@ -52,6 +52,13 @@ public class WidthModifierOperation extends DimensionModifierOperation { return OP_CODE; } + /** + * Write the operation to the buffer + * + * @param buffer a WireBuffer + * @param type the type of dimension rule (DimensionModifierOperation.Type) + * @param value the value of the dimension + */ public static void apply(@NonNull WireBuffer buffer, int type, float value) { buffer.start(OP_CODE); buffer.writeInt(type); diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ZIndexModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ZIndexModifierOperation.java index 35de33a9997a..52841a7e6779 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ZIndexModifierOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/ZIndexModifierOperation.java @@ -55,6 +55,12 @@ public class ZIndexModifierOperation extends DecoratorModifierOperation { apply(buffer, mValue); } + /** + * Serialize the string + * + * @param indent padding to display + * @param serializer append the string + */ // @Override public void serializeToString(int indent, StringSerializer serializer) { serializer.append(indent, "ZINDEX = [" + mValue + "]"); @@ -99,16 +105,33 @@ public class ZIndexModifierOperation extends DecoratorModifierOperation { return OP_CODE; } + /** + * Write the operation to the buffer + * + * @param buffer a WireBuffer + * @param value the z-index value + */ public static void apply(WireBuffer buffer, float value) { buffer.start(OP_CODE); buffer.writeFloat(value); } + /** + * Read this operation and add it to the list of operations + * + * @param buffer the buffer to read + * @param operations the list of operations that will be added to + */ public static void read(WireBuffer buffer, List<Operation> operations) { float value = buffer.readFloat(); operations.add(new ZIndexModifierOperation(value)); } + /** + * Populate the documentation with a description of this operation + * + * @param doc to append the description to. + */ public static void documentation(DocumentationBuilder doc) { doc.operation("Modifier Operations", OP_CODE, CLASS_NAME) .description("define the Z-Index Modifier") diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintBundle.java b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintBundle.java index 95434696abdc..4c7f503e0bf8 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintBundle.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintBundle.java @@ -682,9 +682,9 @@ public class PaintBundle { * @param radius Must be positive. The radius of the gradient. * @param colors The sRGB colors distributed between the center and edge * @param stops May be <code>null</code>. Valid values are between <code>0.0f</code> and <code> - * 1.0f</code>. The relative position of each corresponding color in the colors array. If - * <code>null</code>, colors are distributed evenly between the center and edge of the - * circle. + * 1.0f</code>. The relative position of each corresponding color in the colors + * array. If <code>null</code>, colors are distributed evenly between the center and edge of + * the circle. * @param tileMode The Shader tiling mode */ public void setRadialGradient( @@ -808,7 +808,7 @@ public class PaintBundle { } /** - * Set the color based the R,G,B,A values + * Set the color based the R,G,B,A values (Warning this does not support NaN ids) * * @param r red (0.0 to 1.0) * @param g green (0.0 to 1.0) @@ -816,7 +816,7 @@ public class PaintBundle { * @param a alpha (0.0 to 1.0) */ public void setColor(float r, float g, float b, float a) { - setColor((int) (r * 255), (int) (g * 255), (int) (b * 255), (int) (a * 255)); + setColor(Utils.toARGB(a, r, g, b)); } /** @@ -897,6 +897,11 @@ public class PaintBundle { mPos++; } + /** + * set Filter Bitmap + * + * @param filter set to false to disable interpolation + */ public void setFilterBitmap(boolean filter) { mArray[mPos] = FILTER_BITMAP | (filter ? (1 << 16) : 0); mPos++; @@ -944,6 +949,12 @@ public class PaintBundle { } } + /** + * Convert a blend mode integer as a string + * + * @param mode the blend mode + * @return the blend mode as a string + */ public static @NonNull String blendModeString(int mode) { switch (mode) { case PaintBundle.BLEND_MODE_CLEAR: diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/paint/TextPaint.java b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/TextPaint.java index ff6f45db5385..2812eed8a5ab 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/paint/TextPaint.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/TextPaint.java @@ -17,51 +17,250 @@ package com.android.internal.widget.remotecompose.core.operations.paint; import android.annotation.NonNull; +import java.util.Locale; + // TODO: this interface is unused. Delete it. public interface TextPaint { + + /** + * Helper to setColor(), that takes a,r,g,b and constructs the color int + * + * @param a The new alpha component (0..255) of the paint's color. + * @param r The new red component (0..255) of the paint's color. + * @param g The new green component (0..255) of the paint's color. + * @param b The new blue component (0..255) of the paint's color. + */ void setARGB(int a, int r, int g, int b); + /** + * Helper for setFlags(), setting or clearing the DITHER_FLAG bit Dithering affects how colors + * that are higher precision than the device are down-sampled. No dithering is generally faster, + * but higher precision colors are just truncated down (e.g. 8888 -> 565). Dithering tries to + * distribute the error inherent in this process, to reduce the visual artifacts. + * + * @param dither true to set the dithering bit in flags, false to clear it + */ void setDither(boolean dither); + /** + * Set the paint's elegant height metrics flag. This setting selects font variants that have not + * been compacted to fit Latin-based vertical metrics, and also increases top and bottom bounds + * to provide more space. + * + * @param elegant set the paint's elegant metrics flag for drawing text. + */ void setElegantTextHeight(boolean elegant); + /** + * Set a end hyphen edit on the paint. + * + * <p>By setting end hyphen edit, the measurement and drawing is performed with modifying + * hyphenation at the end of line. For example, by passing character is appended at the end of + * line. + * + * <pre> + * <code> + * Paint paint = new Paint(); + * paint.setEndHyphenEdit(Paint.END_HYPHEN_EDIT_INSERT_HYPHEN); + * paint.measureText("abc", 0, 3); // Returns the width of "abc-" + * Canvas.drawText("abc", 0, 3, 0f, 0f, paint); // Draws "abc-" + * </code> + * </pre> + * + * @param endHyphen a end hyphen edit value. + */ void setEndHyphenEdit(int endHyphen); + /** + * Helper for setFlags(), setting or clearing the FAKE_BOLD_TEXT_FLAG bit + * + * @param fakeBoldText true to set the fakeBoldText bit in the paint's flags, false to clear it. + */ void setFakeBoldText(boolean fakeBoldText); + /** + * Set the paint's flags. Use the Flag enum to specific flag values. + * + * @param flags The new flag bits for the paint + */ void setFlags(int flags); + /** + * Set font feature settings. + * + * <p>The format is the same as the CSS font-feature-settings attribute: <a + * href="https://www.w3.org/TR/css-fonts-3/#font-feature-settings-prop"> + * https://www.w3.org/TR/css-fonts-3/#font-feature-settings-prop</a> + * + * @param settings the font feature settings string to use, may be null. + */ void setFontFeatureSettings(@NonNull String settings); + /** + * Set the paint's hinting mode. May be either + * + * @param mode The new hinting mode. (HINTING_OFF or HINTING_ON) + */ void setHinting(int mode); + /** + * Set the paint's letter-spacing for text. The default value is 0. The value is in 'EM' units. + * Typical values for slight expansion will be around 0.05. Negative values tighten text. + * + * @param letterSpacing set the paint's letter-spacing for drawing text. + */ void setLetterSpacing(float letterSpacing); + /** + * Helper for setFlags(), setting or clearing the LINEAR_TEXT_FLAG bit + * + * @param linearText true to set the linearText bit in the paint's flags, false to clear it. + */ void setLinearText(boolean linearText); + /** + * This draws a shadow layer below the main layer, with the specified offset and color, and blur + * radius. If radius is 0, then the shadow layer is removed. + * + * <p>Can be used to create a blurred shadow underneath text. Support for use with other drawing + * operations is constrained to the software rendering pipeline. + * + * <p>The alpha of the shadow will be the paint's alpha if the shadow color is opaque, or the + * alpha from the shadow color if not. + * + * @param radius the radius of the shadows + * @param dx the x offset of the shadow + * @param dy the y offset of the shadow + * @param shadowColor the color of the shadow + */ void setShadowLayer(float radius, float dx, float dy, int shadowColor); + /** + * Set a start hyphen edit on the paint. + * + * <p>By setting start hyphen edit, the measurement and drawing is performed with modifying + * hyphenation at the start of line. For example, by passing character is appended at the start + * of line. + * + * <pre> + * <code> + * Paint paint = new Paint(); + * paint.setStartHyphenEdit(Paint.START_HYPHEN_EDIT_INSERT_HYPHEN); + * paint.measureText("abc", 0, 3); // Returns the width of "-abc" + * Canvas.drawText("abc", 0, 3, 0f, 0f, paint); // Draws "-abc" + * </code> + * </pre> + * + * The default value is 0 which is equivalent to + * + * @param startHyphen a start hyphen edit value. + */ void setStartHyphenEdit(int startHyphen); + /** + * Helper for setFlags(), setting or clearing the STRIKE_THRU_TEXT_FLAG bit + * + * @param strikeThruText true to set the strikeThruText bit in the paint's flags, false to clear + * it. + */ void setStrikeThruText(boolean strikeThruText); + /** + * Set the paint's Cap. + * + * @param cap set the paint's line cap style, used whenever the paint's style is Stroke or + * StrokeAndFill. + */ void setStrokeCap(int cap); + /** + * Helper for setFlags(), setting or clearing the SUBPIXEL_TEXT_FLAG bit + * + * @param subpixelText true to set the subpixelText bit in the paint's flags, false to clear it. + */ void setSubpixelText(boolean subpixelText); + /** + * Set the paint's text alignment. This controls how the text is positioned relative to its + * origin. LEFT align means that all of the text will be drawn to the right of its origin (i.e. + * the origin specifies the LEFT edge of the text) and so on. + * + * @param align set the paint's Align value for drawing text. + */ void setTextAlign(int align); + /** + * Set the text locale list to a one-member list consisting of just the locale. + * + * @param locale the paint's locale value for drawing text, must not be null. + */ void setTextLocale(int locale); + /** + * Set the text locale list. + * + * <p>The text locale list affects how the text is drawn for some languages. + * + * <p>For example, if the locale list contains {@link Locale#CHINESE} or {@link Locale#CHINA}, + * then the text renderer will prefer to draw text using a Chinese font. Likewise, if the locale + * list contains {@link Locale#JAPANESE} or {@link Locale#JAPAN}, then the text renderer will + * prefer to draw text using a Japanese font. If the locale list contains both, the order those + * locales appear in the list is considered for deciding the font. + * + * <p>This distinction is important because Chinese and Japanese text both use many of the same + * Unicode code points but their appearance is subtly different for each language. + * + * <p>By default, the text locale list is initialized to a one-member list just containing the + * system locales. This assumes that the text to be rendered will most likely be in the user's + * preferred language. + * + * <p>If the actual language or languages of the text is/are known, then they can be provided to + * the text renderer using this method. The text renderer may attempt to guess the language + * script based on the contents of the text to be drawn independent of the text locale here. + * Specifying the text locales just helps it do a better job in certain ambiguous cases. + * + * @param localesArray the paint's locale list for drawing text, must not be null or empty. + */ void setTextLocales(int localesArray); + /** + * Set the paint's horizontal scale factor for text. The default value is 1.0. Values > 1.0 will + * stretch the text wider. Values < 1.0 will stretch the text narrower. + * + * @param scaleX set the paint's scale in X for drawing/measuring text. + */ void setTextScaleX(float scaleX); + /** + * Set the paint's text size. This value must be > 0 + * + * @param textSize set the paint's text size in pixel units. + */ void setTextSize(float textSize); + /** + * Set the paint's horizontal skew factor for text. The default value is 0. For approximating + * oblique text, use values around -0.25. + * + * @param skewX set the paint's skew factor in X for drawing text. + */ void setTextSkewX(float skewX); + /** + * Helper for setFlags(), setting or clearing the UNDERLINE_TEXT_FLAG bit + * + * @param underlineText true to set the underlineText bit in the paint's flags, false to clear + * it. + */ void setUnderlineText(boolean underlineText); + /** + * Set the paint's extra word-spacing for text. + * + * <p>Increases the white space width between words with the given amount of pixels. The default + * value is 0. + * + * @param wordSpacing set the paint's extra word-spacing for drawing text in pixels. + */ void setWordSpacing(float wordSpacing); } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/CollectionsAccess.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/CollectionsAccess.java index b92f96f63eb9..704f6ce3447a 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/CollectionsAccess.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/CollectionsAccess.java @@ -22,6 +22,14 @@ import android.annotation.Nullable; * unavailable */ public interface CollectionsAccess { + + /** + * Get the float value in the array at the given index + * + * @param id the id of the float array + * @param index the index of the value + * @return + */ float getFloatValue(int id, int index); /** diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/DataMap.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/DataMap.java index 07a3d8482db2..04beba3c4af8 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/DataMap.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/DataMap.java @@ -28,6 +28,12 @@ public class DataMap { mIds = ids; } + /** + * Return position for given string + * + * @param str string + * @return position associated with the string + */ public int getPos(@NonNull String str) { for (int i = 0; i < mNames.length; i++) { String name = mNames[i]; @@ -38,10 +44,22 @@ public class DataMap { return -1; } + /** + * Return type for given index + * + * @param pos index + * @return type at index + */ public byte getType(int pos) { return mTypes[pos]; } + /** + * Return id for given index + * + * @param pos index + * @return id at index + */ public int getId(int pos) { return mIds[pos]; } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/ImageScaling.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/ImageScaling.java index 98ee91b370e0..93af8bcef206 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/ImageScaling.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/ImageScaling.java @@ -76,6 +76,20 @@ public class ImageScaling { adjustDrawToType(); } + /** + * Setup the ImageScaling + * + * @param srcLeft src left + * @param srcTop src top + * @param srcRight src right + * @param srcBottom src bottom + * @param dstLeft destination left + * @param dstTop destination top + * @param dstRight destination right + * @param dstBottom destination bottom + * @param type type of scaling + * @param scale scale factor + */ public void setup( float srcLeft, float srcTop, @@ -215,6 +229,12 @@ public class ImageScaling { } } + /** + * Utility to map a string to the given type + * + * @param type + * @return + */ @NonNull public static String typeToString(int type) { String[] typeString = { diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntMap.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntMap.java index b9aa88146f2a..257eb0645938 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntMap.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/IntMap.java @@ -39,12 +39,20 @@ public class IntMap<T> { } } + /** Clear the map */ public void clear() { Arrays.fill(mKeys, NOT_PRESENT); mValues.clear(); mSize = 0; } + /** + * Insert the value into the map with the given key + * + * @param key + * @param value + * @return + */ @Nullable public T put(int key, @NonNull T value) { if (key == NOT_PRESENT) throw new IllegalArgumentException("Key cannot be NOT_PRESENT"); @@ -54,6 +62,12 @@ public class IntMap<T> { return insert(key, value); } + /** + * Return the value associated with the given key + * + * @param key + * @return + */ @Nullable public T get(int key) { int index = findKey(key); @@ -62,6 +76,11 @@ public class IntMap<T> { } else return mValues.get(index); } + /** + * Return the size of the map + * + * @return + */ public int size() { return mSize; } @@ -117,6 +136,12 @@ public class IntMap<T> { } } + /** + * Remote the key from the map + * + * @param key + * @return + */ @Nullable public T remove(int key) { int index = hash(key) % mKeys.length; diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/NanMap.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/NanMap.java index 0616cc7306f5..1f98f62a20b3 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/NanMap.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/NanMap.java @@ -41,18 +41,42 @@ public class NanMap { public static final float CLOSE_NAN = Utils.asNan(CLOSE); public static final float DONE_NAN = Utils.asNan(DONE); + /** + * Returns true if the float id is a system variable + * + * @param value the id encoded as float NaN + * @return + */ public static boolean isSystemVariable(float value) { return (fromNaN(value) >> 20) == 0; } + /** + * Returns true if the float id is a normal variable + * + * @param value the id encoded as float NaN + * @return + */ public static boolean isNormalVariable(float value) { return (fromNaN(value) >> 20) == 1; } + /** + * Returns true if the float id is a data variable + * + * @param value the id encoded as float NaN + * @return + */ public static boolean isDataVariable(float value) { return (fromNaN(value) >> 20) == 2; } + /** + * Returns true if the float id is an operation variable + * + * @param value the id encoded as float NaN + * @return + */ public static boolean isOperationVariable(float value) { return (fromNaN(value) >> 20) == 3; } diff --git a/core/java/com/android/internal/widget/remotecompose/core/semantics/AccessibleComponent.java b/core/java/com/android/internal/widget/remotecompose/core/semantics/AccessibleComponent.java index cc6c2a6ac7a9..928e9ea1a280 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/semantics/AccessibleComponent.java +++ b/core/java/com/android/internal/widget/remotecompose/core/semantics/AccessibleComponent.java @@ -134,6 +134,12 @@ public interface AccessibleComponent extends AccessibilitySemantics { return mDescription; } + /** + * Map int value to Role enum value + * + * @param i int value + * @return corresponding enum value + */ public static Role fromInt(int i) { if (i < UNKNOWN.ordinal()) { return Role.values()[i]; diff --git a/core/java/com/android/internal/widget/remotecompose/core/serialize/MapSerializer.java b/core/java/com/android/internal/widget/remotecompose/core/serialize/MapSerializer.java new file mode 100644 index 000000000000..2be8057ce097 --- /dev/null +++ b/core/java/com/android/internal/widget/remotecompose/core/serialize/MapSerializer.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.widget.remotecompose.core.serialize; + +import android.annotation.Nullable; + +import java.util.List; +import java.util.Map; + +/** Represents a serializer for a map */ +public interface MapSerializer { + + /** + * Add a list entry to this map. The List values can be any primitive, List, Map, or + * Serializable + * + * @param key The key + * @param value The list + */ + <T> void add(String key, @Nullable List<T> value); + + /** + * Add a map entry to this map. The map values can be any primitive, List, Map, or Serializable + * + * @param key The key + * @param value The list + */ + <T> void add(String key, @Nullable Map<String, T> value); + + /** + * Adds any Serializable type to this map + * + * @param key The key + * @param value The Serializable + */ + void add(String key, @Nullable Serializable value); + + /** + * Adds a String entry + * + * @param key The key + * @param value The String + */ + void add(String key, @Nullable String value); + + /** + * Adds a Byte entry + * + * @param key The key + * @param value The Byte + */ + void add(String key, @Nullable Byte value); + + /** + * Adds a Short entry + * + * @param key The key + * @param value The Short + */ + void add(String key, @Nullable Short value); + + /** + * Adds an Integer entry + * + * @param key The key + * @param value The Integer + */ + void add(String key, @Nullable Integer value); + + /** + * Adds a Long entry + * + * @param key The key + * @param value The Long + */ + void add(String key, @Nullable Long value); + + /** + * Adds a Float entry + * + * @param key The key + * @param value The Float + */ + void add(String key, @Nullable Float value); + + /** + * Adds a Double entry + * + * @param key The key + * @param value The Double + */ + void add(String key, @Nullable Double value); + + /** + * Adds a Boolean entry + * + * @param key The key + * @param value The Boolean + */ + void add(String key, @Nullable Boolean value); + + /** + * Adds a Enum entry + * + * @param key The key + * @param value The Enum + */ + <T extends Enum<T>> void add(String key, @Nullable Enum<T> value); +} diff --git a/core/java/com/android/internal/widget/remotecompose/core/serialize/Serializable.java b/core/java/com/android/internal/widget/remotecompose/core/serialize/Serializable.java new file mode 100644 index 000000000000..820cdccfeabb --- /dev/null +++ b/core/java/com/android/internal/widget/remotecompose/core/serialize/Serializable.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.widget.remotecompose.core.serialize; + +/** Implementation for any class that wants to serialize itself */ +public interface Serializable { + + /** + * Called when this class is to be serialized + * + * @param serializer Interface to the serializer + */ + void serialize(MapSerializer serializer); +} diff --git a/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java b/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java index b17e3dc82d50..77f4b6a83eef 100644 --- a/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java +++ b/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java @@ -39,12 +39,13 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.widget.remotecompose.accessibility.RemoteComposeTouchHelper; import com.android.internal.widget.remotecompose.core.CoreDocument; import com.android.internal.widget.remotecompose.core.RemoteContext; +import com.android.internal.widget.remotecompose.core.RemoteContextAware; import com.android.internal.widget.remotecompose.core.operations.NamedVariable; import com.android.internal.widget.remotecompose.core.operations.RootContentBehavior; import com.android.internal.widget.remotecompose.player.platform.RemoteComposeCanvas; /** A view to to display and play RemoteCompose documents */ -public class RemoteComposePlayer extends FrameLayout { +public class RemoteComposePlayer extends FrameLayout implements RemoteContextAware { private RemoteComposeCanvas mInner; private static final int MAX_SUPPORTED_MAJOR_VERSION = MAJOR_VERSION; @@ -65,6 +66,11 @@ public class RemoteComposePlayer extends FrameLayout { init(context, attrs, defStyleAttr); } + @Override + public RemoteContext getRemoteContext() { + return mInner.getRemoteContext(); + } + /** * @inheritDoc */ diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java index 9d385ddafe19..14349b028819 100644 --- a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java +++ b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java @@ -185,7 +185,7 @@ public class AndroidRemoteContext extends RemoteContext { @Override public void runAction(int id, @NonNull String metadata) { - mDocument.performClick(id); + mDocument.performClick(this, id); } @Override diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java b/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java index 334ba62636ff..f76794fc0372 100644 --- a/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java +++ b/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java @@ -147,7 +147,7 @@ public class RemoteComposeCanvas extends FrameLayout implements View.OnAttachSta param.leftMargin = (int) area.getLeft(); param.topMargin = (int) area.getTop(); viewArea.setOnClickListener( - view1 -> mDocument.getDocument().performClick(area.getId())); + view1 -> mDocument.getDocument().performClick(mARContext, area.getId())); addView(viewArea, param); } if (!clickAreas.isEmpty()) { @@ -303,6 +303,10 @@ public class RemoteComposeCanvas extends FrameLayout implements View.OnAttachSta mARContext.setUseChoreographer(value); } + public RemoteContext getRemoteContext() { + return mARContext; + } + public interface ClickCallbacks { void click(int id, String metadata); } diff --git a/core/jni/android_util_Binder.cpp b/core/jni/android_util_Binder.cpp index 639f5bff7614..91b25c2bda06 100644 --- a/core/jni/android_util_Binder.cpp +++ b/core/jni/android_util_Binder.cpp @@ -74,6 +74,7 @@ static struct bindernative_offsets_t jmethodID mExecTransact; jmethodID mGetInterfaceDescriptor; jmethodID mTransactionCallback; + jmethodID mGetExtension; // Object state. jfieldID mObject; @@ -489,8 +490,12 @@ public: if (mVintf) { ::android::internal::Stability::markVintf(b.get()); } - if (mExtension != nullptr) { - b.get()->setExtension(mExtension); + if (mSetExtensionCalled) { + jobject javaIBinderObject = env->CallObjectMethod(obj, gBinderOffsets.mGetExtension); + sp<IBinder> extensionFromJava = ibinderForJavaObject(env, javaIBinderObject); + if (extensionFromJava != nullptr) { + b.get()->setExtension(extensionFromJava); + } } mBinder = b; ALOGV("Creating JavaBinder %p (refs %p) for Object %p, weakCount=%" PRId32 "\n", @@ -516,21 +521,12 @@ public: mVintf = false; } - sp<IBinder> getExtension() { - AutoMutex _l(mLock); - sp<JavaBBinder> b = mBinder.promote(); - if (b != nullptr) { - return b.get()->getExtension(); - } - return mExtension; - } - void setExtension(const sp<IBinder>& extension) { AutoMutex _l(mLock); - mExtension = extension; + mSetExtensionCalled = true; sp<JavaBBinder> b = mBinder.promote(); if (b != nullptr) { - b.get()->setExtension(mExtension); + b.get()->setExtension(extension); } } @@ -542,8 +538,7 @@ private: // is too much binder state here, we can think about making JavaBBinder an // sp here (avoid recreating it) bool mVintf = false; - - sp<IBinder> mExtension; + bool mSetExtensionCalled = false; }; // ---------------------------------------------------------------------------- @@ -1249,10 +1244,6 @@ static void android_os_Binder_blockUntilThreadAvailable(JNIEnv* env, jobject cla return IPCThreadState::self()->blockUntilThreadAvailable(); } -static jobject android_os_Binder_getExtension(JNIEnv* env, jobject obj) { - JavaBBinderHolder* jbh = (JavaBBinderHolder*) env->GetLongField(obj, gBinderOffsets.mObject); - return javaObjectForIBinder(env, jbh->getExtension()); -} static void android_os_Binder_setExtension(JNIEnv* env, jobject obj, jobject extensionObject) { JavaBBinderHolder* jbh = (JavaBBinderHolder*) env->GetLongField(obj, gBinderOffsets.mObject); @@ -1295,8 +1286,7 @@ static const JNINativeMethod gBinderMethods[] = { { "getNativeBBinderHolder", "()J", (void*)android_os_Binder_getNativeBBinderHolder }, { "getNativeFinalizer", "()J", (void*)android_os_Binder_getNativeFinalizer }, { "blockUntilThreadAvailable", "()V", (void*)android_os_Binder_blockUntilThreadAvailable }, - { "getExtension", "()Landroid/os/IBinder;", (void*)android_os_Binder_getExtension }, - { "setExtension", "(Landroid/os/IBinder;)V", (void*)android_os_Binder_setExtension }, + { "setExtensionNative", "(Landroid/os/IBinder;)V", (void*)android_os_Binder_setExtension }, }; // clang-format on @@ -1313,6 +1303,8 @@ static int int_register_android_os_Binder(JNIEnv* env) gBinderOffsets.mTransactionCallback = GetStaticMethodIDOrDie(env, clazz, "transactionCallback", "(IIII)V"); gBinderOffsets.mObject = GetFieldIDOrDie(env, clazz, "mObject", "J"); + gBinderOffsets.mGetExtension = GetMethodIDOrDie(env, clazz, "getExtension", + "()Landroid/os/IBinder;"); return RegisterMethodsOrDie( env, kBinderPathName, diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index b894d3a6888f..586cafdd2b57 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2009,6 +2009,10 @@ <!-- Component name of the built in wallpaper used to display bitmap wallpapers. This must not be null. --> <string name="image_wallpaper_component" translatable="false">com.android.systemui/com.android.systemui.wallpapers.ImageWallpaper</string> + <!-- Component name of the built in wallpaper that is used when the user-selected wallpaper is + incompatible with the display's resolution or aspect ratio. --> + <string name="fallback_wallpaper_component" translatable="false">com.android.systemui/com.android.systemui.wallpapers.GradientColorWallpaper</string> + <!-- True if WallpaperService is enabled --> <bool name="config_enableWallpaperService">true</bool> diff --git a/core/res/res/values/public-final.xml b/core/res/res/values/public-final.xml index d421944917ea..d8e89318a134 100644 --- a/core/res/res/values/public-final.xml +++ b/core/res/res/values/public-final.xml @@ -3922,4 +3922,86 @@ <public type="color" name="system_error_900" id="0x010600d0" /> <public type="color" name="system_error_1000" id="0x010600d1" /> + <!-- =============================================================== + Resources added in version NEXT of the platform + + NOTE: After this version of the platform is forked, changes cannot be made to the root + branch's groups for that release. Only merge changes to the forked platform branch. + =============================================================== --> + <eat-comment/> + + <staging-public-group-final type="attr" first-id="0x01b70000"> + <public name="removed_" /> + <!-- @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") --> + <public name="adServiceTypes" /> + <!-- @FlaggedApi("android.view.inputmethod.ime_switcher_revamp_api") --> + <public name="languageSettingsActivity"/> + <!-- @FlaggedApi(android.service.controls.flags.Flags.FLAG_HOME_PANEL_DREAM) --> + <public name="dreamCategory"/> + <!-- @FlaggedApi("android.permission.flags.replace_body_sensor_permission_enabled") + @hide @SystemApi --> + <public name="backgroundPermission"/> + <!-- @FlaggedApi(android.view.accessibility.supplemental_description) --> + <public name="supplementalDescription"/> + <!-- @FlaggedApi("android.security.enable_intent_matching_flags") --> + <public name="intentMatchingFlags"/> + <!-- @FlaggedApi(android.view.inputmethod.Flags.FLAG_IME_SWITCHER_REVAMP_API) --> + <public name="layoutLabel"/> + <public name="removed_" /> + <public name="removed_" /> + <!-- @FlaggedApi(android.content.pm.Flags.FLAG_APP_COMPAT_OPTION_16KB) --> + <public name="pageSizeCompat" /> + <!-- @FlaggedApi(android.nfc.Flags.FLAG_NFC_ASSOCIATED_ROLE_SERVICES) --> + <public name="wantsRoleHolderPriority"/> + <!-- @FlaggedApi(android.sdk.Flags.FLAG_MAJOR_MINOR_VERSIONING_SCHEME) --> + <public name="minSdkVersionFull"/> + <public name="removed_" /> + <public name="removed_" /> + <public name="removed_" /> + <public name="removed_" /> + </staging-public-group-final> + + <!-- @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") --> + <public type="attr" name="adServiceTypes" id="0x010106a4" /> + <!-- @FlaggedApi("android.view.inputmethod.ime_switcher_revamp_api") --> + <public type="attr" name="languageSettingsActivity" id="0x010106a5" /> + <!-- @FlaggedApi(android.service.controls.flags.Flags.FLAG_HOME_PANEL_DREAM) --> + <public type="attr" name="dreamCategory" id="0x010106a6" /> + <!-- @FlaggedApi("android.permission.flags.replace_body_sensor_permission_enabled") + @hide @SystemApi --> + <public type="attr" name="backgroundPermission" id="0x010106a7" /> + <!-- @FlaggedApi(android.view.accessibility.supplemental_description) --> + <public type="attr" name="supplementalDescription" id="0x010106a8" /> + <!-- @FlaggedApi("android.security.enable_intent_matching_flags") --> + <public type="attr" name="intentMatchingFlags" id="0x010106a9" /> + <!-- @FlaggedApi(android.view.inputmethod.Flags.FLAG_IME_SWITCHER_REVAMP_API) --> + <public type="attr" name="layoutLabel" id="0x010106aa" /> + <!-- @FlaggedApi(android.content.pm.Flags.FLAG_APP_COMPAT_OPTION_16KB) --> + <public type="attr" name="pageSizeCompat" id="0x010106ab" /> + <!-- @FlaggedApi(android.nfc.Flags.FLAG_NFC_ASSOCIATED_ROLE_SERVICES) --> + <public type="attr" name="wantsRoleHolderPriority" id="0x010106ac" /> + <!-- @FlaggedApi(android.sdk.Flags.FLAG_MAJOR_MINOR_VERSIONING_SCHEME) --> + <public type="attr" name="minSdkVersionFull" id="0x010106ad" /> + + <staging-public-group-final type="string" first-id="0x01b40000"> + <!-- @FlaggedApi(android.content.pm.Flags.FLAG_SDK_DEPENDENCY_INSTALLER) + @hide @SystemApi --> + <public name="config_systemDependencyInstaller" /> + <!-- @hide @SystemApi --> + <public name="removed_config_defaultReservedForTestingProfileGroupExclusivity" /> + <!-- @FlaggedApi(android.permission.flags.Flags.FLAG_SYSTEM_VENDOR_INTELLIGENCE_ROLE_ENABLED) + @hide @SystemApi --> + <public name="config_systemVendorIntelligence" /> + <public name="removed_" /> + <public name="removed_" /> + <public name="removed_" /> + </staging-public-group-final> + + <!-- @FlaggedApi(android.content.pm.Flags.FLAG_SDK_DEPENDENCY_INSTALLER) + @hide @SystemApi --> + <public type="string" name="config_systemDependencyInstaller" id="0x0104004a" /> + <!-- @FlaggedApi(android.permission.flags.Flags.FLAG_SYSTEM_VENDOR_INTELLIGENCE_ROLE_ENABLED) + @hide @SystemApi --> + <public type="string" name="config_systemVendorIntelligence" id="0x0104004b" /> + </resources> diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml index 7baaa6d590f2..2d411d0268b3 100644 --- a/core/res/res/values/public-staging.xml +++ b/core/res/res/values/public-staging.xml @@ -109,69 +109,44 @@ =============================================================== --> <eat-comment/> - <staging-public-group type="attr" first-id="0x01b70000"> + <staging-public-group type="attr" first-id="0x01b30000"> <!-- @FlaggedApi("android.content.pm.sdk_lib_independence") --> <public name="optional"/> - <!-- @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") --> - <public name="adServiceTypes" /> - <!-- @FlaggedApi("android.view.inputmethod.ime_switcher_revamp_api") --> - <public name="languageSettingsActivity"/> - <!-- @FlaggedApi(android.service.controls.flags.Flags.FLAG_HOME_PANEL_DREAM) --> - <public name="dreamCategory"/> - <!-- @FlaggedApi("android.permission.flags.replace_body_sensor_permission_enabled") - @hide @SystemApi --> - <public name="backgroundPermission"/> - <!-- @FlaggedApi(android.view.accessibility.supplemental_description) --> - <public name="supplementalDescription"/> - <!-- @FlaggedApi("android.security.enable_intent_matching_flags") --> - <public name="intentMatchingFlags"/> - <!-- @FlaggedApi(android.view.inputmethod.Flags.FLAG_IME_SWITCHER_REVAMP_API) --> - <public name="layoutLabel"/> <!-- @FlaggedApi(android.content.pm.Flags.FLAG_CHANGE_LAUNCHER_BADGING) --> <public name="alternateLauncherIcons"/> <!-- @FlaggedApi(android.content.pm.Flags.FLAG_CHANGE_LAUNCHER_BADGING) --> <public name="alternateLauncherLabels"/> - <!-- @FlaggedApi(android.content.pm.Flags.FLAG_APP_COMPAT_OPTION_16KB) --> - <public name="pageSizeCompat" /> - <!-- @FlaggedApi(android.nfc.Flags.FLAG_NFC_ASSOCIATED_ROLE_SERVICES) --> - <public name="wantsRoleHolderPriority"/> - <!-- @FlaggedApi(android.sdk.Flags.FLAG_MAJOR_MINOR_VERSIONING_SCHEME) --> - <public name="minSdkVersionFull"/> + <!-- @hide Only for device overlay to use this. --> + <public name="pointerIconVectorFill"/> + <!-- @hide Only for device overlay to use this. --> + <public name="pointerIconVectorFillInverse"/> + <!-- @hide Only for device overlay to use this. --> + <public name="pointerIconVectorStroke"/> + <!-- @hide Only for device overlay to use this. --> + <public name="pointerIconVectorStrokeInverse"/> </staging-public-group> - <staging-public-group type="id" first-id="0x01b60000"> + <staging-public-group type="id" first-id="0x01b20000"> <!-- @FlaggedApi(android.appwidget.flags.Flags.FLAG_ENGAGEMENT_METRICS) --> <public name="remoteViewsMetricsId"/> </staging-public-group> - <staging-public-group type="style" first-id="0x01b50000"> + <staging-public-group type="style" first-id="0x01b10000"> </staging-public-group> - <staging-public-group type="string" first-id="0x01b40000"> - <!-- @FlaggedApi(android.content.pm.Flags.FLAG_SDK_DEPENDENCY_INSTALLER) - @hide @SystemApi --> - <public name="config_systemDependencyInstaller" /> - <!-- @hide @SystemApi --> - <public name="removed_config_defaultReservedForTestingProfileGroupExclusivity" /> - <!-- @FlaggedApi(android.permission.flags.Flags.FLAG_SYSTEM_VENDOR_INTELLIGENCE_ROLE_ENABLED) - @hide @SystemApi --> - <public name="config_systemVendorIntelligence" /> - + <staging-public-group type="string" first-id="0x01b00000"> <!-- @FlaggedApi(android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE_MODULE) @hide @SystemApi --> <public name="config_defaultOnDeviceIntelligenceService" /> - <!-- @FlaggedApi(android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE_MODULE) @hide @SystemApi --> <public name="config_defaultOnDeviceSandboxedInferenceService" /> - <!-- @FlaggedApi(android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE_MODULE) @hide @SystemApi --> <public name="config_defaultOnDeviceIntelligenceDeviceConfigNamespace" /> - </staging-public-group> - <staging-public-group type="dimen" first-id="0x01b30000"> + <staging-public-group type="dimen" first-id="0x01af0000"> <!-- @FlaggedApi(android.os.Flags.FLAG_MATERIAL_MOTION_TOKENS)--> <public name="config_motionStandardFastSpatialDamping"/> <!-- @FlaggedApi(android.os.Flags.FLAG_MATERIAL_MOTION_TOKENS)--> @@ -208,7 +183,7 @@ <public name="config_shapeCornerRadiusXlarge"/> </staging-public-group> - <staging-public-group type="color" first-id="0x01b20000"> + <staging-public-group type="color" first-id="0x01ae0000"> <!-- @FlaggedApi(android.os.Flags.FLAG_MATERIAL_COLORS_10_2024)--> <public name="system_inverse_on_surface_light"/> <!-- @FlaggedApi(android.os.Flags.FLAG_MATERIAL_COLORS_10_2024)--> @@ -235,28 +210,28 @@ <public name="system_surface_tint_dark"/> </staging-public-group> - <staging-public-group type="array" first-id="0x01b10000"> + <staging-public-group type="array" first-id="0x01ad0000"> </staging-public-group> - <staging-public-group type="drawable" first-id="0x01b00000"> + <staging-public-group type="drawable" first-id="0x01ac0000"> </staging-public-group> - <staging-public-group type="layout" first-id="0x01af0000"> + <staging-public-group type="layout" first-id="0x01ab0000"> </staging-public-group> - <staging-public-group type="anim" first-id="0x01ae0000"> + <staging-public-group type="anim" first-id="0x01aa0000"> </staging-public-group> - <staging-public-group type="animator" first-id="0x01ad0000"> + <staging-public-group type="animator" first-id="0x01a90000"> </staging-public-group> - <staging-public-group type="interpolator" first-id="0x01ac0000"> + <staging-public-group type="interpolator" first-id="0x01a80000"> </staging-public-group> - <staging-public-group type="mipmap" first-id="0x01ab0000"> + <staging-public-group type="mipmap" first-id="0x01a70000"> </staging-public-group> - <staging-public-group type="integer" first-id="0x01aa0000"> + <staging-public-group type="integer" first-id="0x01a60000"> <!-- @FlaggedApi(android.os.Flags.FLAG_MATERIAL_MOTION_TOKENS)--> <public name="config_motionStandardFastSpatialStiffness"/> <!-- @FlaggedApi(android.os.Flags.FLAG_MATERIAL_MOTION_TOKENS)--> @@ -283,16 +258,16 @@ <public name="config_motionExpressiveSlowEffectStiffness"/> </staging-public-group> - <staging-public-group type="transition" first-id="0x01a90000"> + <staging-public-group type="transition" first-id="0x01a50000"> </staging-public-group> - <staging-public-group type="raw" first-id="0x01a80000"> + <staging-public-group type="raw" first-id="0x01a40000"> </staging-public-group> - <staging-public-group type="bool" first-id="0x01a70000"> + <staging-public-group type="bool" first-id="0x01a30000"> </staging-public-group> - <staging-public-group type="fraction" first-id="0x01a60000"> + <staging-public-group type="fraction" first-id="0x01a20000"> </staging-public-group> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 8315e3cf1a11..772a7413a4a7 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2270,6 +2270,7 @@ <java-symbol type="string" name="heavy_weight_notification" /> <java-symbol type="string" name="heavy_weight_notification_detail" /> <java-symbol type="string" name="image_wallpaper_component" /> + <java-symbol type="string" name="fallback_wallpaper_component" /> <java-symbol type="string" name="input_method_binding_label" /> <java-symbol type="string" name="input_method_ime_switch_long_click_action_desc" /> <java-symbol type="string" name="launch_warning_original" /> diff --git a/core/tests/FileSystemUtilsTest/Android.bp b/core/tests/FileSystemUtilsTest/Android.bp index 962ff3c0a6e0..f4d92522bb25 100644 --- a/core/tests/FileSystemUtilsTest/Android.bp +++ b/core/tests/FileSystemUtilsTest/Android.bp @@ -36,10 +36,10 @@ cc_library { ldflags: ["-z max-page-size=0x1000"], } -android_test_helper_app { - name: "app_with_4kb_elf", +java_defaults { + name: "app_with_4kb_elf_defaults", srcs: ["app_with_4kb_elf/src/**/*.java"], - manifest: "app_with_4kb_elf/app_with_4kb_elf.xml", + resource_dirs: ["app_with_4kb_elf/res"], compile_multilib: "64", jni_libs: [ "libpunchtest_4kb", @@ -47,7 +47,36 @@ android_test_helper_app { static_libs: [ "androidx.test.rules", "platform-test-annotations", + "androidx.test.uiautomator_uiautomator", + "sysui-helper", ], +} + +android_test_helper_app { + name: "app_with_4kb_elf", + defaults: ["app_with_4kb_elf_defaults"], + manifest: "app_with_4kb_elf/app_with_4kb_elf.xml", + use_embedded_native_libs: true, +} + +android_test_helper_app { + name: "app_with_4kb_compressed_elf", + defaults: ["app_with_4kb_elf_defaults"], + manifest: "app_with_4kb_elf/app_with_4kb_elf.xml", + use_embedded_native_libs: false, +} + +android_test_helper_app { + name: "page_size_compat_disabled_app", + defaults: ["app_with_4kb_elf_defaults"], + manifest: "app_with_4kb_elf/page_size_compat_disabled.xml", + use_embedded_native_libs: true, +} + +android_test_helper_app { + name: "app_with_4kb_elf_no_override", + defaults: ["app_with_4kb_elf_defaults"], + manifest: "app_with_4kb_elf/app_with_4kb_no_override.xml", use_embedded_native_libs: true, } @@ -99,6 +128,9 @@ java_test_host { ":embedded_native_libs_test_app", ":extract_native_libs_test_app", ":app_with_4kb_elf", + ":page_size_compat_disabled_app", + ":app_with_4kb_compressed_elf", + ":app_with_4kb_elf_no_override", ], test_suites: ["general-tests"], test_config: "AndroidTest.xml", diff --git a/core/tests/FileSystemUtilsTest/AndroidTest.xml b/core/tests/FileSystemUtilsTest/AndroidTest.xml index 651a7ca15dac..27f49b2289ba 100644 --- a/core/tests/FileSystemUtilsTest/AndroidTest.xml +++ b/core/tests/FileSystemUtilsTest/AndroidTest.xml @@ -22,7 +22,6 @@ <option name="cleanup-apks" value="true" /> <option name="test-file-name" value="embedded_native_libs_test_app.apk" /> <option name="test-file-name" value="extract_native_libs_test_app.apk" /> - <option name="test-file-name" value="app_with_4kb_elf.apk" /> </target_preparer> <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" > diff --git a/core/tests/FileSystemUtilsTest/app_with_4kb_elf/app_with_4kb_elf.xml b/core/tests/FileSystemUtilsTest/app_with_4kb_elf/app_with_4kb_elf.xml index b9d6d4db2c81..d7a37336cbc3 100644 --- a/core/tests/FileSystemUtilsTest/app_with_4kb_elf/app_with_4kb_elf.xml +++ b/core/tests/FileSystemUtilsTest/app_with_4kb_elf/app_with_4kb_elf.xml @@ -18,7 +18,6 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android.test.pagesizecompat"> <application - android:extractNativeLibs="false" android:pageSizeCompat="enabled"> <uses-library android:name="android.test.runner"/> <activity android:name=".MainActivity" diff --git a/core/tests/FileSystemUtilsTest/app_with_4kb_elf/app_with_4kb_no_override.xml b/core/tests/FileSystemUtilsTest/app_with_4kb_elf/app_with_4kb_no_override.xml new file mode 100644 index 000000000000..b0b5204d6e80 --- /dev/null +++ b/core/tests/FileSystemUtilsTest/app_with_4kb_elf/app_with_4kb_no_override.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android.test.pagesizecompat"> + <application + android:label="PageSizeCompatTestApp"> + <uses-library android:name="android.test.runner"/> + <activity android:name=".MainActivity" + android:exported="true" + android:label="Home page" + android:process=":NewProcess"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + <category android:name="android.intent.category.DEFAULT"/> + </intent-filter> + </activity> + </application> + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="android.test.pagesizecompat"/> +</manifest>
\ No newline at end of file diff --git a/core/tests/FileSystemUtilsTest/app_with_4kb_elf/page_size_compat_disabled.xml b/core/tests/FileSystemUtilsTest/app_with_4kb_elf/page_size_compat_disabled.xml new file mode 100644 index 000000000000..641c5e741014 --- /dev/null +++ b/core/tests/FileSystemUtilsTest/app_with_4kb_elf/page_size_compat_disabled.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android.test.pagesizecompat"> + <application + android:pageSizeCompat="disabled"> + <uses-library android:name="android.test.runner"/> + <activity android:name=".MainActivity" + android:exported="true" + android:process=":NewProcess"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + <category android:name="android.intent.category.DEFAULT"/> + </intent-filter> + </activity> + </application> + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="android.test.pagesizecompat"/> +</manifest>
\ No newline at end of file diff --git a/core/tests/FileSystemUtilsTest/app_with_4kb_elf/res/layout/hello.xml b/core/tests/FileSystemUtilsTest/app_with_4kb_elf/res/layout/hello.xml new file mode 100644 index 000000000000..473f3f9f9402 --- /dev/null +++ b/core/tests/FileSystemUtilsTest/app_with_4kb_elf/res/layout/hello.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:keepScreenOn="true"> + <TextView + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:text="this is a test activity" + /> +</LinearLayout> + diff --git a/core/tests/FileSystemUtilsTest/app_with_4kb_elf/src/android/test/pagesizecompat/MainActivity.java b/core/tests/FileSystemUtilsTest/app_with_4kb_elf/src/android/test/pagesizecompat/MainActivity.java index 893f9cd01497..5d8d8081b0e5 100644 --- a/core/tests/FileSystemUtilsTest/app_with_4kb_elf/src/android/test/pagesizecompat/MainActivity.java +++ b/core/tests/FileSystemUtilsTest/app_with_4kb_elf/src/android/test/pagesizecompat/MainActivity.java @@ -19,6 +19,7 @@ package android.test.pagesizecompat; import android.app.Activity; import android.content.Intent; import android.os.Bundle; +import android.view.View; import androidx.annotation.VisibleForTesting; @@ -43,6 +44,8 @@ public class MainActivity extends Activity { @Override public void onCreate(Bundle savedOnstanceState) { super.onCreate(savedOnstanceState); + View view = getLayoutInflater().inflate(R.layout.hello, null); + setContentView(view); Intent received = getIntent(); int op1 = received.getIntExtra(KEY_OPERAND_1, -1); diff --git a/core/tests/FileSystemUtilsTest/app_with_4kb_elf/src/android/test/pagesizecompat/PageSizeCompatTest.java b/core/tests/FileSystemUtilsTest/app_with_4kb_elf/src/android/test/pagesizecompat/PageSizeCompatTest.java index 9cbe414a0993..7d05c64f7624 100644 --- a/core/tests/FileSystemUtilsTest/app_with_4kb_elf/src/android/test/pagesizecompat/PageSizeCompatTest.java +++ b/core/tests/FileSystemUtilsTest/app_with_4kb_elf/src/android/test/pagesizecompat/PageSizeCompatTest.java @@ -16,6 +16,8 @@ package android.test.pagesizecompat; +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -23,6 +25,10 @@ import android.content.IntentFilter; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; +import androidx.test.uiautomator.By; +import androidx.test.uiautomator.UiDevice; +import androidx.test.uiautomator.UiObject2; +import androidx.test.uiautomator.Until; import org.junit.Assert; import org.junit.Test; @@ -33,9 +39,10 @@ import java.util.concurrent.TimeUnit; @RunWith(AndroidJUnit4.class) public class PageSizeCompatTest { + private static final String WARNING_TEXT = "PageSizeCompatTestApp"; + private static final long TIMEOUT = 5000; - @Test - public void testPageSizeCompat_embedded4KbLib() throws Exception { + public void testPageSizeCompat_appLaunch(boolean shouldPass) throws Exception { Context context = InstrumentationRegistry.getContext(); CountDownLatch receivedSignal = new CountDownLatch(1); @@ -62,6 +69,30 @@ public class PageSizeCompatTest { launchIntent.putExtra(MainActivity.KEY_OPERAND_2, op2); context.startActivity(launchIntent); - Assert.assertTrue("Failed to launch app", receivedSignal.await(10, TimeUnit.SECONDS)); + UiDevice device = UiDevice.getInstance(getInstrumentation()); + device.waitForWindowUpdate(null, TIMEOUT); + + Assert.assertEquals(receivedSignal.await(10, TimeUnit.SECONDS), shouldPass); + } + + @Test + public void testPageSizeCompat_compatEnabled() throws Exception { + testPageSizeCompat_appLaunch(true); + } + + @Test + public void testPageSizeCompat_compatDisabled() throws Exception { + testPageSizeCompat_appLaunch(false); + } + + @Test + public void testPageSizeCompat_compatByAlignmentChecks() throws Exception { + testPageSizeCompat_appLaunch(true); + + //verify warning dialog + UiDevice device = UiDevice.getInstance(getInstrumentation()); + device.waitForWindowUpdate(null, TIMEOUT); + UiObject2 targetObject = device.wait(Until.findObject(By.text(WARNING_TEXT)), TIMEOUT); + Assert.assertTrue(targetObject != null); } } diff --git a/core/tests/FileSystemUtilsTest/src/com/android/internal/content/FileSystemUtilsTest.java b/core/tests/FileSystemUtilsTest/src/com/android/internal/content/FileSystemUtilsTest.java index aed907a0242f..208d74e49afe 100644 --- a/core/tests/FileSystemUtilsTest/src/com/android/internal/content/FileSystemUtilsTest.java +++ b/core/tests/FileSystemUtilsTest/src/com/android/internal/content/FileSystemUtilsTest.java @@ -17,10 +17,12 @@ package com.android.internal.content; import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; import android.platform.test.annotations.AppModeFull; import com.android.tradefed.device.DeviceNotAvailableException; +import com.android.tradefed.targetprep.TargetSetupError; import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; @@ -29,6 +31,12 @@ import org.junit.runner.RunWith; @RunWith(DeviceJUnit4ClassRunner.class) public class FileSystemUtilsTest extends BaseHostJUnit4Test { + private static final String PAGE_SIZE_COMPAT_ENABLED = "app_with_4kb_elf.apk"; + private static final String PAGE_SIZE_COMPAT_DISABLED = "page_size_compat_disabled_app.apk"; + private static final String PAGE_SIZE_COMPAT_ENABLED_COMPRESSED_ELF = + "app_with_4kb_compressed_elf.apk"; + private static final String PAGE_SIZE_COMPAT_ENABLED_BY_PLATFORM = + "app_with_4kb_elf_no_override.apk"; @Test @AppModeFull @@ -48,12 +56,50 @@ public class FileSystemUtilsTest extends BaseHostJUnit4Test { runDeviceTests(appPackage, appPackage + "." + testName); } - @Test - @AppModeFull - public void runAppWith4KbLib_overrideCompatMode() throws DeviceNotAvailableException { + private void runPageSizeCompatTest(String appName, String testMethodName) + throws DeviceNotAvailableException, TargetSetupError { + getDevice().enableAdbRoot(); + String result = getDevice().executeShellCommand("getconf PAGE_SIZE"); + assumeTrue("16384".equals(result.strip())); + installPackage(appName, "-r"); String appPackage = "android.test.pagesizecompat"; String testName = "PageSizeCompatTest"; assertTrue(isPackageInstalled(appPackage)); - runDeviceTests(appPackage, appPackage + "." + testName); + assertTrue(runDeviceTests(appPackage, appPackage + "." + testName, + testMethodName)); + uninstallPackage(appPackage); + } + + @Test + @AppModeFull + public void runAppWith4KbLib_overrideCompatMode() + throws DeviceNotAvailableException, TargetSetupError { + runPageSizeCompatTest(PAGE_SIZE_COMPAT_ENABLED, "testPageSizeCompat_compatEnabled"); + } + + @Test + @AppModeFull + public void runAppWith4KbCompressedLib_overrideCompatMode() + throws DeviceNotAvailableException, TargetSetupError { + runPageSizeCompatTest(PAGE_SIZE_COMPAT_ENABLED_COMPRESSED_ELF, + "testPageSizeCompat_compatEnabled"); + } + + @Test + @AppModeFull + public void runAppWith4KbLib_disabledCompatMode() + throws DeviceNotAvailableException, TargetSetupError { + // This test is expected to fail since compat is disabled in manifest + runPageSizeCompatTest(PAGE_SIZE_COMPAT_DISABLED, + "testPageSizeCompat_compatDisabled"); + } + + @Test + @AppModeFull + public void runAppWith4KbLib_compatByAlignmentChecks() + throws DeviceNotAvailableException, TargetSetupError { + // This test is expected to fail since compat is disabled in manifest + runPageSizeCompatTest(PAGE_SIZE_COMPAT_ENABLED_BY_PLATFORM, + "testPageSizeCompat_compatByAlignmentChecks"); } } diff --git a/core/tests/coretests/src/android/app/NotificationManagerTest.java b/core/tests/coretests/src/android/app/NotificationManagerTest.java index 41432294b3c2..9b97c8feaf12 100644 --- a/core/tests/coretests/src/android/app/NotificationManagerTest.java +++ b/core/tests/coretests/src/android/app/NotificationManagerTest.java @@ -19,7 +19,6 @@ package android.app; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeast; @@ -304,9 +303,8 @@ public class NotificationManagerTest { // It doesn't matter what the returned contents are, as long as we return a channel. // This setup must set up getNotificationChannels(), as that's the method called. - when(mNotificationManager.mBackendService.getOrCreateNotificationChannels(any(), any(), - anyInt(), anyBoolean())).thenReturn( - new ParceledListSlice<>(List.of(exampleChannel()))); + when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(), + anyInt())).thenReturn(new ParceledListSlice<>(List.of(exampleChannel()))); // ask for the same channel 100 times without invalidating the cache for (int i = 0; i < 100; i++) { @@ -318,7 +316,7 @@ public class NotificationManagerTest { NotificationChannel unused = mNotificationManager.getNotificationChannel("id"); verify(mNotificationManager.mBackendService, times(2)) - .getOrCreateNotificationChannels(any(), any(), anyInt(), anyBoolean()); + .getNotificationChannels(any(), any(), anyInt()); } @Test @@ -331,24 +329,23 @@ public class NotificationManagerTest { NotificationChannel c2 = new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_NONE); - when(mNotificationManager.mBackendService.getOrCreateNotificationChannels(any(), any(), - anyInt(), anyBoolean())).thenReturn(new ParceledListSlice<>(List.of(c1, c2))); + when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(), + anyInt())).thenReturn(new ParceledListSlice<>(List.of(c1, c2))); assertThat(mNotificationManager.getNotificationChannel("id1")).isEqualTo(c1); assertThat(mNotificationManager.getNotificationChannel("id2")).isEqualTo(c2); assertThat(mNotificationManager.getNotificationChannel("id3")).isNull(); verify(mNotificationManager.mBackendService, times(1)) - .getOrCreateNotificationChannels(any(), any(), anyInt(), anyBoolean()); + .getNotificationChannels(any(), any(), anyInt()); } @Test @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) public void getNotificationChannels_cachedUntilInvalidated() throws Exception { NotificationManager.invalidateNotificationChannelCache(); - when(mNotificationManager.mBackendService.getOrCreateNotificationChannels(any(), any(), - anyInt(), anyBoolean())).thenReturn( - new ParceledListSlice<>(List.of(exampleChannel()))); + when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(), + anyInt())).thenReturn(new ParceledListSlice<>(List.of(exampleChannel()))); // ask for channels 100 times without invalidating the cache for (int i = 0; i < 100; i++) { @@ -360,7 +357,7 @@ public class NotificationManagerTest { List<NotificationChannel> res = mNotificationManager.getNotificationChannels(); verify(mNotificationManager.mBackendService, times(2)) - .getOrCreateNotificationChannels(any(), any(), anyInt(), anyBoolean()); + .getNotificationChannels(any(), any(), anyInt()); assertThat(res).containsExactlyElementsIn(List.of(exampleChannel())); } @@ -378,9 +375,8 @@ public class NotificationManagerTest { NotificationChannel c2 = new NotificationChannel("other", "name2", NotificationManager.IMPORTANCE_DEFAULT); - when(mNotificationManager.mBackendService.getOrCreateNotificationChannels(any(), any(), - anyInt(), anyBoolean())).thenReturn( - new ParceledListSlice<>(List.of(c1, conv1, c2))); + when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(), + anyInt())).thenReturn(new ParceledListSlice<>(List.of(c1, conv1, c2))); // Lookup for channel c1 and c2: returned as expected assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(c1); @@ -397,9 +393,9 @@ public class NotificationManagerTest { // Lookup of a nonexistent channel is null assertThat(mNotificationManager.getNotificationChannel("id3")).isNull(); - // All of that should have been one call to getOrCreateNotificationChannels() + // All of that should have been one call to getNotificationChannels() verify(mNotificationManager.mBackendService, times(1)) - .getOrCreateNotificationChannels(any(), any(), anyInt(), anyBoolean()); + .getNotificationChannels(any(), any(), anyInt()); } @Test @@ -419,12 +415,12 @@ public class NotificationManagerTest { NotificationChannel channel3 = channel1.copy(); channel3.setName("name3"); - when(mNotificationManager.mBackendService.getOrCreateNotificationChannels(any(), eq(pkg1), - eq(userId), anyBoolean())).thenReturn(new ParceledListSlice<>(List.of(channel1))); - when(mNotificationManager.mBackendService.getOrCreateNotificationChannels(any(), eq(pkg2), - eq(userId), anyBoolean())).thenReturn(new ParceledListSlice<>(List.of(channel2))); - when(mNotificationManager.mBackendService.getOrCreateNotificationChannels(any(), eq(pkg1), - eq(userId1), anyBoolean())).thenReturn(new ParceledListSlice<>(List.of(channel3))); + when(mNotificationManager.mBackendService.getNotificationChannels(any(), eq(pkg1), + eq(userId))).thenReturn(new ParceledListSlice<>(List.of(channel1))); + when(mNotificationManager.mBackendService.getNotificationChannels(any(), eq(pkg2), + eq(userId))).thenReturn(new ParceledListSlice<>(List.of(channel2))); + when(mNotificationManager.mBackendService.getNotificationChannels(any(), eq(pkg1), + eq(userId1))).thenReturn(new ParceledListSlice<>(List.of(channel3))); // set our context to pretend to be from package 1 and userId 0 mContext.setParameters(pkg1, pkg1, userId); @@ -440,7 +436,7 @@ public class NotificationManagerTest { // Those should have been three different calls verify(mNotificationManager.mBackendService, times(3)) - .getOrCreateNotificationChannels(any(), any(), anyInt(), anyBoolean()); + .getNotificationChannels(any(), any(), anyInt()); } @Test diff --git a/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java b/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java index 1bdb006c3465..9f1580cb8b57 100644 --- a/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java +++ b/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java @@ -124,7 +124,8 @@ public class NotificationRankingUpdateTest { getRankingAdjustment(i), isBubble(i), getProposedImportance(i), - hasSensitiveContent(i) + hasSensitiveContent(i), + getSummarization(i) ); rankings[i] = ranking; } @@ -334,6 +335,17 @@ public class NotificationRankingUpdateTest { } /** + * Produces a String that can be used to represent getSummarization, based on the provided + * index. + */ + public static String getSummarization(int index) { + if ((android.app.Flags.nmSummarizationUi() || android.app.Flags.nmSummarization())) { + return "summary " + index; + } + return null; + } + + /** * Produces a boolean that can be used to represent isBubble, based on the provided index. */ public static boolean isBubble(int index) { @@ -461,7 +473,8 @@ public class NotificationRankingUpdateTest { /* rankingAdjustment= */ 0, /* isBubble= */ false, /* proposedImportance= */ 0, - /* sensitiveContent= */ false + /* sensitiveContent= */ false, + /* summarization = */ null ); return ranking; } @@ -550,7 +563,8 @@ public class NotificationRankingUpdateTest { tweak.getRankingAdjustment(), tweak.isBubble(), tweak.getProposedImportance(), - tweak.hasSensitiveContent() + tweak.hasSensitiveContent(), + tweak.getSummarization() ); assertNotEquals(nru, nru2); } diff --git a/core/tests/coretests/src/android/window/DesktopExperienceFlagsTest.java b/core/tests/coretests/src/android/window/DesktopExperienceFlagsTest.java new file mode 100644 index 000000000000..cc06f3d21332 --- /dev/null +++ b/core/tests/coretests/src/android/window/DesktopExperienceFlagsTest.java @@ -0,0 +1,164 @@ +/* + * 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 android.window; + +import static com.android.window.flags.Flags.FLAG_SHOW_DESKTOP_EXPERIENCE_DEV_OPTION; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assume.assumeTrue; +import static org.junit.Assume.assumeFalse; + +import android.content.Context; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.FlagsParameterization; +import android.platform.test.flag.junit.SetFlagsRule; +import android.support.test.uiautomator.UiDevice; +import android.window.DesktopExperienceFlags.DesktopExperienceFlag; + +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.window.flags.Flags; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + +import java.lang.reflect.Field; +import java.util.List; + +/** + * Test class for {@link android.window.DesktopExperienceFlags} + * + * <p>Build/Install/Run: atest FrameworksCoreTests:DesktopExperienceFlagsTest + */ +@SmallTest +@Presubmit +@RunWith(ParameterizedAndroidJunit4.class) +public class DesktopExperienceFlagsTest { + + @Parameters(name = "{0}") + public static List<FlagsParameterization> getParams() { + return FlagsParameterization.allCombinationsOf(FLAG_SHOW_DESKTOP_EXPERIENCE_DEV_OPTION); + } + + @Rule public SetFlagsRule mSetFlagsRule; + + private UiDevice mUiDevice; + private Context mContext; + private boolean mLocalFlagValue = false; + private final DesktopExperienceFlag mOverriddenLocalFlag = + new DesktopExperienceFlag(() -> mLocalFlagValue, true); + private final DesktopExperienceFlag mNotOverriddenLocalFlag = + new DesktopExperienceFlag(() -> mLocalFlagValue, false); + + private static final String OVERRIDE_OFF_SETTING = "0"; + private static final String OVERRIDE_ON_SETTING = "1"; + private static final String OVERRIDE_INVALID_SETTING = "garbage"; + + public DesktopExperienceFlagsTest(FlagsParameterization flags) { + mSetFlagsRule = new SetFlagsRule(flags); + } + + @Before + public void setUp() throws Exception { + mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); + setSysProp(null); + } + + @After + public void tearDown() throws Exception { + resetCache(); + setSysProp(null); + } + + @Test + public void isTrue_overrideOff_featureFlagOn_returnsTrue() throws Exception { + mLocalFlagValue = true; + setSysProp(OVERRIDE_OFF_SETTING); + + assertThat(mOverriddenLocalFlag.isTrue()).isTrue(); + assertThat(mNotOverriddenLocalFlag.isTrue()).isTrue(); + } + + @Test + public void isTrue_overrideOn_featureFlagOn_returnsTrue() throws Exception { + mLocalFlagValue = true; + setSysProp(OVERRIDE_ON_SETTING); + + assertThat(mOverriddenLocalFlag.isTrue()).isTrue(); + assertThat(mNotOverriddenLocalFlag.isTrue()).isTrue(); + } + + @Test + public void isTrue_overrideOff_featureFlagOff_returnsFalse() throws Exception { + mLocalFlagValue = false; + setSysProp(OVERRIDE_OFF_SETTING); + + assertThat(mOverriddenLocalFlag.isTrue()).isFalse(); + assertThat(mNotOverriddenLocalFlag.isTrue()).isFalse(); + } + + @Test + public void isTrue_devOptionEnabled_overrideOn_featureFlagOff() throws Exception { + assumeTrue(Flags.showDesktopExperienceDevOption()); + mLocalFlagValue = false; + setSysProp(OVERRIDE_ON_SETTING); + + assertThat(mOverriddenLocalFlag.isTrue()).isTrue(); + assertThat(mNotOverriddenLocalFlag.isTrue()).isFalse(); + } + + @Test + public void isTrue_devOptionDisabled_overrideOn_featureFlagOff_returnsFalse() throws Exception { + assumeFalse(Flags.showDesktopExperienceDevOption()); + mLocalFlagValue = false; + setSysProp(OVERRIDE_ON_SETTING); + + assertThat(mOverriddenLocalFlag.isTrue()).isFalse(); + assertThat(mNotOverriddenLocalFlag.isTrue()).isFalse(); + } + + private void setSysProp(String value) throws Exception { + if (value == null) { + resetSysProp(); + } else { + mUiDevice.executeShellCommand( + "setprop " + DesktopModeFlags.SYSTEM_PROPERTY_NAME + " " + value); + } + } + + private void resetSysProp() throws Exception { + mUiDevice.executeShellCommand("setprop " + DesktopModeFlags.SYSTEM_PROPERTY_NAME + " ''"); + } + + private void resetCache() throws Exception { + Field cachedToggleOverride = + DesktopExperienceFlags.class.getDeclaredField("sCachedToggleOverride"); + cachedToggleOverride.setAccessible(true); + cachedToggleOverride.set(null, null); + } +} diff --git a/core/tests/coretests/src/android/window/DesktopModeFlagsTest.java b/core/tests/coretests/src/android/window/DesktopModeFlagsTest.java index b28e2b04b342..49927be65ae5 100644 --- a/core/tests/coretests/src/android/window/DesktopModeFlagsTest.java +++ b/core/tests/coretests/src/android/window/DesktopModeFlagsTest.java @@ -21,33 +21,42 @@ import static android.window.DesktopModeFlags.ToggleOverride.OVERRIDE_OFF; import static android.window.DesktopModeFlags.ToggleOverride.OVERRIDE_ON; import static android.window.DesktopModeFlags.ToggleOverride.OVERRIDE_UNSET; import static android.window.DesktopModeFlags.ToggleOverride.fromSetting; -import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TRANSITIONS; import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE; -import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TRANSITIONS; +import static com.android.window.flags.Flags.FLAG_SHOW_DESKTOP_EXPERIENCE_DEV_OPTION; import static com.android.window.flags.Flags.FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeTrue; + import android.content.ContentResolver; import android.content.Context; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.FlagsParameterization; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; +import android.support.test.uiautomator.UiDevice; +import android.window.DesktopModeFlags.DesktopModeFlag; -import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; +import com.android.window.flags.Flags; + import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + import java.lang.reflect.Field; +import java.util.List; /** * Test class for {@link android.window.DesktopModeFlags} @@ -57,21 +66,39 @@ import java.lang.reflect.Field; */ @SmallTest @Presubmit -@RunWith(AndroidJUnit4.class) +@RunWith(ParameterizedAndroidJunit4.class) public class DesktopModeFlagsTest { + @Parameters(name = "{0}") + public static List<FlagsParameterization> getParams() { + return FlagsParameterization.allCombinationsOf(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, + FLAG_SHOW_DESKTOP_EXPERIENCE_DEV_OPTION); + } + @Rule - public SetFlagsRule setFlagsRule = new SetFlagsRule(); + public SetFlagsRule mSetFlagsRule; + private UiDevice mUiDevice; private Context mContext; + private boolean mLocalFlagValue = false; + private final DesktopModeFlag mOverriddenLocalFlag = new DesktopModeFlag( + () -> mLocalFlagValue, true); + private final DesktopModeFlag mNotOverriddenLocalFlag = new DesktopModeFlag( + () -> mLocalFlagValue, false); private static final int OVERRIDE_OFF_SETTING = 0; private static final int OVERRIDE_ON_SETTING = 1; private static final int OVERRIDE_UNSET_SETTING = -1; + public DesktopModeFlagsTest(FlagsParameterization flags) { + mSetFlagsRule = new SetFlagsRule(flags); + } + @Before - public void setUp() { + public void setUp() throws Exception { mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); + setOverride(null); } @After @@ -80,26 +107,35 @@ public class DesktopModeFlagsTest { } @Test - @DisableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - public void isTrue_devOptionFlagDisabled_overrideOff_featureFlagOn_returnsTrue() { + public void isTrue_overrideOff_featureFlagOn() throws Exception { setOverride(OVERRIDE_OFF_SETTING); - // In absence of dev options, follow flag - assertThat(ENABLE_DESKTOP_WINDOWING_MODE.isTrue()).isTrue(); + + if (showDesktopWindowingDevOpts()) { + // DW Dev Opts turns off flags when ON + assertThat(ENABLE_DESKTOP_WINDOWING_MODE.isTrue()).isFalse(); + } else { + // DE Dev Opts doesn't turn flags OFF + assertThat(ENABLE_DESKTOP_WINDOWING_MODE.isTrue()).isTrue(); + } } @Test - @DisableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) - public void isTrue_devOptionFlagDisabled_overrideOn_featureFlagOff_returnsFalse() { + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isTrue_overrideOn_featureFlagOff() throws Exception { setOverride(OVERRIDE_ON_SETTING); - assertThat(ENABLE_DESKTOP_WINDOWING_MODE.isTrue()).isFalse(); + if (showAnyDevOpts()) { + assertThat(ENABLE_DESKTOP_WINDOWING_MODE.isTrue()).isTrue(); + } else { + assertThat(ENABLE_DESKTOP_WINDOWING_MODE.isTrue()).isFalse(); + } } @Test - @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) - public void isTrue_overrideUnset_featureFlagOn_returnsTrue() { + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isTrue_overrideUnset_featureFlagOn() throws Exception { setOverride(OVERRIDE_UNSET_SETTING); // For overridableFlag, for unset overrides, follow flag @@ -107,9 +143,8 @@ public class DesktopModeFlagsTest { } @Test - @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - public void isTrue_overrideUnset_featureFlagOff_returnsFalse() { + public void isTrue_overrideUnset_featureFlagOff() throws Exception { setOverride(OVERRIDE_UNSET_SETTING); // For overridableFlag, for unset overrides, follow flag @@ -117,8 +152,8 @@ public class DesktopModeFlagsTest { } @Test - @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) - public void isTrue_noOverride_featureFlagOn_returnsTrue() { + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isTrue_noOverride_featureFlagOn_returnsTrue() throws Exception { setOverride(null); // For overridableFlag, in absence of overrides, follow flag @@ -126,9 +161,8 @@ public class DesktopModeFlagsTest { } @Test - @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - public void isTrue_noOverride_featureFlagOff_returnsFalse() { + public void isTrue_noOverride_featureFlagOff_returnsFalse() throws Exception { setOverride(null); // For overridableFlag, in absence of overrides, follow flag @@ -136,8 +170,8 @@ public class DesktopModeFlagsTest { } @Test - @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) - public void isTrue_unrecognizableOverride_featureFlagOn_returnsTrue() { + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isTrue_unrecognizableOverride_featureFlagOn_returnsTrue() throws Exception { setOverride(-2); // For overridableFlag, for unrecognized overrides, follow flag @@ -145,9 +179,8 @@ public class DesktopModeFlagsTest { } @Test - @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - public void isTrue_unrecognizableOverride_featureFlagOff_returnsFalse() { + public void isTrue_unrecognizableOverride_featureFlagOff_returnsFalse() throws Exception { setOverride(-2); // For overridableFlag, for unrecognizable overrides, follow flag @@ -155,27 +188,10 @@ public class DesktopModeFlagsTest { } @Test - @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) - public void isTrue_overrideOff_featureFlagOn_returnsFalse() { - setOverride(OVERRIDE_OFF_SETTING); - - // For overridableFlag, follow override if they exist - assertThat(ENABLE_DESKTOP_WINDOWING_MODE.isTrue()).isFalse(); - } - - @Test - @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) - @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - public void isTrue_overrideOn_featureFlagOff_returnsTrue() { - setOverride(OVERRIDE_ON_SETTING); - - // For overridableFlag, follow override if they exist - assertThat(ENABLE_DESKTOP_WINDOWING_MODE.isTrue()).isTrue(); - } + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isTrue_overrideOffThenOn_featureFlagOn_returnsFalseAndFalse() throws Exception { + assumeTrue(showDesktopWindowingDevOpts()); - @Test - @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) - public void isTrue_overrideOffThenOn_featureFlagOn_returnsFalseAndFalse() { setOverride(OVERRIDE_OFF_SETTING); // For overridableFlag, follow override if they exist @@ -188,9 +204,9 @@ public class DesktopModeFlagsTest { } @Test - @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - public void isTrue_overrideOnThenOff_featureFlagOff_returnsTrueAndTrue() { + public void isTrue_overrideOnThenOff_featureFlagOff_returnsTrueAndTrue() throws Exception { + assumeTrue(showAnyDevOpts()); setOverride(OVERRIDE_ON_SETTING); // For overridableFlag, follow override if they exist @@ -203,146 +219,144 @@ public class DesktopModeFlagsTest { } @Test - @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE, - FLAG_ENABLE_DESKTOP_WINDOWING_TRANSITIONS}) - public void isTrue_dwFlagOn_overrideUnset_featureFlagOn_returnsTrue() { + @EnableFlags({FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isTrue_dwFlagOn_overrideUnset_featureFlagOn() throws Exception { + mLocalFlagValue = true; setOverride(OVERRIDE_UNSET_SETTING); // For unset overrides, follow flag - assertThat(ENABLE_DESKTOP_WINDOWING_TRANSITIONS.isTrue()).isTrue(); + assertThat(mOverriddenLocalFlag.isTrue()).isTrue(); + assertThat(mNotOverriddenLocalFlag.isTrue()).isTrue(); } @Test - @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) - @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_TRANSITIONS) - public void isTrue_dwFlagOn_overrideUnset_featureFlagOff_returnsFalse() { + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isTrue_dwFlagOn_overrideUnset_featureFlagOff() throws Exception { + mLocalFlagValue = false; setOverride(OVERRIDE_UNSET_SETTING); // For unset overrides, follow flag - assertThat(ENABLE_DESKTOP_WINDOWING_TRANSITIONS.isTrue()).isFalse(); + assertThat(mOverriddenLocalFlag.isTrue()).isFalse(); + assertThat(mNotOverriddenLocalFlag.isTrue()).isFalse(); } @Test - @EnableFlags({ - FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, - FLAG_ENABLE_DESKTOP_WINDOWING_MODE, - FLAG_ENABLE_DESKTOP_WINDOWING_TRANSITIONS - }) - public void isTrue_dwFlagOn_overrideOn_featureFlagOn_returnsTrue() { + @EnableFlags({FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isTrue_dwFlagOn_overrideOn_featureFlagOn() throws Exception { + mLocalFlagValue = true; setOverride(OVERRIDE_ON_SETTING); // When toggle override matches its default state (dw flag), don't override flags - assertThat(ENABLE_DESKTOP_WINDOWING_TRANSITIONS.isTrue()).isTrue(); + assertThat(mOverriddenLocalFlag.isTrue()).isTrue(); + assertThat(mNotOverriddenLocalFlag.isTrue()).isTrue(); } @Test - @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) - @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_TRANSITIONS) - public void isTrue_dwFlagOn_overrideOn_featureFlagOff_returnsFalse() { + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isTrue_dwFlagOn_overrideOn_featureFlagOff() throws Exception { + mLocalFlagValue = false; setOverride(OVERRIDE_ON_SETTING); - // When toggle override matches its default state (dw flag), don't override flags - assertThat(ENABLE_DESKTOP_WINDOWING_TRANSITIONS.isTrue()).isFalse(); + if (showDesktopExperienceDevOpts()) { + assertThat(mOverriddenLocalFlag.isTrue()).isTrue(); + } else { + assertThat(mOverriddenLocalFlag.isTrue()).isFalse(); + } + assertThat(mNotOverriddenLocalFlag.isTrue()).isFalse(); } @Test - @EnableFlags({ - FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, - FLAG_ENABLE_DESKTOP_WINDOWING_MODE, - FLAG_ENABLE_DESKTOP_WINDOWING_TRANSITIONS - }) - public void isTrue_dwFlagOn_overrideOff_featureFlagOn_returnsTrue() { + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isTrue_dwFlagOn_overrideOff_featureFlagOn() throws Exception { + mLocalFlagValue = true; setOverride(OVERRIDE_OFF_SETTING); - // Follow override if they exist, and is not equal to default toggle state (dw flag) - assertThat(ENABLE_DESKTOP_WINDOWING_TRANSITIONS.isTrue()).isTrue(); + if (showDesktopWindowingDevOpts()) { + // Follow override if they exist, and is not equal to default toggle state (dw flag) + assertThat(mOverriddenLocalFlag.isTrue()).isFalse(); + } else { + assertThat(mOverriddenLocalFlag.isTrue()).isTrue(); + } + assertThat(mNotOverriddenLocalFlag.isTrue()).isTrue(); } @Test - @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) - @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_TRANSITIONS) - public void isTrue_dwFlagOn_overrideOff_featureFlagOff_returnsFalse() { + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isTrue_dwFlagOn_overrideOff_featureFlagOff_returnsFalse() throws Exception { + mLocalFlagValue = false; setOverride(OVERRIDE_OFF_SETTING); // Follow override if they exist, and is not equal to default toggle state (dw flag) - assertThat(ENABLE_DESKTOP_WINDOWING_TRANSITIONS.isTrue()).isFalse(); + assertThat(mOverriddenLocalFlag.isTrue()).isFalse(); + assertThat(mNotOverriddenLocalFlag.isTrue()).isFalse(); } @Test - @EnableFlags({ - FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, - FLAG_ENABLE_DESKTOP_WINDOWING_TRANSITIONS - }) @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - public void isTrue_dwFlagOff_overrideUnset_featureFlagOn_returnsTrue() { + public void isTrue_dwFlagOff_overrideUnset_featureFlagOn_returnsTrue() throws Exception { + mLocalFlagValue = true; setOverride(OVERRIDE_UNSET_SETTING); // For unset overrides, follow flag - assertThat(ENABLE_DESKTOP_WINDOWING_TRANSITIONS.isTrue()).isTrue(); + assertThat(mOverriddenLocalFlag.isTrue()).isTrue(); + assertThat(mNotOverriddenLocalFlag.isTrue()).isTrue(); } @Test - @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) - @DisableFlags({ - FLAG_ENABLE_DESKTOP_WINDOWING_MODE, - FLAG_ENABLE_DESKTOP_WINDOWING_TRANSITIONS - }) - public void isTrue_dwFlagOff_overrideUnset_featureFlagOff_returnsFalse() { + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isTrue_dwFlagOff_overrideUnset_featureFlagOff_returnsFalse() throws Exception { + mLocalFlagValue = false; setOverride(OVERRIDE_UNSET_SETTING); // For unset overrides, follow flag - assertThat(ENABLE_DESKTOP_WINDOWING_TRANSITIONS.isTrue()).isFalse(); + assertThat(mOverriddenLocalFlag.isTrue()).isFalse(); + assertThat(mNotOverriddenLocalFlag.isTrue()).isFalse(); } @Test - @EnableFlags({ - FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, - FLAG_ENABLE_DESKTOP_WINDOWING_TRANSITIONS - }) @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - public void isTrue_dwFlagOff_overrideOn_featureFlagOn_returnsTrue() { + public void isTrue_dwFlagOff_overrideOn_featureFlagOn_returnsTrue() throws Exception { + mLocalFlagValue = true; setOverride(OVERRIDE_ON_SETTING); // Follow override if they exist, and is not equal to default toggle state (dw flag) - assertThat(ENABLE_DESKTOP_WINDOWING_TRANSITIONS.isTrue()).isTrue(); + assertThat(mOverriddenLocalFlag.isTrue()).isTrue(); + assertThat(mNotOverriddenLocalFlag.isTrue()).isTrue(); } @Test - @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) - @DisableFlags({ - FLAG_ENABLE_DESKTOP_WINDOWING_MODE, - FLAG_ENABLE_DESKTOP_WINDOWING_TRANSITIONS - }) - public void isTrue_dwFlagOff_overrideOn_featureFlagOff_returnFalse() { + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isTrue_dwFlagOff_overrideOn_featureFlagOff() throws Exception { + mLocalFlagValue = false; setOverride(OVERRIDE_ON_SETTING); - // Follow override if they exist, and is not equal to default toggle state (dw flag) - assertThat(ENABLE_DESKTOP_WINDOWING_TRANSITIONS.isTrue()).isFalse(); + if (showAnyDevOpts()) { + assertThat(mOverriddenLocalFlag.isTrue()).isTrue(); + } else { + // Follow override if they exist, and is not equal to default toggle state (dw flag) + assertThat(mOverriddenLocalFlag.isTrue()).isFalse(); + } + assertThat(mNotOverriddenLocalFlag.isTrue()).isFalse(); } @Test - @EnableFlags({ - FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, - FLAG_ENABLE_DESKTOP_WINDOWING_TRANSITIONS - }) @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - public void isTrue_dwFlagOff_overrideOff_featureFlagOn_returnsTrue() { + public void isTrue_dwFlagOff_overrideOff_featureFlagOn_returnsTrue() throws Exception { + mLocalFlagValue = true; setOverride(OVERRIDE_OFF_SETTING); // When toggle override matches its default state (dw flag), don't override flags - assertThat(ENABLE_DESKTOP_WINDOWING_TRANSITIONS.isTrue()).isTrue(); + assertThat(mOverriddenLocalFlag.isTrue()).isTrue(); + assertThat(mNotOverriddenLocalFlag.isTrue()).isTrue(); } @Test - @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) - @DisableFlags({ - FLAG_ENABLE_DESKTOP_WINDOWING_MODE, - FLAG_ENABLE_DESKTOP_WINDOWING_TRANSITIONS - }) - public void isTrue_dwFlagOff_overrideOff_featureFlagOff_returnsFalse() { + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isTrue_dwFlagOff_overrideOff_featureFlagOff_returnsFalse() throws Exception { + mLocalFlagValue = false; setOverride(OVERRIDE_OFF_SETTING); - // When toggle override matches its default state (dw flag), don't override flags - assertThat(ENABLE_DESKTOP_WINDOWING_TRANSITIONS.isTrue()).isFalse(); + assertThat(mOverriddenLocalFlag.isTrue()).isFalse(); + assertThat(mNotOverriddenLocalFlag.isTrue()).isFalse(); } @Test @@ -365,7 +379,9 @@ public class DesktopModeFlagsTest { assertThat(OVERRIDE_UNSET.getSetting()).isEqualTo(-1); } - private void setOverride(Integer setting) { + private void setOverride(Integer setting) throws Exception { + setSysProp(setting); + ContentResolver contentResolver = mContext.getContentResolver(); String key = Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES; @@ -376,11 +392,35 @@ public class DesktopModeFlagsTest { } } + private void setSysProp(Integer value) throws Exception { + if (value == null) { + resetSysProp(); + } else { + mUiDevice.executeShellCommand( + "setprop " + DesktopModeFlags.SYSTEM_PROPERTY_NAME + " " + value); + } + } + + private void resetSysProp() throws Exception { + mUiDevice.executeShellCommand("setprop " + DesktopModeFlags.SYSTEM_PROPERTY_NAME + " ''"); + } + private void resetCache() throws Exception { Field cachedToggleOverride = DesktopModeFlags.class.getDeclaredField( "sCachedToggleOverride"); cachedToggleOverride.setAccessible(true); cachedToggleOverride.set(null, null); - setOverride(OVERRIDE_UNSET_SETTING); + } + + private boolean showDesktopWindowingDevOpts() { + return Flags.showDesktopWindowingDevOption() && !Flags.showDesktopExperienceDevOption(); + } + + private boolean showDesktopExperienceDevOpts() { + return Flags.showDesktopExperienceDevOption(); + } + + private boolean showAnyDevOpts() { + return Flags.showDesktopWindowingDevOption() || Flags.showDesktopExperienceDevOption(); } } diff --git a/data/etc/com.android.systemui.xml b/data/etc/com.android.systemui.xml index 45952ea75b6f..3eadf3b94515 100644 --- a/data/etc/com.android.systemui.xml +++ b/data/etc/com.android.systemui.xml @@ -95,5 +95,6 @@ <permission name="android.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND" /> <permission name="android.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW"/> <permission name="android.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE" /> + <permission name="android.permission.SET_UNRESTRICTED_GESTURE_EXCLUSION" /> </privapp-permissions> </permissions> diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java index b332cf0d751f..3d4dccf095f5 100644 --- a/graphics/java/android/graphics/Paint.java +++ b/graphics/java/android/graphics/Paint.java @@ -2198,10 +2198,12 @@ public class Paint { * is configured as {@code 'wght' 500, 'ital' 1}, and if the override is specified as * {@code 'wght' 700, `wdth` 150}, then the effective font variation setting is * {@code `wght' 700, 'ital' 1, 'wdth' 150}. The `wght` value is updated by override, 'ital' - * value is preserved because no overrides, and `wdth` value is added by override. + * value is preserved because no overrides, and `wdth` value is added by override. If the font + * variation override is empty or null, nothing overrides and original font variation settings + * assigned to the font instance is used as it is. * - * @param fontVariationOverride font variation settings. You can pass null or empty string as - * no variation settings. + * @param fontVariationOverride font variation override. You can pass null or empty string for + * clearing font variation override. * * @return true if the provided font variation settings is valid. Otherwise returns false. * diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index d0d1721115cb..1bcb0bb91515 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -18,6 +18,7 @@ package androidx.window.extensions.embedding; import static android.app.ActivityManager.START_SUCCESS; import static android.app.ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; @@ -3154,15 +3155,22 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen final WindowContainerTransaction wct = transactionRecord.getTransaction(); final TaskFragmentContainer launchedInTaskFragment; if (launchingActivity != null) { - final int taskId = getTaskId(launchingActivity); final String overlayTag = options.getString(KEY_OVERLAY_TAG); if (Flags.activityEmbeddingOverlayPresentationFlag() && overlayTag != null) { launchedInTaskFragment = createOrUpdateOverlayTaskFragmentIfNeeded(wct, options, intent, launchingActivity); } else { - launchedInTaskFragment = resolveStartActivityIntent(wct, taskId, intent, - launchingActivity); + final int taskId = getTaskId(launchingActivity); + if (taskId != INVALID_TASK_ID) { + launchedInTaskFragment = resolveStartActivityIntent(wct, taskId, intent, + launchingActivity); + } else { + // We cannot get a valid task id of launchingActivity so we fall back to + // treat it as a non-Activity context. + launchedInTaskFragment = + resolveStartActivityIntentFromNonActivityContext(wct, intent); + } } } else { launchedInTaskFragment = resolveStartActivityIntentFromNonActivityContext(wct, diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt index dd387b382dc6..09a93d501f8e 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt @@ -296,5 +296,9 @@ class BubbleControllerBubbleBarTest { override fun onBubbleStateChange(update: BubbleBarUpdate?) {} override fun animateBubbleBarLocation(location: BubbleBarLocation?) {} + + override fun onDragItemOverBubbleBarDragZone(location: BubbleBarLocation) {} + + override fun onItemDraggedOutsideBubbleBarDropZone() {} } } diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java index 2ca011bfe000..0a1e3b9495a0 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java @@ -111,7 +111,7 @@ public class GroupedTaskInfo implements Parcelable { * Create new for a pair of tasks in split screen */ public static GroupedTaskInfo forSplitTasks(@NonNull TaskInfo task1, - @NonNull TaskInfo task2, @Nullable SplitBounds splitBounds) { + @NonNull TaskInfo task2, @NonNull SplitBounds splitBounds) { return new GroupedTaskInfo(List.of(task1, task2), splitBounds, TYPE_SPLIT, null /* minimizedFreeformTasks */); } diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java index 840de2c86f92..4d00c74155a8 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java @@ -127,6 +127,46 @@ public class TransitionUtil { } /** + * Check if all changes in this transition are only ordering changes. If so, we won't animate. + */ + public static boolean isAllOrderOnly(TransitionInfo info) { + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + if (!isOrderOnly(info.getChanges().get(i))) return false; + } + return true; + } + + /** + * Look through a transition and see if all non-closing changes are no-animation. If so, no + * animation should play. + */ + public static boolean isAllNoAnimation(TransitionInfo info) { + if (isClosingType(info.getType())) { + // no-animation is only relevant for launching (open) activities. + return false; + } + boolean hasNoAnimation = false; + final int changeSize = info.getChanges().size(); + for (int i = changeSize - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (isClosingType(change.getMode())) { + // ignore closing apps since they are a side-effect of the transition and don't + // animate. + continue; + } + if (change.hasFlags(TransitionInfo.FLAG_NO_ANIMATION)) { + hasNoAnimation = true; + } else if (!isOrderOnly(change) && !change.hasFlags(TransitionInfo.FLAG_IS_OCCLUDED)) { + // Ignore the order only or occluded changes since they shouldn't be visible during + // animation. For anything else, we need to animate if at-least one relevant + // participant *is* animated, + return false; + } + } + return hasNoAnimation; + } + + /** * Filter that selects leaf-tasks only. THIS IS ORDER-DEPENDENT! For it to work properly, you * MUST call `test` in the same order that the changes appear in the TransitionInfo. */ diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt new file mode 100644 index 000000000000..0ea3c2a80fb4 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt @@ -0,0 +1,60 @@ +/* + * 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.wm.shell.shared.desktopmode + +import android.app.TaskInfo +import android.content.Context +import android.window.DesktopModeFlags +import com.android.internal.R + +/** + * Class to decide whether to apply app compat policies in desktop mode. + */ +// TODO(b/347289970): Consider replacing with API +class DesktopModeCompatPolicy(context: Context) { + + private val systemUiPackage: String = context.resources.getString(R.string.config_systemUi) + + /** + * If the top activity should be exempt from desktop windowing and forced back to fullscreen. + * Currently includes all system ui activities and modal dialogs. However if the top activity is + * not being displayed, regardless of its configuration, we will not exempt it as to remain in + * the desktop windowing environment. + */ + fun isTopActivityExemptFromDesktopWindowing(task: TaskInfo) = + isTopActivityExemptFromDesktopWindowing(task.baseActivity?.packageName, + task.numActivities, task.isTopActivityNoDisplay, task.isActivityStackTransparent) + + fun isTopActivityExemptFromDesktopWindowing(packageName: String?, + numActivities: Int, isTopActivityNoDisplay: Boolean, isActivityStackTransparent: Boolean) = + DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue + && ((isSystemUiTask(packageName) + || isTransparentTask(isActivityStackTransparent, numActivities)) + && !isTopActivityNoDisplay) + + /** + * Returns true if all activities in a tasks stack are transparent. If there are no activities + * will return false. + */ + fun isTransparentTask(task: TaskInfo): Boolean = + isTransparentTask(task.isActivityStackTransparent, task.numActivities) + + private fun isTransparentTask(isActivityStackTransparent: Boolean, numActivities: Int) = + isActivityStackTransparent && numActivities > 0 + + private fun isSystemUiTask(packageName: String?) = packageName == systemUiPackage +} diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java index 2fed1380b635..1ee71ca78815 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java @@ -218,6 +218,13 @@ public class DesktopModeStatus { return isDeviceEligibleForDesktopMode(context) && Flags.showDesktopWindowingDevOption(); } + /** + * Return {@code true} if desktop mode dev option should be shown on current device + */ + public static boolean canShowDesktopExperienceDevOption(@NonNull Context context) { + return Flags.showDesktopExperienceDevOption(); + } + /** Returns if desktop mode dev option should be enabled if there is no user override. */ public static boolean shouldDevOptionBeEnabledByDefault() { return Flags.enableDesktopWindowingMode(); @@ -290,7 +297,7 @@ public class DesktopModeStatus { /** * Return {@code true} if desktop mode is unrestricted and is supported in the device. */ - private static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) { + public static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) { return !enforceDeviceRestrictions() || isDesktopModeSupported(context); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java index e8e25e20d8d8..c40a276cb7bd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -28,6 +28,7 @@ import android.annotation.Nullable; import android.app.Notification; import android.app.PendingIntent; import android.app.Person; +import android.app.TaskInfo; import android.content.Context; import android.content.Intent; import android.content.LocusId; @@ -55,8 +56,10 @@ import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView; import com.android.wm.shell.bubbles.bar.BubbleBarLayerView; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; import com.android.wm.shell.shared.bubbles.BubbleInfo; import com.android.wm.shell.shared.bubbles.ParcelableFlyoutMessage; +import com.android.wm.shell.taskview.TaskView; import java.io.PrintWriter; import java.util.List; @@ -204,6 +207,13 @@ public class Bubble implements BubbleViewProvider { private Intent mAppIntent; /** + * Set while preparing a transition for animation. Several steps are needed before animation + * starts, so this is used to detect and route associated events to the coordinating transition. + */ + @Nullable + private BubbleTransitions.BubbleTransition mPreparingTransition; + + /** * Create a bubble with limited information based on given {@link ShortcutInfo}. * Note: Currently this is only being used when the bubble is persisted to disk. */ @@ -280,6 +290,30 @@ public class Bubble implements BubbleViewProvider { mShortcutInfo = info; } + private Bubble( + TaskInfo task, + UserHandle user, + @Nullable Icon icon, + String key, + @ShellMainThread Executor mainExecutor, + @ShellBackgroundThread Executor bgExecutor) { + mGroupKey = null; + mLocusId = null; + mFlags = 0; + mUser = user; + mIcon = icon; + mIsAppBubble = true; + mKey = key; + mShowBubbleUpdateDot = false; + mMainExecutor = mainExecutor; + mBgExecutor = bgExecutor; + mTaskId = task.taskId; + mAppIntent = null; + mDesiredHeight = Integer.MAX_VALUE; + mPackageName = task.baseActivity.getPackageName(); + } + + /** Creates an app bubble. */ public static Bubble createAppBubble(Intent intent, UserHandle user, @Nullable Icon icon, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) { @@ -291,6 +325,16 @@ public class Bubble implements BubbleViewProvider { mainExecutor, bgExecutor); } + /** Creates a task bubble. */ + public static Bubble createTaskBubble(TaskInfo info, UserHandle user, @Nullable Icon icon, + @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) { + return new Bubble(info, + user, + icon, + getAppBubbleKeyForTask(info), + mainExecutor, bgExecutor); + } + /** Creates a shortcut bubble. */ public static Bubble createShortcutBubble( ShortcutInfo info, @@ -316,6 +360,15 @@ public class Bubble implements BubbleViewProvider { return info.getPackage() + ":" + info.getUserId() + ":" + info.getId(); } + /** + * Returns the key for an app bubble from an app with package name, {@code packageName} on an + * Android user, {@code user}. + */ + public static String getAppBubbleKeyForTask(TaskInfo taskInfo) { + Objects.requireNonNull(taskInfo); + return KEY_APP_BUBBLE + ":" + taskInfo.taskId; + } + @VisibleForTesting(visibility = PRIVATE) public Bubble(@NonNull final BubbleEntry entry, final Bubbles.BubbleMetadataFlagListener listener, @@ -469,6 +522,10 @@ public class Bubble implements BubbleViewProvider { return mBubbleTaskView; } + public TaskView getTaskView() { + return mBubbleTaskView.getTaskView(); + } + /** * @return the ShortcutInfo id if it exists, or the metadata shortcut id otherwise. */ @@ -486,6 +543,10 @@ public class Bubble implements BubbleViewProvider { return (mMetadataShortcutId != null && !mMetadataShortcutId.isEmpty()); } + public BubbleTransitions.BubbleTransition getPreparingTransition() { + return mPreparingTransition; + } + /** * Call this to clean up the task for the bubble. Ensure this is always called when done with * the bubble. @@ -512,7 +573,8 @@ public class Bubble implements BubbleViewProvider { mIntentActive = false; } - private void cleanupTaskView() { + /** Cleans-up the taskview associated with this bubble (possibly removing the task from wm) */ + public void cleanupTaskView() { if (mBubbleTaskView != null) { mBubbleTaskView.cleanup(); mBubbleTaskView = null; @@ -533,7 +595,7 @@ public class Bubble implements BubbleViewProvider { * <p>If we're switching between bar and floating modes, pass {@code false} on * {@code cleanupTaskView} to avoid recreating it in the new mode. */ - void cleanupViews(boolean cleanupTaskView) { + public void cleanupViews(boolean cleanupTaskView) { cleanupExpandedView(cleanupTaskView); mIconView = null; } @@ -556,6 +618,13 @@ public class Bubble implements BubbleViewProvider { } /** + * Sets the current bubble-transition that is coordinating a change in this bubble. + */ + void setPreparingTransition(BubbleTransitions.BubbleTransition transit) { + mPreparingTransition = transit; + } + + /** * Sets whether this bubble is considered text changed. This method is purely for * testing. */ @@ -1025,7 +1094,7 @@ public class Bubble implements BubbleViewProvider { * intent for an app. In this case we don't show a badge on the icon. */ public boolean isAppLaunchIntent() { - if (Flags.enableBubbleAnything() && mAppIntent != null) { + if (BubbleAnythingFlagHelper.enableCreateAnyBubble() && mAppIntent != null) { return mAppIntent.hasCategory("android.intent.category.LAUNCHER"); } return false; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 4f9028e8aaf3..5cd04b11bbfd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -40,9 +40,11 @@ import android.annotation.BinderThread; import android.annotation.NonNull; import android.annotation.UserIdInt; import android.app.ActivityManager; +import android.app.ActivityOptions; import android.app.Notification; import android.app.NotificationChannel; import android.app.PendingIntent; +import android.app.TaskInfo; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -78,6 +80,8 @@ import android.view.WindowInsets; import android.view.WindowManager; import android.window.ScreenCapture; import android.window.ScreenCapture.SynchronousScreenCaptureListener; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; import androidx.annotation.MainThread; import androidx.annotation.Nullable; @@ -110,6 +114,7 @@ import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.onehanded.OneHandedTransitionCallback; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; import com.android.wm.shell.shared.bubbles.BubbleBarLocation; import com.android.wm.shell.shared.bubbles.BubbleBarUpdate; import com.android.wm.shell.sysui.ConfigurationChangeListener; @@ -287,6 +292,8 @@ public class BubbleController implements ConfigurationChangeListener, /** Used to send updates to the views from {@link #mBubbleDataListener}. */ private BubbleViewCallback mBubbleViewCallback; + private final BubbleTransitions mBubbleTransitions; + public BubbleController(Context context, ShellInit shellInit, ShellCommandHandler shellCommandHandler, @@ -350,12 +357,16 @@ public class BubbleController implements ConfigurationChangeListener, context.getResources().getDimensionPixelSize( com.android.internal.R.dimen.importance_ring_stroke_width)); mDisplayController = displayController; + final TaskViewTransitions tvTransitions; if (TaskViewTransitions.useRepo()) { - mTaskViewController = new TaskViewTransitions(transitions, taskViewRepository, - organizer, syncQueue); + tvTransitions = new TaskViewTransitions(transitions, taskViewRepository, organizer, + syncQueue); } else { - mTaskViewController = taskViewTransitions; + tvTransitions = taskViewTransitions; } + mTaskViewController = new BubbleTaskViewController(tvTransitions); + mBubbleTransitions = new BubbleTransitions(transitions, organizer, taskViewRepository, data, + tvTransitions, context); mTransitions = transitions; mOneHandedOptional = oneHandedOptional; mDragAndDropController = dragAndDropController; @@ -1422,7 +1433,7 @@ public class BubbleController implements ConfigurationChangeListener, * @param info the shortcut info for the bubble. */ public void expandStackAndSelectBubble(ShortcutInfo info) { - if (!Flags.enableBubbleAnything()) return; + if (!BubbleAnythingFlagHelper.enableCreateAnyBubble()) return; Bubble b = mBubbleData.getOrCreateBubble(info); // Removes from overflow ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - shortcut=%s", info); if (b.isInflated()) { @@ -1439,7 +1450,7 @@ public class BubbleController implements ConfigurationChangeListener, * @param intent the intent for the bubble. */ public void expandStackAndSelectBubble(Intent intent, UserHandle user) { - if (!Flags.enableBubbleAnything()) return; + if (!BubbleAnythingFlagHelper.enableCreateAnyBubble()) return; Bubble b = mBubbleData.getOrCreateBubble(intent, user); // Removes from overflow ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - intent=%s", intent); if (b.isInflated()) { @@ -1456,7 +1467,19 @@ public class BubbleController implements ConfigurationChangeListener, * @param taskInfo the task. */ public void expandStackAndSelectBubble(ActivityManager.RunningTaskInfo taskInfo) { - // TODO(384976265): Not implemented yet + if (!BubbleAnythingFlagHelper.enableBubbleToFullscreen()) return; + Bubble b = mBubbleData.getOrCreateBubble(taskInfo); // Removes from overflow + ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - intent=%s", taskInfo.taskId); + if (b.isInflated()) { + mBubbleData.setSelectedBubbleAndExpandStack(b); + } else { + b.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); + // Lazy init stack view when a bubble is created + ensureBubbleViewsAndWindowCreated(); + mBubbleTransitions.startConvertToBubble(b, taskInfo, mExpandedViewManager, + mBubbleTaskViewFactory, mBubblePositioner, mLogger, mStackView, mLayerView, + mBubbleIconFactory, mInflateSynchronously); + } } /** @@ -2057,7 +2080,12 @@ public class BubbleController implements ConfigurationChangeListener, @Override public void removeBubble(Bubble removedBubble) { if (mLayerView != null) { + final BubbleTransitions.BubbleTransition bubbleTransit = + removedBubble.getPreparingTransition(); mLayerView.removeBubble(removedBubble, () -> { + if (bubbleTransit != null) { + bubbleTransit.continueCollapse(); + } if (!mBubbleData.hasBubbles() && !isStackExpanded()) { mLayerView.setVisibility(INVISIBLE); removeFromWindowManagerMaybe(); @@ -2261,9 +2289,16 @@ public class BubbleController implements ConfigurationChangeListener, private void showExpandedViewForBubbleBar() { BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble(); - if (selectedBubble != null && mLayerView != null) { - mLayerView.showExpandedView(selectedBubble); + if (selectedBubble == null) return; + if (selectedBubble instanceof Bubble) { + final Bubble bubble = (Bubble) selectedBubble; + if (bubble.getPreparingTransition() != null) { + bubble.getPreparingTransition().continueExpand(); + return; + } } + if (mLayerView == null) return; + mLayerView.showExpandedView(selectedBubble); } private void collapseExpandedViewForBubbleBar() { @@ -2481,7 +2516,7 @@ public class BubbleController implements ConfigurationChangeListener, * @param entry the entry to bubble. */ static boolean canLaunchInTaskView(Context context, BubbleEntry entry) { - if (Flags.enableBubbleAnything()) return true; + if (BubbleAnythingFlagHelper.enableCreateAnyBubble()) return true; PendingIntent intent = entry.getBubbleMetadata() != null ? entry.getBubbleMetadata().getIntent() : null; @@ -2613,6 +2648,17 @@ public class BubbleController implements ConfigurationChangeListener, public void animateBubbleBarLocation(BubbleBarLocation location) { mListener.call(l -> l.animateBubbleBarLocation(location)); } + + @Override + public void onDragItemOverBubbleBarDragZone( + @NonNull BubbleBarLocation location) { + mListener.call(l -> l.onDragItemOverBubbleBarDragZone(location)); + } + + @Override + public void onItemDraggedOutsideBubbleBarDropZone() { + mListener.call(IBubblesListener::onItemDraggedOutsideBubbleBarDropZone); + } }; IBubblesImpl(BubbleController controller) { @@ -2665,7 +2711,18 @@ public class BubbleController implements ConfigurationChangeListener, @Override public void collapseBubbles() { - mMainExecutor.execute(() -> mController.collapseStack()); + mMainExecutor.execute(() -> { + if (mBubbleData.getSelectedBubble() instanceof Bubble) { + if (((Bubble) mBubbleData.getSelectedBubble()).getPreparingTransition() + != null) { + // Currently preparing a transition which will, itself, collapse the bubble. + // For transition preparation, the timing of bubble-collapse must be in + // sync with the rest of the set-up. + return; + } + } + mController.collapseStack(); + }); } @Override @@ -3057,4 +3114,84 @@ public class BubbleController implements ConfigurationChangeListener, return mKeyToShownInShadeMap.get(key); } } + + private class BubbleTaskViewController implements TaskViewController { + private final TaskViewTransitions mBaseTransitions; + + BubbleTaskViewController(TaskViewTransitions baseTransitions) { + mBaseTransitions = baseTransitions; + } + + @Override + public void registerTaskView(TaskViewTaskController tv) { + mBaseTransitions.registerTaskView(tv); + } + + @Override + public void unregisterTaskView(TaskViewTaskController tv) { + mBaseTransitions.unregisterTaskView(tv); + } + + @Override + public void startShortcutActivity(@NonNull TaskViewTaskController destination, + @NonNull ShortcutInfo shortcut, @NonNull ActivityOptions options, + @Nullable Rect launchBounds) { + mBaseTransitions.startShortcutActivity(destination, shortcut, options, launchBounds); + } + + @Override + public void startActivity(@NonNull TaskViewTaskController destination, + @NonNull PendingIntent pendingIntent, @Nullable Intent fillInIntent, + @NonNull ActivityOptions options, @Nullable Rect launchBounds) { + mBaseTransitions.startActivity(destination, pendingIntent, fillInIntent, + options, launchBounds); + } + + @Override + public void startRootTask(@NonNull TaskViewTaskController destination, + ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash, + @Nullable WindowContainerTransaction wct) { + mBaseTransitions.startRootTask(destination, taskInfo, leash, wct); + } + + @Override + public void removeTaskView(@NonNull TaskViewTaskController taskView, + @Nullable WindowContainerToken taskToken) { + mBaseTransitions.removeTaskView(taskView, taskToken); + } + + @Override + public void moveTaskViewToFullscreen(@NonNull TaskViewTaskController taskView) { + final TaskInfo tinfo = taskView.getTaskInfo(); + if (tinfo == null) { + return; + } + Bubble bub = null; + for (Bubble b : mBubbleData.getBubbles()) { + if (b.getTaskId() == tinfo.taskId) { + bub = b; + break; + } + } + if (bub == null) { + return; + } + mBubbleTransitions.startConvertFromBubble(bub, tinfo); + } + + @Override + public void setTaskViewVisible(TaskViewTaskController taskView, boolean visible) { + mBaseTransitions.setTaskViewVisible(taskView, visible); + } + + @Override + public void setTaskBounds(TaskViewTaskController taskView, Rect boundsOnScreen) { + mBaseTransitions.setTaskBounds(taskView, boundsOnScreen); + } + + @Override + public boolean isUsingShellTransitions() { + return mBaseTransitions.isUsingShellTransitions(); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java index 74302094a296..96d0f6d5654e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -22,6 +22,7 @@ import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES; import android.annotation.NonNull; import android.app.PendingIntent; +import android.app.TaskInfo; import android.content.Context; import android.content.Intent; import android.content.LocusId; @@ -470,6 +471,17 @@ public class BubbleData { return bubbleToReturn; } + Bubble getOrCreateBubble(TaskInfo taskInfo) { + UserHandle user = UserHandle.of(mCurrentUserId); + String bubbleKey = Bubble.getAppBubbleKeyForTask(taskInfo); + Bubble bubbleToReturn = findAndRemoveBubbleFromOverflow(bubbleKey); + if (bubbleToReturn == null) { + bubbleToReturn = Bubble.createTaskBubble(taskInfo, user, null, mMainExecutor, + mBgExecutor); + } + return bubbleToReturn; + } + @Nullable private Bubble findAndRemoveBubbleFromOverflow(String key) { Bubble bubbleToReturn = getBubbleInStackWithKey(key); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index 13f8e9ef9dd3..e98d53e85b94 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -71,6 +71,7 @@ import com.android.wm.shell.Flags; import com.android.wm.shell.R; import com.android.wm.shell.common.AlphaOptimizedButton; import com.android.wm.shell.shared.TriangleShape; +import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; import com.android.wm.shell.taskview.TaskView; import java.io.PrintWriter; @@ -226,7 +227,8 @@ public class BubbleExpandedView extends LinearLayout { MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); final boolean isShortcutBubble = (mBubble.hasMetadataShortcutId() - || (mBubble.getShortcutInfo() != null && Flags.enableBubbleAnything())); + || (mBubble.getShortcutInfo() != null + && BubbleAnythingFlagHelper.enableCreateAnyBubble())); if (mBubble.isAppBubble()) { Context context = diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt index 62995319db80..086c91985ae3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt @@ -137,14 +137,15 @@ class BubbleOverflow(private val context: Context, private val positioner: Bubbl // Update bitmap val fg = InsetDrawable(overflowBtn?.iconDrawable, overflowIconInset) val drawable = AdaptiveIconDrawable(ColorDrawable(colorAccent), fg) - bitmap = iconFactory.createBadgedIconBitmap(drawable).icon + val bubbleBitmapScale = FloatArray(1) + bitmap = iconFactory.getBubbleBitmap(drawable, bubbleBitmapScale) // Update dot path dotPath = PathParser.createPathFromPathData( res.getString(com.android.internal.R.string.config_icon_mask) ) - val scale = iconFactory.normalizer.getScale(iconView!!.iconDrawable) + val scale = bubbleBitmapScale[0] val radius = BadgedImageView.DEFAULT_PATH_SIZE / 2f val matrix = Matrix() matrix.setScale( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java index 1a61793eab87..a725e04d3f8a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java @@ -87,6 +87,7 @@ public class BubblePositioner { private int mExpandedViewLargeScreenWidth; private int mExpandedViewLargeScreenInsetClosestEdge; private int mExpandedViewLargeScreenInsetFurthestEdge; + private int mExpandedViewBubbleBarWidth; private int mOverflowWidth; private int mExpandedViewPadding; @@ -158,12 +159,13 @@ public class BubblePositioner { mBubbleOffscreenAmount = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen); mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation); + mExpandedViewBubbleBarWidth = Math.min( + res.getDimensionPixelSize(R.dimen.bubble_bar_expanded_view_width), + mPositionRect.width() - 2 * mExpandedViewPadding + ); if (mShowingInBubbleBar) { - mExpandedViewLargeScreenWidth = Math.min( - res.getDimensionPixelSize(R.dimen.bubble_bar_expanded_view_width), - mPositionRect.width() - 2 * mExpandedViewPadding - ); + mExpandedViewLargeScreenWidth = mExpandedViewBubbleBarWidth; } else if (mDeviceConfig.isSmallTablet()) { mExpandedViewLargeScreenWidth = (int) (bounds.width() * EXPANDED_VIEW_SMALL_TABLET_WIDTH_PERCENT); @@ -888,7 +890,7 @@ public class BubblePositioner { * How wide the expanded view should be when showing from the bubble bar. */ public int getExpandedViewWidthForBubbleBar(boolean isOverflow) { - return isOverflow ? mOverflowWidth : mExpandedViewLargeScreenWidth; + return isOverflow ? mOverflowWidth : mExpandedViewBubbleBarWidth; } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index 4e7f87c48a86..f1f49eda75b6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -633,8 +633,6 @@ public class BubbleStackView extends FrameLayout mMagneticTarget, mIndividualBubbleMagnetListener); - hideCurrentInputMethod(); - // Save the magnetized individual bubble so we can dispatch touch events to it. mMagnetizedObject = mExpandedAnimationController.getMagnetizedBubbleDraggingOut(); } else { @@ -671,6 +669,10 @@ public class BubbleStackView extends FrameLayout return; } + if (mPositioner.isImeVisible()) { + hideCurrentInputMethod(); + } + // Show the dismiss target, if we haven't already. mDismissView.show(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java index 89c038b4a26b..a6b858500dcb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java @@ -36,7 +36,7 @@ import android.view.ViewGroup; import androidx.annotation.Nullable; import com.android.internal.protolog.ProtoLog; -import com.android.wm.shell.Flags; +import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; import com.android.wm.shell.taskview.TaskView; /** @@ -108,8 +108,11 @@ public class BubbleTaskViewHelper { options.setPendingIntentBackgroundActivityStartMode( MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); final boolean isShortcutBubble = (mBubble.hasMetadataShortcutId() - || (mBubble.getShortcutInfo() != null && Flags.enableBubbleAnything())); - if (mBubble.isAppBubble()) { + || (mBubble.getShortcutInfo() != null + && BubbleAnythingFlagHelper.enableCreateAnyBubble())); + if (mBubble.getPreparingTransition() != null) { + mBubble.getPreparingTransition().surfaceCreated(); + } else if (mBubble.isAppBubble()) { Context context = mContext.createContextAsUser( mBubble.getUser(), Context.CONTEXT_RESTRICTED); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java new file mode 100644 index 000000000000..29fb1a23017c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java @@ -0,0 +1,518 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.bubbles; + +import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.view.View.INVISIBLE; +import static android.view.WindowManager.TRANSIT_CHANGE; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.TaskInfo; +import android.content.Context; +import android.graphics.Rect; +import android.os.IBinder; +import android.util.Slog; +import android.view.SurfaceControl; +import android.view.SurfaceView; +import android.view.View; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.launcher3.icons.BubbleIconFactory; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView; +import com.android.wm.shell.bubbles.bar.BubbleBarLayerView; +import com.android.wm.shell.taskview.TaskView; +import com.android.wm.shell.taskview.TaskViewRepository; +import com.android.wm.shell.taskview.TaskViewTaskController; +import com.android.wm.shell.taskview.TaskViewTransitions; +import com.android.wm.shell.transition.Transitions; + +import java.util.concurrent.Executor; + +/** + * Implements transition coordination for bubble operations. + */ +public class BubbleTransitions { + private static final String TAG = "BubbleTransitions"; + + /** + * Multiplier used to convert a view elevation to an "equivalent" shadow-radius. This is the + * same multiple used by skia and surface-outsets in WMS. + */ + private static final float ELEVATION_TO_RADIUS = 2; + + @NonNull final Transitions mTransitions; + @NonNull final ShellTaskOrganizer mTaskOrganizer; + @NonNull final TaskViewRepository mRepository; + @NonNull final Executor mMainExecutor; + @NonNull final BubbleData mBubbleData; + @NonNull final TaskViewTransitions mTaskViewTransitions; + @NonNull final Context mContext; + + BubbleTransitions(@NonNull Transitions transitions, @NonNull ShellTaskOrganizer organizer, + @NonNull TaskViewRepository repository, @NonNull BubbleData bubbleData, + @NonNull TaskViewTransitions taskViewTransitions, Context context) { + mTransitions = transitions; + mTaskOrganizer = organizer; + mRepository = repository; + mMainExecutor = transitions.getMainExecutor(); + mBubbleData = bubbleData; + mTaskViewTransitions = taskViewTransitions; + mContext = context; + } + + /** + * Starts a convert-to-bubble transition. + * + * @see ConvertToBubble + */ + public BubbleTransition startConvertToBubble(Bubble bubble, TaskInfo taskInfo, + BubbleExpandedViewManager expandedViewManager, BubbleTaskViewFactory factory, + BubblePositioner positioner, BubbleLogger logger, BubbleStackView stackView, + BubbleBarLayerView layerView, BubbleIconFactory iconFactory, + boolean inflateSync) { + ConvertToBubble convert = new ConvertToBubble(bubble, taskInfo, mContext, + expandedViewManager, factory, positioner, logger, stackView, layerView, iconFactory, + inflateSync); + return convert; + } + + /** + * Starts a convert-from-bubble transition. + * + * @see ConvertFromBubble + */ + public BubbleTransition startConvertFromBubble(Bubble bubble, + TaskInfo taskInfo) { + ConvertFromBubble convert = new ConvertFromBubble(bubble, taskInfo); + return convert; + } + + /** + * Plucks the task-surface out of an ancestor view while making the view invisible. This helper + * attempts to do this seamlessly (ie. view becomes invisible in sync with task reparent). + */ + private void pluck(SurfaceControl taskLeash, View fromView, SurfaceControl dest, + float destX, float destY, float cornerRadius, SurfaceControl.Transaction t, + Runnable onPlucked) { + SurfaceControl.Transaction pluckT = new SurfaceControl.Transaction(); + pluckT.reparent(taskLeash, dest); + t.reparent(taskLeash, dest); + pluckT.setPosition(taskLeash, destX, destY); + t.setPosition(taskLeash, destX, destY); + pluckT.show(taskLeash); + pluckT.setAlpha(taskLeash, 1.f); + float shadowRadius = fromView.getElevation() * ELEVATION_TO_RADIUS; + pluckT.setShadowRadius(taskLeash, shadowRadius); + pluckT.setCornerRadius(taskLeash, cornerRadius); + t.setShadowRadius(taskLeash, shadowRadius); + t.setCornerRadius(taskLeash, cornerRadius); + + // Need to remove the taskview AFTER applying the startTransaction because it isn't + // synchronized. + pluckT.addTransactionCommittedListener(mMainExecutor, onPlucked::run); + fromView.getViewRootImpl().applyTransactionOnDraw(pluckT); + fromView.setVisibility(INVISIBLE); + } + + /** + * Interface to a bubble-specific transition. Bubble transitions have a multi-step lifecycle + * in order to coordinate with the bubble view logic. These steps are communicated on this + * interface. + */ + interface BubbleTransition { + default void surfaceCreated() {} + default void continueExpand() {} + void skip(); + default void continueCollapse() {} + } + + /** + * BubbleTransition that coordinates the process of a non-bubble task becoming a bubble. The + * steps are as follows: + * + * 1. Start inflating the bubble view + * 2. Once inflated (but not-yet visible), tell WM to do the shell-transition. + * 3. Transition becomes ready, so notify Launcher + * 4. Launcher responds with showExpandedView which calls continueExpand() to make view visible + * 5. Surface is created which kicks off actual animation + * + * So, constructor -> onInflated -> startAnimation -> continueExpand -> surfaceCreated. + * + * continueExpand and surfaceCreated are set-up to happen in either order, though, to support + * UX/timing adjustments. + */ + @VisibleForTesting + class ConvertToBubble implements Transitions.TransitionHandler, BubbleTransition { + final BubbleBarLayerView mLayerView; + Bubble mBubble; + IBinder mTransition; + Transitions.TransitionFinishCallback mFinishCb; + WindowContainerTransaction mFinishWct = null; + final Rect mStartBounds = new Rect(); + SurfaceControl mSnapshot = null; + TaskInfo mTaskInfo; + boolean mFinishedExpand = false; + BubbleViewProvider mPriorBubble = null; + + private SurfaceControl.Transaction mFinishT; + private SurfaceControl mTaskLeash; + + ConvertToBubble(Bubble bubble, TaskInfo taskInfo, Context context, + BubbleExpandedViewManager expandedViewManager, BubbleTaskViewFactory factory, + BubblePositioner positioner, BubbleLogger logger, BubbleStackView stackView, + BubbleBarLayerView layerView, BubbleIconFactory iconFactory, boolean inflateSync) { + mBubble = bubble; + mTaskInfo = taskInfo; + mLayerView = layerView; + mBubble.setInflateSynchronously(inflateSync); + mBubble.setPreparingTransition(this); + mBubble.inflate( + this::onInflated, + context, + expandedViewManager, + factory, + positioner, + logger, + stackView, + layerView, + iconFactory, + false /* skipInflation */); + } + + @VisibleForTesting + void onInflated(Bubble b) { + if (b != mBubble) { + throw new IllegalArgumentException("inflate callback doesn't match bubble"); + } + final Rect launchBounds = new Rect(); + mLayerView.getExpandedViewRestBounds(launchBounds); + WindowContainerTransaction wct = new WindowContainerTransaction(); + if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) { + if (mTaskInfo.getParentTaskId() != INVALID_TASK_ID) { + wct.reparent(mTaskInfo.token, null, true); + } + } + + wct.setAlwaysOnTop(mTaskInfo.token, true); + wct.setWindowingMode(mTaskInfo.token, WINDOWING_MODE_MULTI_WINDOW); + wct.setBounds(mTaskInfo.token, launchBounds); + + final TaskView tv = b.getTaskView(); + tv.setSurfaceLifecycle(SurfaceView.SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT); + final TaskViewRepository.TaskViewState state = mRepository.byTaskView( + tv.getController()); + if (state != null) { + state.mVisible = true; + } + mTaskViewTransitions.enqueueExternal(tv.getController(), () -> { + mTransition = mTransitions.startTransition(TRANSIT_CHANGE, wct, this); + return mTransition; + }); + } + + @Override + public void skip() { + mBubble.setPreparingTransition(null); + mFinishCb.onTransitionFinished(mFinishWct); + mFinishCb = null; + } + + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @Nullable TransitionRequestInfo request) { + return null; + } + + @Override + public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + } + + @Override + public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted, + @NonNull SurfaceControl.Transaction finishTransaction) { + if (!aborted) return; + mTransition = null; + mTaskViewTransitions.onExternalDone(transition); + } + + @Override + public boolean startAnimation(@NonNull IBinder transition, + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + if (mTransition != transition) return false; + boolean found = false; + for (int i = 0; i < info.getChanges().size(); ++i) { + final TransitionInfo.Change chg = info.getChanges().get(i); + if (chg.getTaskInfo() == null) continue; + if (chg.getMode() != TRANSIT_CHANGE) continue; + if (!mTaskInfo.token.equals(chg.getTaskInfo().token)) continue; + mStartBounds.set(chg.getStartAbsBounds()); + // Converting a task into taskview, so treat as "new" + mFinishWct = new WindowContainerTransaction(); + mTaskInfo = chg.getTaskInfo(); + mFinishT = finishTransaction; + mTaskLeash = chg.getLeash(); + found = true; + mSnapshot = chg.getSnapshot(); + break; + } + if (!found) { + Slog.w(TAG, "Expected a TaskView conversion in this transition but didn't get " + + "one, cleaning up the task view"); + mBubble.getTaskView().getController().setTaskNotFound(); + mTaskViewTransitions.onExternalDone(transition); + return false; + } + mFinishCb = finishCallback; + + // Now update state (and talk to launcher) in parallel with snapshot stuff + mBubbleData.notificationEntryUpdated(mBubble, /* suppressFlyout= */ true, + /* showInShade= */ false); + + startTransaction.show(mSnapshot); + // Move snapshot to root so that it remains visible while task is moved to taskview + startTransaction.reparent(mSnapshot, info.getRoot(0).getLeash()); + startTransaction.setPosition(mSnapshot, + mStartBounds.left - info.getRoot(0).getOffset().x, + mStartBounds.top - info.getRoot(0).getOffset().y); + startTransaction.setLayer(mSnapshot, Integer.MAX_VALUE); + startTransaction.apply(); + + mTaskViewTransitions.onExternalDone(transition); + return true; + } + + @Override + public void continueExpand() { + mFinishedExpand = true; + final boolean animate = mLayerView.canExpandView(mBubble); + if (animate) { + mPriorBubble = mLayerView.prepareConvertedView(mBubble); + } + if (mPriorBubble != null) { + // TODO: an animation. For now though, just remove it. + final BubbleBarExpandedView priorView = mPriorBubble.getBubbleBarExpandedView(); + mLayerView.removeView(priorView); + mPriorBubble = null; + } + if (!animate || mBubble.getTaskView().getSurfaceControl() != null) { + playAnimation(animate); + } + } + + @Override + public void surfaceCreated() { + mMainExecutor.execute(() -> { + final TaskViewTaskController tvc = mBubble.getTaskView().getController(); + final TaskViewRepository.TaskViewState state = mRepository.byTaskView(tvc); + if (state == null) return; + state.mVisible = true; + if (mFinishedExpand) { + playAnimation(true /* animate */); + } + }); + } + + private void playAnimation(boolean animate) { + final TaskViewTaskController tv = mBubble.getTaskView().getController(); + final SurfaceControl.Transaction startT = new SurfaceControl.Transaction(); + mTaskViewTransitions.prepareOpenAnimation(tv, true /* new */, startT, mFinishT, + (ActivityManager.RunningTaskInfo) mTaskInfo, mTaskLeash, mFinishWct); + + if (mFinishWct.isEmpty()) { + mFinishWct = null; + } + + // Preparation is complete. + mBubble.setPreparingTransition(null); + + if (animate) { + mLayerView.animateConvert(startT, mStartBounds, mSnapshot, mTaskLeash, () -> { + mFinishCb.onTransitionFinished(mFinishWct); + mFinishCb = null; + }); + } else { + startT.apply(); + mFinishCb.onTransitionFinished(mFinishWct); + mFinishCb = null; + } + } + } + + /** + * BubbleTransition that coordinates the setup for moving a task out of a bubble. The actual + * animation is owned by the "receiver" of the task; however, because Bubbles uses TaskView, + * we need to do some extra coordination work to get the task surface out of the view + * "seamlessly". + * + * The process here looks like: + * 1. Send transition to WM for leaving bubbles mode + * 2. in startAnimation, set-up a "pluck" operation to pull the task surface out of taskview + * 3. Once "plucked", remove the view (calls continueCollapse when surfaces can be cleaned-up) + * 4. Then re-dispatch the transition animation so that the "receiver" can animate it. + * + * So, constructor -> startAnimation -> continueCollapse -> re-dispatch. + */ + @VisibleForTesting + class ConvertFromBubble implements Transitions.TransitionHandler, BubbleTransition { + @NonNull final Bubble mBubble; + IBinder mTransition; + TaskInfo mTaskInfo; + SurfaceControl mTaskLeash; + SurfaceControl mRootLeash; + + ConvertFromBubble(@NonNull Bubble bubble, TaskInfo taskInfo) { + mBubble = bubble; + mTaskInfo = taskInfo; + + mBubble.setPreparingTransition(this); + WindowContainerTransaction wct = new WindowContainerTransaction(); + WindowContainerToken token = mTaskInfo.getToken(); + wct.setWindowingMode(token, WINDOWING_MODE_UNDEFINED); + wct.setAlwaysOnTop(token, false); + mTaskOrganizer.setInterceptBackPressedOnTaskRoot(token, false); + mTaskViewTransitions.enqueueExternal( + mBubble.getTaskView().getController(), + () -> { + mTransition = mTransitions.startTransition(TRANSIT_CHANGE, wct, this); + return mTransition; + }); + } + + @Override + public void skip() { + mBubble.setPreparingTransition(null); + final TaskViewTaskController tv = + mBubble.getTaskView().getController(); + tv.notifyTaskRemovalStarted(tv.getTaskInfo()); + mTaskLeash = null; + } + + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @android.annotation.Nullable TransitionRequestInfo request) { + return null; + } + + @Override + public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + } + + @Override + public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted, + @NonNull SurfaceControl.Transaction finishTransaction) { + if (!aborted) return; + mTransition = null; + skip(); + mTaskViewTransitions.onExternalDone(transition); + } + + @Override + public boolean startAnimation(@NonNull IBinder transition, + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + if (mTransition != transition) return false; + + final TaskViewTaskController tv = + mBubble.getTaskView().getController(); + if (tv == null) { + mTaskViewTransitions.onExternalDone(transition); + return false; + } + + TransitionInfo.Change taskChg = null; + + boolean found = false; + for (int i = 0; i < info.getChanges().size(); ++i) { + final TransitionInfo.Change chg = info.getChanges().get(i); + if (chg.getTaskInfo() == null) continue; + if (chg.getMode() != TRANSIT_CHANGE) continue; + if (!mTaskInfo.token.equals(chg.getTaskInfo().token)) continue; + found = true; + mRepository.remove(tv); + taskChg = chg; + break; + } + + if (!found) { + Slog.w(TAG, "Expected a TaskView conversion in this transition but didn't get " + + "one, cleaning up the task view"); + tv.setTaskNotFound(); + skip(); + mTaskViewTransitions.onExternalDone(transition); + return false; + } + + mTaskLeash = taskChg.getLeash(); + mRootLeash = info.getRoot(0).getLeash(); + + SurfaceControl dest = + mBubble.getBubbleBarExpandedView().getViewRootImpl().getSurfaceControl(); + final Runnable onPlucked = () -> { + // Need to remove the taskview AFTER applying the startTransaction because + // it isn't synchronized. + tv.notifyTaskRemovalStarted(tv.getTaskInfo()); + // Unset after removeView so it can be used to pick a different animation. + mBubble.setPreparingTransition(null); + mBubbleData.setExpanded(false /* expanded */); + }; + if (dest != null) { + pluck(mTaskLeash, mBubble.getBubbleBarExpandedView(), dest, + taskChg.getStartAbsBounds().left - info.getRoot(0).getOffset().x, + taskChg.getStartAbsBounds().top - info.getRoot(0).getOffset().y, + mBubble.getBubbleBarExpandedView().getCornerRadius(), startTransaction, + onPlucked); + mBubble.getBubbleBarExpandedView().post(() -> mTransitions.dispatchTransition( + mTransition, info, startTransaction, finishTransaction, finishCallback, + null)); + } else { + onPlucked.run(); + mTransitions.dispatchTransition(mTransition, info, startTransaction, + finishTransaction, finishCallback, null); + } + + mTaskViewTransitions.onExternalDone(transition); + return true; + } + + @Override + public void continueCollapse() { + mBubble.cleanupTaskView(); + if (mTaskLeash == null) return; + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + t.reparent(mTaskLeash, mRootLeash); + t.apply(); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java index 62895fe7c7cc..4297fac0f6a8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -36,6 +36,7 @@ import android.window.ScreenCapture.ScreenshotHardwareBuffer; import android.window.ScreenCapture.SynchronousScreenCaptureListener; import androidx.annotation.IntDef; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.wm.shell.shared.annotations.ExternalThread; @@ -330,6 +331,18 @@ public interface Bubbles { * Does not result in a state change. */ void animateBubbleBarLocation(BubbleBarLocation location); + + /** + * Called when an application icon is being dragged over the Bubble Bar drop zone. + * The location of the Bubble Bar is provided as an argument. + */ + void onDragItemOverBubbleBarDragZone(@NonNull BubbleBarLocation location); + + /** + * Called when an application icon is being dragged outside the Bubble Bar drop zone. + * Always called after {@link #onDragItemOverBubbleBarDragZone(BubbleBarLocation)} + */ + void onItemDraggedOutsideBubbleBarDropZone(); } /** Listener to find out about stack expansion / collapse events. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl index eb907dbb6597..9fc769f562a9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl @@ -33,4 +33,16 @@ oneway interface IBubblesListener { * Does not result in a state change. */ void animateBubbleBarLocation(in BubbleBarLocation location); + + /** + * Called when an application icon is being dragged over the Bubble Bar drop zone. + * The location of the Bubble Bar is provided as an argument. + */ + void onDragItemOverBubbleBarDragZone(in BubbleBarLocation location); + + /** + * Called when an application icon is being dragged outside the Bubble Bar drop zone. + * Always called after {@link #onDragItemOverBubbleBarDragZone(BubbleBarLocation)} + */ + void onItemDraggedOutsideBubbleBarDropZone(); }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java index de6d1f6c8852..52f20646fb4a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java @@ -36,17 +36,21 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; +import android.annotation.NonNull; import android.content.Context; import android.graphics.Point; import android.graphics.Rect; import android.util.Log; import android.util.Size; +import android.view.SurfaceControl; import android.widget.FrameLayout; import androidx.annotation.Nullable; import com.android.app.animation.Interpolators; import com.android.wm.shell.R; +import com.android.wm.shell.animation.SizeChangeAnimation; +import com.android.wm.shell.bubbles.Bubble; import com.android.wm.shell.bubbles.BubbleOverflow; import com.android.wm.shell.bubbles.BubblePositioner; import com.android.wm.shell.bubbles.BubbleViewProvider; @@ -571,6 +575,49 @@ public class BubbleBarAnimationHelper { } /** + * Animates converting of a non-bubble task into an expanded bubble view. + */ + public void animateConvert(BubbleViewProvider expandedBubble, + @NonNull SurfaceControl.Transaction startT, + @NonNull Rect origBounds, + @NonNull SurfaceControl snapshot, + @NonNull SurfaceControl taskLeash, + @Nullable Runnable afterAnimation) { + mExpandedBubble = expandedBubble; + final BubbleBarExpandedView bbev = getExpandedView(); + if (bbev == null) { + return; + } + + bbev.setTaskViewAlpha(1f); + SurfaceControl tvSf = ((Bubble) mExpandedBubble).getTaskView().getSurfaceControl(); + + final Size size = getExpandedViewSize(); + Point position = getExpandedViewRestPosition(size); + + final SizeChangeAnimation sca = + new SizeChangeAnimation( + new Rect(origBounds.left - position.x, origBounds.top - position.y, + origBounds.right - position.x, origBounds.bottom - position.y), + new Rect(0, 0, size.getWidth(), size.getHeight())); + sca.initialize(bbev, taskLeash, snapshot, startT); + + Animator a = sca.buildViewAnimator(bbev, tvSf, snapshot, /* onFinish */ (va) -> { + updateExpandedView(bbev); + snapshot.release(); + bbev.setSurfaceZOrderedOnTop(false); + bbev.setAnimating(false); + if (afterAnimation != null) { + afterAnimation.run(); + } + }); + + bbev.setSurfaceZOrderedOnTop(true); + a.setDuration(EXPANDED_VIEW_ANIMATE_TO_REST_DURATION); + a.start(); + } + + /** * Cancel current animations */ public void cancelAnimations() { @@ -627,6 +674,13 @@ public class BubbleBarAnimationHelper { bbev.maybeShowOverflow(); } + void getExpandedViewRestBounds(Rect out) { + final int width = mPositioner.getExpandedViewWidthForBubbleBar(false /* overflow */); + final int height = mPositioner.getExpandedViewHeightForBubbleBar(false /* overflow */); + Point position = getExpandedViewRestPosition(new Size(width, height)); + out.set(position.x, position.y, position.x + width, position.y + height); + } + private Point getExpandedViewRestPosition(Size size) { final int padding = mPositioner.getBubbleBarExpandedViewPadding(); Point point = new Point(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index eaa0bd250fc4..f3f8d6f96a42 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -28,6 +28,7 @@ import android.graphics.Rect; import android.graphics.Region; import android.graphics.drawable.ColorDrawable; import android.view.Gravity; +import android.view.SurfaceControl; import android.view.TouchDelegate; import android.view.View; import android.view.ViewTreeObserver; @@ -174,14 +175,34 @@ public class BubbleBarLayerView extends FrameLayout /** Shows the expanded view of the provided bubble. */ public void showExpandedView(BubbleViewProvider b) { - BubbleBarExpandedView expandedView = b.getBubbleBarExpandedView(); - if (expandedView == null) { - return; - } + if (!canExpandView(b)) return; + animateExpand(prepareExpandedView(b)); + } + + /** + * @return whether it's possible to expand {@param b} right now. This is {@code false} if + * the bubble has no view or if the bubble is already showing. + */ + public boolean canExpandView(BubbleViewProvider b) { + if (b.getBubbleBarExpandedView() == null) return false; if (mExpandedBubble != null && mIsExpanded && b.getKey().equals(mExpandedBubble.getKey())) { - // Already showing this bubble, skip animating - return; + // Already showing this bubble so can't expand it. + return false; + } + return true; + } + + /** + * Prepares the expanded view of the provided bubble to be shown. This includes removing any + * stale content and cancelling any related animations. + * + * @return previous open bubble if there was one. + */ + private BubbleViewProvider prepareExpandedView(BubbleViewProvider b) { + if (!canExpandView(b)) { + throw new IllegalStateException("Can't prepare expand. Check canExpandView(b) first."); } + BubbleBarExpandedView expandedView = b.getBubbleBarExpandedView(); BubbleViewProvider previousBubble = null; if (mExpandedBubble != null && !b.getKey().equals(mExpandedBubble.getKey())) { if (mIsExpanded && mExpandedBubble.getBubbleBarExpandedView() != null) { @@ -251,7 +272,20 @@ public class BubbleBarLayerView extends FrameLayout mIsExpanded = true; mBubbleController.getSysuiProxy().onStackExpandChanged(true); + showScrim(true); + return previousBubble; + } + /** + * Performs an animation to open a bubble with content that is not already visible. + * + * @param previousBubble If non-null, this is a bubble that is already showing before the new + * bubble is expanded. + */ + public void animateExpand(BubbleViewProvider previousBubble) { + if (!mIsExpanded || mExpandedBubble == null) { + throw new IllegalStateException("Can't animateExpand without expnaded state"); + } final Runnable afterAnimation = () -> { if (mExpandedView == null) return; // Touch delegate for the menu @@ -274,14 +308,57 @@ public class BubbleBarLayerView extends FrameLayout } else { mAnimationHelper.animateExpansion(mExpandedBubble, afterAnimation); } + } - showScrim(true); + /** + * Like {@link #prepareExpandedView} but also makes the current expanded bubble visible + * immediately so it gets a surface that can be animated. Since the surface may not be ready + * yet, this keeps the TaskView alpha=0. + */ + public BubbleViewProvider prepareConvertedView(BubbleViewProvider b) { + final BubbleViewProvider prior = prepareExpandedView(b); + + final BubbleBarExpandedView bbev = mExpandedBubble.getBubbleBarExpandedView(); + if (bbev != null) { + updateExpandedView(); + bbev.setAnimating(true); + bbev.setContentVisibility(true); + bbev.setSurfaceZOrderedOnTop(true); + bbev.setTaskViewAlpha(0.f); + bbev.setVisibility(VISIBLE); + } + + return prior; + } + + /** + * Starts and animates a conversion-from transition. + * + * @param startT A transaction with first-frame work. this *will* be applied here! + */ + public void animateConvert(@NonNull SurfaceControl.Transaction startT, + @NonNull Rect startBounds, @NonNull SurfaceControl snapshot, SurfaceControl taskLeash, + Runnable animFinish) { + if (!mIsExpanded || mExpandedBubble == null) { + throw new IllegalStateException("Can't animateExpand without expanded state"); + } + mAnimationHelper.animateConvert(mExpandedBubble, startT, startBounds, snapshot, taskLeash, + animFinish); + } + + /** + * Populates {@param out} with the rest bounds of an expanded bubble. + */ + public void getExpandedViewRestBounds(Rect out) { + mAnimationHelper.getExpandedViewRestBounds(out); } /** Removes the given {@code bubble}. */ public void removeBubble(Bubble bubble, Runnable endAction) { + final boolean inTransition = bubble.getPreparingTransition() != null; Runnable cleanUp = () -> { - bubble.cleanupViews(); + // The transition is already managing the task/wm state. + bubble.cleanupViews(!inTransition); endAction.run(); }; if (mBubbleData.getBubbles().isEmpty()) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt index 4cd2fd04d3cf..ff3e65a247ae 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt @@ -15,16 +15,21 @@ */ package com.android.wm.shell.common +import android.annotation.UserIdInt import android.app.PendingIntent import android.content.ComponentName import android.content.Context import android.content.pm.LauncherApps import android.content.pm.PackageManager +import android.content.pm.PackageManager.Property import android.os.UserHandle import android.view.WindowManager.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI import com.android.internal.protolog.ProtoLog import com.android.wm.shell.R import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL +import com.android.wm.shell.sysui.ShellCommandHandler +import com.android.wm.shell.sysui.ShellInit +import java.io.PrintWriter import java.util.Arrays /** @@ -35,12 +40,23 @@ class MultiInstanceHelper @JvmOverloads constructor( private val packageManager: PackageManager, private val staticAppsSupportingMultiInstance: Array<String> = context.resources .getStringArray(R.array.config_appsSupportMultiInstancesSplit), - private val supportsMultiInstanceProperty: Boolean) { + shellInit: ShellInit, + private val shellCommandHandler: ShellCommandHandler, + private val supportsMultiInstanceProperty: Boolean +) : ShellCommandHandler.ShellCommandActionHandler { + + init { + shellInit.addInitCallback(this::onInit, this) + } + + private fun onInit() { + shellCommandHandler.addCommandCallback("multi-instance", this, this) + } /** * Returns whether a specific component desires to be launched in multiple instances. */ - fun supportsMultiInstanceSplit(componentName: ComponentName?): Boolean { + fun supportsMultiInstanceSplit(componentName: ComponentName?, @UserIdInt userId: Int): Boolean { if (componentName == null || componentName.packageName == null) { // TODO(b/262864589): Handle empty component case return false @@ -63,8 +79,9 @@ class MultiInstanceHelper @JvmOverloads constructor( // Check the activity property first try { - val activityProp = packageManager.getProperty( - PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, componentName) + val activityProp = packageManager.getPropertyAsUser( + PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, componentName.packageName, + componentName.className, userId) // If the above call doesn't throw a NameNotFoundException, then the activity property // should override the application property value if (activityProp.isBoolean) { @@ -80,8 +97,9 @@ class MultiInstanceHelper @JvmOverloads constructor( // Check the application property otherwise try { - val appProp = packageManager.getProperty( - PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, packageName) + val appProp = packageManager.getPropertyAsUser( + PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, packageName, null /* className */, + userId) if (appProp.isBoolean) { ProtoLog.v(WM_SHELL, "application=%s supports multi-instance", packageName) return appProp.boolean @@ -96,6 +114,66 @@ class MultiInstanceHelper @JvmOverloads constructor( return false } + override fun onShellCommand(args: Array<out String>?, pw: PrintWriter?): Boolean { + if (pw == null || args == null || args.isEmpty()) { + return false + } + when (args[0]) { + "list" -> return dumpSupportedApps(pw) + } + return false + } + + override fun printShellCommandHelp(pw: PrintWriter, prefix: String) { + pw.println("${prefix}list") + pw.println("$prefix Lists all the packages that support the multiinstance property") + } + + /** + * Dumps the static allowlist and list of apps that have the declared property in the manifest. + */ + private fun dumpSupportedApps(pw: PrintWriter): Boolean { + pw.println("Static allow list (for all users):") + staticAppsSupportingMultiInstance.forEach { pkg -> + pw.println(" $pkg") + } + + // TODO(b/391693747): Dump this per-user once PM allows us to query properties + // for non-calling users + val apps = packageManager.queryApplicationProperty( + PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI) + val activities = packageManager.queryActivityProperty( + PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI) + val appsWithProperty = (apps + activities) + .sortedWith(object : Comparator<Property?> { + override fun compare(o1: Property?, o2: Property?): Int { + if (o1?.packageName != o2?.packageName) { + return o1?.packageName!!.compareTo(o2?.packageName!!) + } else { + if (o1?.className != null) { + return o1.className!!.compareTo(o2?.className!!) + } else if (o2?.className != null) { + return -o2.className!!.compareTo(o1?.className!!) + } + return 0 + } + } + }) + if (appsWithProperty.isNotEmpty()) { + pw.println("Apps (User ${context.userId}):") + appsWithProperty.forEach { prop -> + if (prop.isBoolean && prop.boolean) { + if (prop.className != null) { + pw.println(" ${prop.packageName}/${prop.className}") + } else { + pw.println(" ${prop.packageName}") + } + } + } + } + return true + } + companion object { /** Returns the component from a PendingIntent */ @JvmStatic diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/UserProfileContexts.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/UserProfileContexts.kt new file mode 100644 index 000000000000..0577f9e625ca --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/UserProfileContexts.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common + +import android.app.ActivityManager +import android.content.Context +import android.content.pm.UserInfo +import android.os.UserHandle +import android.os.UserManager +import android.util.SparseArray +import com.android.wm.shell.sysui.ShellController +import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.sysui.UserChangeListener + +/** Creates and manages contexts for all the profiles of the current user. */ +class UserProfileContexts( + private val baseContext: Context, + private val shellController: ShellController, + shellInit: ShellInit, +) { + // Contexts for all the profiles of the current user. + private val currentProfilesContext = SparseArray<Context>() + + lateinit var userContext: Context + private set + + init { + shellInit.addInitCallback(this::onInit, this) + } + + private fun onInit() { + shellController.addUserChangeListener( + object : UserChangeListener { + override fun onUserChanged(newUserId: Int, userContext: Context) { + currentProfilesContext.clear() + this@UserProfileContexts.userContext = userContext + currentProfilesContext.put(newUserId, userContext) + } + + override fun onUserProfilesChanged(profiles: List<UserInfo>) { + updateProfilesContexts(profiles) + } + } + ) + val defaultUserId = ActivityManager.getCurrentUser() + val userManager = baseContext.getSystemService(UserManager::class.java) + userContext = baseContext.createContextAsUser(UserHandle.of(defaultUserId), /* flags= */ 0) + updateProfilesContexts(userManager.getProfiles(defaultUserId)) + } + + private fun updateProfilesContexts(profiles: List<UserInfo>) { + for (profile in profiles) { + if (profile.id in currentProfilesContext) continue + val profileContext = baseContext.createContextAsUser(profile.userHandle, /* flags= */ 0) + currentProfilesContext.put(profile.id, profileContext) + } + val profilesToRemove = buildList<Int> { + for (i in 0..<currentProfilesContext.size()) { + val userId = currentProfilesContext.keyAt(i) + if (profiles.none { it.id == userId }) { + add(userId) + } + } + } + profilesToRemove.forEach { currentProfilesContext.remove(it) } + } + + operator fun get(userId: Int): Context? = currentProfilesContext.get(userId) +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/AppCompatUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/AppCompatUtils.kt deleted file mode 100644 index d1dcc9b1d591..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/AppCompatUtils.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@file:JvmName("AppCompatUtils") - -package com.android.wm.shell.compatui - -import android.app.ActivityManager.RunningTaskInfo -import android.content.Context -import com.android.internal.R - -// TODO(b/347289970): Consider replacing with API -/** - * If the top activity should be exempt from desktop windowing and forced back to fullscreen. - * Currently includes all system ui activities and modal dialogs. However if the top activity is not - * being displayed, regardless of its configuration, we will not exempt it as to remain in the - * desktop windowing environment. - */ -fun isTopActivityExemptFromDesktopWindowing(context: Context, task: RunningTaskInfo) = - (isSystemUiTask(context, task) || isTransparentTask(task)) - && !task.isTopActivityNoDisplay - -/** - * Returns true if all activities in a tasks stack are transparent. If there are no activities will - * return false. - */ -fun isTransparentTask(task: RunningTaskInfo): Boolean = task.isActivityStackTransparent - && task.numActivities > 0 - -private fun isSystemUiTask(context: Context, task: RunningTaskInfo): Boolean { - val sysUiPackageName: String = - context.resources.getString(R.string.config_systemUi) - return task.baseActivity?.packageName == sysUiPackageName -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 84042591ad1b..e0a829df79ad 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -43,6 +43,8 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.activityembedding.ActivityEmbeddingController; +import com.android.wm.shell.appzoomout.AppZoomOut; +import com.android.wm.shell.appzoomout.AppZoomOutController; import com.android.wm.shell.back.BackAnimation; import com.android.wm.shell.back.BackAnimationBackground; import com.android.wm.shell.back.BackAnimationController; @@ -112,9 +114,8 @@ import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.shared.annotations.ShellAnimationThread; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.shared.annotations.ShellSplashscreenThread; +import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; -import com.android.wm.shell.appzoomout.AppZoomOut; -import com.android.wm.shell.appzoomout.AppZoomOutController; import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.startingsurface.StartingSurface; @@ -258,6 +259,12 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides + static DesktopModeCompatPolicy provideDesktopModeCompatPolicy(Context context) { + return new DesktopModeCompatPolicy(context); + } + + @WMSingleton + @Provides static Optional<CompatUIHandler> provideCompatUIController( Context context, ShellInit shellInit, @@ -410,9 +417,13 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides - static MultiInstanceHelper provideMultiInstanceHelper(Context context) { + static MultiInstanceHelper provideMultiInstanceHelper( + Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler + ) { return new MultiInstanceHelper(context, context.getPackageManager(), - Flags.supportsMultiInstanceSystemUi()); + shellInit, shellCommandHandler, Flags.supportsMultiInstanceSystemUi()); } // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index fdfaa90ac8b9..6657c9e4b9a9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -16,6 +16,7 @@ package com.android.wm.shell.dagger; +import static android.window.DesktopModeFlags.ENABLE_DESKTOP_SYSTEM_DIALOGS_TRANSITIONS; import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_ENTER_TRANSITIONS; import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_ENTER_TRANSITIONS_BUGFIX; import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY; @@ -37,6 +38,7 @@ import android.os.UserManager; import android.view.Choreographer; import android.view.IWindowManager; import android.view.WindowManager; +import android.window.DesktopModeFlags; import androidx.annotation.OptIn; @@ -70,6 +72,7 @@ import com.android.wm.shell.common.MultiInstanceHelper; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TaskStackListenerImpl; +import com.android.wm.shell.common.UserProfileContexts; import com.android.wm.shell.common.split.SplitState; import com.android.wm.shell.compatui.letterbox.LetterboxCommandHandler; import com.android.wm.shell.compatui.letterbox.LetterboxTransitionObserver; @@ -108,6 +111,8 @@ import com.android.wm.shell.desktopmode.education.AppToWebEducationController; import com.android.wm.shell.desktopmode.education.AppToWebEducationFilter; import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository; import com.android.wm.shell.desktopmode.education.data.AppToWebEducationDatastoreRepository; +import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer; +import com.android.wm.shell.desktopmode.multidesks.RootTaskDesksOrganizer; import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository; import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer; import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializerImpl; @@ -129,6 +134,7 @@ import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.shared.annotations.ShellAnimationThread; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.sysui.ShellCommandHandler; @@ -395,6 +401,7 @@ public abstract class WMShellModule { ShellTaskOrganizer shellTaskOrganizer, Optional<DesktopUserRepositories> desktopUserRepositories, Optional<DesktopTasksController> desktopTasksController, + DesktopModeLoggerTransitionObserver desktopModeLoggerTransitionObserver, LaunchAdjacentController launchAdjacentController, WindowDecorViewModel windowDecorViewModel, Optional<TaskChangeListener> taskChangeListener) { @@ -407,6 +414,7 @@ public abstract class WMShellModule { shellTaskOrganizer, desktopUserRepositories, desktopTasksController, + desktopModeLoggerTransitionObserver, launchAdjacentController, windowDecorViewModel, taskChangeListener); @@ -703,6 +711,16 @@ public abstract class WMShellModule { @WMSingleton @Provides + static DesksOrganizer provideDesksOrganizer( + @NonNull ShellInit shellInit, + @NonNull ShellCommandHandler shellCommandHandler, + @NonNull ShellTaskOrganizer shellTaskOrganizer + ) { + return new RootTaskDesksOrganizer(shellInit, shellCommandHandler, shellTaskOrganizer); + } + + @WMSingleton + @Provides @DynamicOverride static DesktopTasksController provideDesktopTasksController( Context context, @@ -741,7 +759,10 @@ public abstract class WMShellModule { DesktopTilingDecorViewModel desktopTilingDecorViewModel, DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider, Optional<BubbleController> bubbleController, - OverviewToDesktopTransitionObserver overviewToDesktopTransitionObserver) { + OverviewToDesktopTransitionObserver overviewToDesktopTransitionObserver, + DesksOrganizer desksOrganizer, + UserProfileContexts userProfileContexts, + DesktopModeCompatPolicy desktopModeCompatPolicy) { return new DesktopTasksController( context, shellInit, @@ -775,7 +796,10 @@ public abstract class WMShellModule { desktopTilingDecorViewModel, desktopWallpaperActivityTokenProvider, bubbleController, - overviewToDesktopTransitionObserver); + overviewToDesktopTransitionObserver, + desksOrganizer, + userProfileContexts, + desktopModeCompatPolicy); } @WMSingleton @@ -792,7 +816,9 @@ public abstract class WMShellModule { ReturnToDragStartAnimator returnToDragStartAnimator, @DynamicOverride DesktopUserRepositories desktopUserRepositories, DesktopModeEventLogger desktopModeEventLogger, - WindowDecorTaskResourceLoader windowDecorTaskResourceLoader) { + WindowDecorTaskResourceLoader windowDecorTaskResourceLoader, + FocusTransitionObserver focusTransitionObserver, + @ShellMainThread ShellExecutor mainExecutor) { return new DesktopTilingDecorViewModel( context, mainDispatcher, @@ -806,7 +832,9 @@ public abstract class WMShellModule { returnToDragStartAnimator, desktopUserRepositories, desktopModeEventLogger, - windowDecorTaskResourceLoader + windowDecorTaskResourceLoader, + focusTransitionObserver, + mainExecutor ); } @@ -907,7 +935,7 @@ public abstract class WMShellModule { if (DesktopModeStatus.canEnterDesktopMode(context) && useKeyGestureEventHandler() && manageKeyGestures() && (Flags.enableMoveToNextDisplayShortcut() - || Flags.enableTaskResizingKeyboardShortcuts())) { + || DesktopModeFlags.ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS.isTrue())) { return Optional.of(new DesktopModeKeyGestureHandler(context, desktopModeWindowDecorViewModel, desktopTasksController, inputManager, shellTaskOrganizer, focusTransitionObserver, @@ -953,7 +981,8 @@ public abstract class WMShellModule { DesktopModeEventLogger desktopModeEventLogger, DesktopModeUiEventLogger desktopModeUiEventLogger, WindowDecorTaskResourceLoader taskResourceLoader, - RecentsTransitionHandler recentsTransitionHandler + RecentsTransitionHandler recentsTransitionHandler, + DesktopModeCompatPolicy desktopModeCompatPolicy ) { if (!DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)) { return Optional.empty(); @@ -969,7 +998,7 @@ public abstract class WMShellModule { desktopTasksLimiter, appHandleEducationController, appToWebEducationController, windowDecorCaptionHandleRepository, activityOrientationChangeHandler, focusTransitionObserver, desktopModeEventLogger, desktopModeUiEventLogger, - taskResourceLoader, recentsTransitionHandler)); + taskResourceLoader, recentsTransitionHandler, desktopModeCompatPolicy)); } @WMSingleton @@ -977,9 +1006,10 @@ public abstract class WMShellModule { static WindowDecorTaskResourceLoader provideWindowDecorTaskResourceLoader( @NonNull Context context, @NonNull ShellInit shellInit, @NonNull ShellController shellController, - @NonNull ShellCommandHandler shellCommandHandler) { + @NonNull ShellCommandHandler shellCommandHandler, + @NonNull UserProfileContexts userProfileContexts) { return new WindowDecorTaskResourceLoader(context, shellInit, shellController, - shellCommandHandler); + shellCommandHandler, userProfileContexts); } @WMSingleton @@ -990,16 +1020,17 @@ public abstract class WMShellModule { @ShellAnimationThread ShellExecutor animExecutor, ShellInit shellInit, Transitions transitions, - @DynamicOverride DesktopUserRepositories desktopUserRepositories) { + @DynamicOverride DesktopUserRepositories desktopUserRepositories, + DesktopModeCompatPolicy desktopModeCompatPolicy) { if (!DesktopModeStatus.canEnterDesktopMode(context) || !ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue() - || !Flags.enableDesktopSystemDialogsTransitions()) { + || !ENABLE_DESKTOP_SYSTEM_DIALOGS_TRANSITIONS.isTrue()) { return Optional.empty(); } return Optional.of( new SystemModalsTransitionHandler( context, mainExecutor, animExecutor, shellInit, transitions, - desktopUserRepositories)); + desktopUserRepositories, desktopModeCompatPolicy)); } @WMSingleton @@ -1183,10 +1214,12 @@ public abstract class WMShellModule { Transitions transitions, DisplayController displayController, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, - IWindowManager windowManager + IWindowManager windowManager, + Optional<DesktopUserRepositories> desktopUserRepositories, + Optional<DesktopTasksController> desktopTasksController, + ShellTaskOrganizer shellTaskOrganizer ) { - if (!DesktopModeStatus.canEnterDesktopMode(context) - || !Flags.enableDisplayWindowingModeSwitching()) { + if (!DesktopModeStatus.canEnterDesktopMode(context)) { return Optional.empty(); } return Optional.of( @@ -1196,7 +1229,10 @@ public abstract class WMShellModule { transitions, displayController, rootTaskDisplayAreaOrganizer, - windowManager)); + windowManager, + desktopUserRepositories.get(), + desktopTasksController.get(), + shellTaskOrganizer)); } @WMSingleton @@ -1406,4 +1442,14 @@ public abstract class WMShellModule { Transitions transitions, ShellInit shellInit) { return new OverviewToDesktopTransitionObserver(transitions, shellInit); } + + @WMSingleton + @Provides + static UserProfileContexts provideUserProfilesContexts( + Context context, + ShellController shellController, + ShellInit shellInit) { + return new UserProfileContexts(context, shellController, shellInit); + } + } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java index 793bdf0b5614..413300612f7d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java @@ -159,11 +159,12 @@ public abstract class Pip2Module { PipUiEventLogger pipUiEventLogger, PipTaskListener pipTaskListener, @NonNull PipTransitionState pipTransitionState, + @NonNull PipDisplayLayoutState pipDisplayLayoutState, @ShellMainThread ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler) { return new PhonePipMenuController(context, pipBoundsState, pipMediaController, - systemWindows, pipUiEventLogger, pipTaskListener, pipTransitionState, mainExecutor, - mainHandler); + systemWindows, pipUiEventLogger, pipTaskListener, pipTransitionState, + pipDisplayLayoutState, mainExecutor, mainHandler); } @@ -178,6 +179,8 @@ public abstract class Pip2Module { @NonNull PipTransitionState pipTransitionState, @NonNull PipScheduler pipScheduler, @NonNull SizeSpecSource sizeSpecSource, + @NonNull PipDisplayLayoutState pipDisplayLayoutState, + DisplayController displayController, PipMotionHelper pipMotionHelper, FloatingContentCoordinator floatingContentCoordinator, PipUiEventLogger pipUiEventLogger, @@ -185,8 +188,9 @@ public abstract class Pip2Module { Optional<PipPerfHintController> pipPerfHintControllerOptional) { return new PipTouchHandler(context, shellInit, shellCommandHandler, menuPhoneController, pipBoundsAlgorithm, pipBoundsState, pipTransitionState, pipScheduler, - sizeSpecSource, pipMotionHelper, floatingContentCoordinator, pipUiEventLogger, - mainExecutor, pipPerfHintControllerOptional); + sizeSpecSource, pipDisplayLayoutState, displayController, pipMotionHelper, + floatingContentCoordinator, pipUiEventLogger, mainExecutor, + pipPerfHintControllerOptional); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt index 43e8d2a30930..6f455df6cfec 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt @@ -16,7 +16,10 @@ package com.android.wm.shell.desktopmode +import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED +import android.app.WindowConfiguration.windowingModeToString import android.content.Context import android.provider.Settings import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS @@ -24,9 +27,15 @@ import android.view.Display.DEFAULT_DISPLAY import android.view.IWindowManager import android.view.WindowManager.TRANSIT_CHANGE import android.window.WindowContainerTransaction +import com.android.internal.protolog.ProtoLog +import com.android.window.flags.Flags import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener +import com.android.wm.shell.desktopmode.multidesks.OnDeskRemovedListener +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions @@ -38,7 +47,13 @@ class DesktopDisplayEventHandler( private val displayController: DisplayController, private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, private val windowManager: IWindowManager, -) : OnDisplaysChangedListener { + private val desktopUserRepositories: DesktopUserRepositories, + private val desktopTasksController: DesktopTasksController, + private val shellTaskOrganizer: ShellTaskOrganizer, +) : OnDisplaysChangedListener, OnDeskRemovedListener { + + private val desktopRepository: DesktopRepository + get() = desktopUserRepositories.current init { shellInit.addInitCallback({ onInit() }, this) @@ -46,23 +61,48 @@ class DesktopDisplayEventHandler( private fun onInit() { displayController.addDisplayWindowListener(this) + + if (Flags.enableMultipleDesktopsBackend()) { + desktopTasksController.onDeskRemovedListener = this + } } override fun onDisplayAdded(displayId: Int) { - if (displayId == DEFAULT_DISPLAY) { + if (displayId != DEFAULT_DISPLAY) { + refreshDisplayWindowingMode() + } + + if (!supportsDesks(displayId)) { + logV("Display #$displayId does not support desks") return } - refreshDisplayWindowingMode() + logV("Creating new desk in new display#$displayId") + // TODO: b/362720497 - when SystemUI crashes with a freeform task open for any reason, the + // task is recreated and received in [FreeformTaskListener] before this display callback + // is invoked, which results in the repository trying to add the task to a desk before the + // desk has been recreated here, which may result in a crash-loop if the repository is + // checking that the desk exists before adding a task to it. See b/391984373. + desktopTasksController.createDesk(displayId) } override fun onDisplayRemoved(displayId: Int) { - if (displayId == DEFAULT_DISPLAY) { - return + if (displayId != DEFAULT_DISPLAY) { + refreshDisplayWindowingMode() + } + + // TODO: b/362720497 - move desks in closing display to the remaining desk. + } + + override fun onDeskRemoved(lastDisplayId: Int, deskId: Int) { + val remainingDesks = desktopRepository.getNumberOfDesks(lastDisplayId) + if (remainingDesks == 0) { + logV("All desks removed from display#$lastDisplayId, creating empty desk") + desktopTasksController.createDesk(lastDisplayId) } - refreshDisplayWindowingMode() } private fun refreshDisplayWindowingMode() { + if (!Flags.enableDisplayWindowingModeSwitching()) return // TODO: b/375319538 - Replace the check with a DisplayManager API once it's available. val isExtendedDisplayEnabled = 0 != @@ -89,13 +129,46 @@ class DesktopDisplayEventHandler( } val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY) requireNotNull(tdaInfo) { "DisplayAreaInfo of DEFAULT_DISPLAY must be non-null." } - if (tdaInfo.configuration.windowConfiguration.windowingMode == targetDisplayWindowingMode) { + val currentDisplayWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode + if (currentDisplayWindowingMode == targetDisplayWindowingMode) { // Already in the target mode. return } + logV( + "As an external display is connected, changing default display's windowing mode from" + + " ${windowingModeToString(currentDisplayWindowingMode)}" + + " to ${windowingModeToString(targetDisplayWindowingMode)}" + ) + val wct = WindowContainerTransaction() wct.setWindowingMode(tdaInfo.token, targetDisplayWindowingMode) + shellTaskOrganizer + .getRunningTasks(DEFAULT_DISPLAY) + .filter { it.activityType == ACTIVITY_TYPE_STANDARD } + .forEach { + // TODO: b/391965153 - Reconsider the logic under multi-desk window hierarchy + when (it.windowingMode) { + currentDisplayWindowingMode -> { + wct.setWindowingMode(it.token, currentDisplayWindowingMode) + } + targetDisplayWindowingMode -> { + wct.setWindowingMode(it.token, WINDOWING_MODE_UNDEFINED) + } + } + } transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null) } + + // TODO: b/362720497 - connected/projected display considerations. + private fun supportsDesks(displayId: Int): Boolean = + DesktopModeStatus.canEnterDesktopMode(context) + + private fun logV(msg: String, vararg arguments: Any?) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + + companion object { + private const val TAG = "DesktopDisplayEventHandler" + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt index 7897c0aa35bc..f5a95a670036 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt @@ -24,10 +24,10 @@ import android.view.MotionEvent import android.view.MotionEvent.TOOL_TYPE_FINGER import android.view.MotionEvent.TOOL_TYPE_MOUSE import android.view.MotionEvent.TOOL_TYPE_STYLUS +import android.window.DesktopModeFlags import com.android.internal.annotations.VisibleForTesting import com.android.internal.protolog.ProtoLog import com.android.internal.util.FrameworkStatsLog -import com.android.window.flags.Flags import com.android.wm.shell.EventLogTags import com.android.wm.shell.common.DisplayController import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE @@ -185,7 +185,7 @@ class DesktopModeEventLogger { displayController: DisplayController? = null, displayLayoutSize: Size? = null, ) { - if (!Flags.enableResizingMetrics()) return + if (!DesktopModeFlags.ENABLE_RESIZING_METRICS.isTrue) return val sessionId = currentSessionId.get() if (sessionId == NO_SESSION_ID) { @@ -232,7 +232,7 @@ class DesktopModeEventLogger { displayController: DisplayController? = null, displayLayoutSize: Size? = null, ) { - if (!Flags.enableResizingMetrics()) return + if (!DesktopModeFlags.ENABLE_RESIZING_METRICS.isTrue) return val sessionId = currentSessionId.get() if (sessionId == NO_SESSION_ID) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt index 9334898fdb93..5269318943d9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt @@ -23,10 +23,10 @@ import android.hardware.input.InputManager import android.hardware.input.InputManager.KeyGestureEventHandler import android.hardware.input.KeyGestureEvent import android.os.IBinder +import android.window.DesktopModeFlags import com.android.hardware.input.Flags.manageKeyGestures import com.android.internal.protolog.ProtoLog import com.android.window.flags.Flags.enableMoveToNextDisplayShortcut -import com.android.window.flags.Flags.enableTaskResizingKeyboardShortcuts import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.ShellExecutor @@ -144,7 +144,8 @@ class DesktopModeKeyGestureHandler( KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW, KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW, KeyGestureEvent.KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW -> - enableTaskResizingKeyboardShortcuts() && manageKeyGestures() + DesktopModeFlags.ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS.isTrue && + manageKeyGestures() else -> false } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt index a43358603bc3..3b051694ae81 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt @@ -167,6 +167,29 @@ class DesktopModeLoggerTransitionObserver( override fun onTransitionFinished(transition: IBinder, aborted: Boolean) {} + fun onTaskVanished(taskInfo: RunningTaskInfo) { + // At this point the task should have been cleared up due to transition. If it's not yet + // cleared up, it might be one of the edge cases where transitions don't give the correct + // signal. + if (visibleFreeformTaskInfos.containsKey(taskInfo.taskId)) { + val postTransitionFreeformTasks: SparseArray<TaskInfo> = SparseArray() + postTransitionFreeformTasks.putAll(visibleFreeformTaskInfos) + postTransitionFreeformTasks.remove(taskInfo.taskId) + ProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "DesktopModeLogger: processing tasks after task vanished %s", + postTransitionFreeformTasks.size(), + ) + identifyLogEventAndUpdateState( + transition = null, + transitionInfo = null, + preTransitionVisibleFreeformTasks = visibleFreeformTaskInfos, + postTransitionVisibleFreeformTasks = postTransitionFreeformTasks, + newFocusedFreeformTask = null, + ) + } + } + // Returns null if there was no change in focused task private fun getNewFocusedFreeformTask(info: TransitionInfo): TaskInfo? { val freeformWindowChanges = @@ -253,8 +276,8 @@ class DesktopModeLoggerTransitionObserver( * state and update it */ private fun identifyLogEventAndUpdateState( - transition: IBinder, - transitionInfo: TransitionInfo, + transition: IBinder?, + transitionInfo: TransitionInfo?, preTransitionVisibleFreeformTasks: SparseArray<TaskInfo>, postTransitionVisibleFreeformTasks: SparseArray<TaskInfo>, newFocusedFreeformTask: TaskInfo?, @@ -310,8 +333,8 @@ class DesktopModeLoggerTransitionObserver( /** Compare the old and new state of taskInfos and identify and log the changes */ private fun identifyAndLogTaskUpdates( - transition: IBinder, - transitionInfo: TransitionInfo, + transition: IBinder?, + transitionInfo: TransitionInfo?, preTransitionVisibleFreeformTasks: SparseArray<TaskInfo>, postTransitionVisibleFreeformTasks: SparseArray<TaskInfo>, newFocusedFreeformTask: TaskInfo?, @@ -384,22 +407,24 @@ class DesktopModeLoggerTransitionObserver( } private fun getMinimizeReason( - transition: IBinder, - transitionInfo: TransitionInfo, + transition: IBinder?, + transitionInfo: TransitionInfo?, taskInfo: TaskInfo, ): MinimizeReason? { - if (transitionInfo.type == Transitions.TRANSIT_MINIMIZE) { + if (transitionInfo?.type == Transitions.TRANSIT_MINIMIZE) { return MinimizeReason.MINIMIZE_BUTTON } - val minimizingTask = desktopTasksLimiter.getOrNull()?.getMinimizingTask(transition) + val minimizingTask = + transition?.let { desktopTasksLimiter.getOrNull()?.getMinimizingTask(transition) } if (minimizingTask?.taskId == taskInfo.taskId) { return minimizingTask.minimizeReason } return null } - private fun getUnminimizeReason(transition: IBinder, taskInfo: TaskInfo): UnminimizeReason? { - val unminimizingTask = desktopTasksLimiter.getOrNull()?.getUnminimizingTask(transition) + private fun getUnminimizeReason(transition: IBinder?, taskInfo: TaskInfo): UnminimizeReason? { + val unminimizingTask = + transition?.let { desktopTasksLimiter.getOrNull()?.getUnminimizingTask(transition) } if (unminimizingTask?.taskId == taskInfo.taskId) { return unminimizingTask.unminimizeReason } @@ -441,24 +466,24 @@ class DesktopModeLoggerTransitionObserver( } /** Get [EnterReason] for this session enter */ - private fun getEnterReason(transitionInfo: TransitionInfo): EnterReason { + private fun getEnterReason(transitionInfo: TransitionInfo?): EnterReason { val enterReason = when { - transitionInfo.type == WindowManager.TRANSIT_WAKE + transitionInfo?.type == WindowManager.TRANSIT_WAKE // If there is a screen lock, desktop window entry is after dismissing keyguard || - (transitionInfo.type == WindowManager.TRANSIT_TO_BACK && + (transitionInfo?.type == WindowManager.TRANSIT_TO_BACK && wasPreviousTransitionExitByScreenOff) -> EnterReason.SCREEN_ON - transitionInfo.type == TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP -> + transitionInfo?.type == TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP -> EnterReason.APP_HANDLE_DRAG - transitionInfo.type == TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON -> + transitionInfo?.type == TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON -> EnterReason.APP_HANDLE_MENU_BUTTON - transitionInfo.type == TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW -> + transitionInfo?.type == TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW -> EnterReason.APP_FROM_OVERVIEW - transitionInfo.type == TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT -> + transitionInfo?.type == TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT -> EnterReason.KEYBOARD_SHORTCUT_ENTER // NOTE: the below condition also applies for EnterReason quickswitch - transitionInfo.type == WindowManager.TRANSIT_TO_FRONT -> EnterReason.OVERVIEW + transitionInfo?.type == WindowManager.TRANSIT_TO_FRONT -> EnterReason.OVERVIEW // Enter desktop mode from cancelled recents has no transition. Enter is detected on // the // next transition involving freeform windows. @@ -469,12 +494,13 @@ class DesktopModeLoggerTransitionObserver( // after // a cancelled recents. wasPreviousTransitionExitToOverview -> EnterReason.OVERVIEW - transitionInfo.type == WindowManager.TRANSIT_OPEN -> EnterReason.APP_FREEFORM_INTENT + transitionInfo?.type == WindowManager.TRANSIT_OPEN -> + EnterReason.APP_FREEFORM_INTENT else -> { ProtoLog.w( WM_SHELL_DESKTOP_MODE, "Unknown enter reason for transition type: %s", - transitionInfo.type, + transitionInfo?.type, ) EnterReason.UNKNOWN_ENTER } @@ -484,30 +510,31 @@ class DesktopModeLoggerTransitionObserver( } /** Get [ExitReason] for this session exit */ - private fun getExitReason(transitionInfo: TransitionInfo): ExitReason = + private fun getExitReason(transitionInfo: TransitionInfo?): ExitReason = when { - transitionInfo.type == WindowManager.TRANSIT_SLEEP -> { + transitionInfo?.type == WindowManager.TRANSIT_SLEEP -> { wasPreviousTransitionExitByScreenOff = true ExitReason.SCREEN_OFF } // TODO(b/384490301): differentiate back gesture / button exit from clicking the close // button located in the window top corner. - transitionInfo.type == WindowManager.TRANSIT_TO_BACK -> ExitReason.TASK_MOVED_TO_BACK - transitionInfo.type == WindowManager.TRANSIT_CLOSE -> ExitReason.TASK_FINISHED - transitionInfo.type == TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG -> ExitReason.DRAG_TO_EXIT - transitionInfo.type == TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON -> + transitionInfo?.type == WindowManager.TRANSIT_TO_BACK -> ExitReason.TASK_MOVED_TO_BACK + transitionInfo?.type == WindowManager.TRANSIT_CLOSE -> ExitReason.TASK_FINISHED + transitionInfo?.type == TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG -> ExitReason.DRAG_TO_EXIT + transitionInfo?.type == TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON -> ExitReason.APP_HANDLE_MENU_BUTTON_EXIT - transitionInfo.type == TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT -> + transitionInfo?.type == TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT -> ExitReason.KEYBOARD_SHORTCUT_EXIT - transitionInfo.isExitToRecentsTransition() -> ExitReason.RETURN_HOME_OR_OVERVIEW - transitionInfo.type == Transitions.TRANSIT_MINIMIZE -> ExitReason.TASK_MINIMIZED + transitionInfo?.isExitToRecentsTransition() == true -> + ExitReason.RETURN_HOME_OR_OVERVIEW + transitionInfo?.type == Transitions.TRANSIT_MINIMIZE -> ExitReason.TASK_MINIMIZED else -> { ProtoLog.w( WM_SHELL_DESKTOP_MODE, "Unknown exit reason for transition type: %s", - transitionInfo.type, + transitionInfo?.type, ) ExitReason.UNKNOWN_EXIT } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt index 9b9988457808..164d04bbde65 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt @@ -110,8 +110,8 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl pw.println("Error: display id should be an integer") return false } - pw.println("Not implemented.") - return false + controller.createDesk(displayId) + return true } private fun runActivateDesk(args: Array<String>, pw: PrintWriter): Boolean { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt index fa696682de28..4ff1a5f1be31 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt @@ -171,6 +171,9 @@ class DesktopRepository( /** Returns a list of all [Desk]s in the repository. */ private fun desksSequence(): Sequence<Desk> = desktopData.desksSequence() + /** Returns the number of desks in the given display. */ + fun getNumberOfDesks(displayId: Int) = desktopData.getNumberOfDesks(displayId) + /** Adds [regionListener] to inform about changes to exclusion regions for all Desktop tasks. */ fun setExclusionRegionListener(regionListener: Consumer<Region>, executor: Executor) { desktopGestureExclusionListener = regionListener @@ -201,11 +204,11 @@ class DesktopRepository( /** Adds the given desk under the given display. */ fun addDesk(displayId: Int, deskId: Int) { - desktopData.getOrCreateDesk(displayId, deskId) + desktopData.createDesk(displayId, deskId) } /** Returns the default desk in the given display. */ - fun getDefaultDesk(displayId: Int): Int? = desktopData.getDefaultDesk(displayId)?.deskId + private fun getDefaultDesk(displayId: Int): Desk? = desktopData.getDefaultDesk(displayId) /** Sets the given desk as the active one in the given display. */ fun setActiveDesk(displayId: Int, deskId: Int) { @@ -229,15 +232,14 @@ class DesktopRepository( * TODO: b/389960283 - add explicit [deskId] argument. */ private fun addActiveTask(displayId: Int, taskId: Int) { - val activeDeskId = - desktopData.getActiveDesk(displayId)?.deskId - ?: error("Expected active desk in display: $displayId") + val activeDesk = desktopData.getDefaultDesk(displayId) + checkNotNull(activeDesk) { "Expected desk in display: $displayId" } // Removes task if it is active on another desk excluding [activeDesk]. - removeActiveTask(taskId, excludedDeskId = activeDeskId) + removeActiveTask(taskId, excludedDeskId = activeDesk.deskId) - if (desktopData.getOrCreateDesk(displayId, activeDeskId).activeTasks.add(taskId)) { - logD("Adds active task=%d displayId=%d deskId=%d", taskId, displayId, activeDeskId) + if (activeDesk.activeTasks.add(taskId)) { + logD("Adds active task=%d displayId=%d deskId=%d", taskId, displayId, activeDesk.deskId) updateActiveTasksListeners(displayId) } } @@ -266,18 +268,23 @@ class DesktopRepository( * TODO: b/389960283 - add explicit [deskId] argument. */ fun addClosingTask(displayId: Int, taskId: Int) { - val activeDeskId = - desktopData.getActiveDesk(displayId)?.deskId + val activeDesk = + desktopData.getActiveDesk(displayId) ?: error("Expected active desk in display: $displayId") - if (desktopData.getOrCreateDesk(displayId, activeDeskId).closingTasks.add(taskId)) { - logD("Added closing task=%d displayId=%d deskId=%d", taskId, displayId, activeDeskId) + if (activeDesk.closingTasks.add(taskId)) { + logD( + "Added closing task=%d displayId=%d deskId=%d", + taskId, + displayId, + activeDesk.deskId, + ) } else { // If the task hasn't been removed from closing list after it disappeared. logW( "Task with taskId=%d displayId=%d deskId=%d is already closing", taskId, displayId, - activeDeskId, + activeDesk.deskId, ) } } @@ -323,7 +330,7 @@ class DesktopRepository( /** * Returns the active tasks in the given display's active desk. * - * TODO: b/389960283 - add explicit [deskId] argument. + * TODO: b/389960283 - migrate callers to [getActiveTaskIdsInDesk]. */ @VisibleForTesting fun getActiveTasks(displayId: Int): ArraySet<Int> = @@ -332,19 +339,27 @@ class DesktopRepository( /** * Returns the minimized tasks in the given display's active desk. * - * TODO: b/389960283 - add explicit [deskId] argument. + * TODO: b/389960283 - migrate callers to [getMinimizedTaskIdsInDesk]. */ fun getMinimizedTasks(displayId: Int): ArraySet<Int> = ArraySet(desktopData.getActiveDesk(displayId)?.minimizedTasks) + @VisibleForTesting + fun getMinimizedTaskIdsInDesk(deskId: Int): ArraySet<Int> = + ArraySet(desktopData.getDesk(deskId)?.minimizedTasks) + /** * Returns all active non-minimized tasks for [displayId] ordered from top to bottom. * - * TODO: b/389960283 - add explicit [deskId] argument. + * TODO: b/389960283 - migrate callers to [getExpandedTasksIdsInDeskOrdered]. */ fun getExpandedTasksOrdered(displayId: Int): List<Int> = getFreeformTasksInZOrder(displayId).filter { !isMinimizedTask(it) } + @VisibleForTesting + fun getExpandedTasksIdsInDeskOrdered(deskId: Int): List<Int> = + getFreeformTasksIdsInDeskInZOrder(deskId).filter { !isMinimizedTask(it) } + /** * Returns the count of active non-minimized tasks for [displayId]. * @@ -357,11 +372,15 @@ class DesktopRepository( /** * Returns a list of freeform tasks, ordered from top-bottom (top at index 0). * - * TODO: b/389960283 - add explicit [deskId] argument. + * TODO: b/389960283 - migrate callers to [getFreeformTasksIdsInDeskInZOrder]. */ @VisibleForTesting fun getFreeformTasksInZOrder(displayId: Int): ArrayList<Int> = - ArrayList(desktopData.getActiveDesk(displayId)?.freeformTasksInZOrder ?: emptyList()) + ArrayList(desktopData.getDefaultDesk(displayId)?.freeformTasksInZOrder ?: emptyList()) + + @VisibleForTesting + fun getFreeformTasksIdsInDeskInZOrder(deskId: Int): ArrayList<Int> = + ArrayList(desktopData.getDesk(deskId)?.freeformTasksInZOrder ?: emptyList()) /** Returns the tasks inside the given desk. */ fun getActiveTaskIdsInDesk(deskId: Int): Set<Int> = @@ -401,8 +420,8 @@ class DesktopRepository( } val prevCount = getVisibleTaskCount(displayId) if (isVisible) { - desktopData.getActiveDesk(displayId)?.visibleTasks?.add(taskId) - ?: error("Expected non-null active desk in display $displayId") + desktopData.getDefaultDesk(displayId)?.visibleTasks?.add(taskId) + ?: error("Expected non-null desk in display $displayId") unminimizeTask(displayId, taskId) } else { desktopData.getActiveDesk(displayId)?.visibleTasks?.remove(taskId) @@ -587,17 +606,15 @@ class DesktopRepository( * TODO: b/389960283 - add explicit [deskId] argument. */ private fun addOrMoveFreeformTaskToTop(displayId: Int, taskId: Int) { - val activeDesk = - desktopData.getActiveDesk(displayId) - ?: error("Expected a desk to be active in display: $displayId") + val desk = getDefaultDesk(displayId) ?: error("Expected a desk in display: $displayId") logD( "Add or move task to top: display=%d taskId=%d deskId=%d", taskId, displayId, - activeDesk.deskId, + desk.deskId, ) - desktopData.forAllDesks { _, desk -> desk.freeformTasksInZOrder.remove(taskId) } - activeDesk.freeformTasksInZOrder.add(0, taskId) + desktopData.forAllDesks { _, desk1 -> desk1.freeformTasksInZOrder.remove(taskId) } + desk.freeformTasksInZOrder.add(0, taskId) // Unminimize the task if it is minimized. unminimizeTask(displayId, taskId) if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { @@ -835,13 +852,8 @@ class DesktopRepository( /** An interface for the desktop hierarchy's data managed by this repository. */ private interface DesktopData { - /** - * Returns the existing desk or creates a new entry if needed. - * - * TODO: 389787966 - consider removing this as it cannot be assumed a desk can be created in - * all devices / form-factors. - */ - fun getOrCreateDesk(displayId: Int, deskId: Int): Desk + /** Creates a desk record. */ + fun createDesk(displayId: Int, deskId: Int) /** Returns the desk with the given id, or null if it does not exist. */ fun getDesk(deskId: Int): Desk? @@ -894,7 +906,8 @@ class DesktopRepository( /** * A [DesktopData] implementation that only supports one desk per display. * - * Internally, it reuses the displayId as that display's single desk's id. + * Internally, it reuses the displayId as that display's single desk's id. It also never truly + * "removes" a desk, it just clears its content. */ private class SingleDesktopData : DesktopData { private val deskByDisplayId = @@ -907,12 +920,16 @@ class DesktopRepository( } } - override fun getOrCreateDesk(displayId: Int, deskId: Int): Desk { - check(displayId == deskId) - return deskByDisplayId.getOrCreate(displayId) + override fun createDesk(displayId: Int, deskId: Int) { + check(displayId == deskId) { "Display and desk ids must match" } + deskByDisplayId.getOrCreate(displayId) } - override fun getDesk(deskId: Int): Desk = getOrCreateDesk(deskId, deskId) + override fun getDesk(deskId: Int): Desk = + // TODO: b/362720497 - consider enforcing that the desk has been created before trying + // to use it. As of now, there are cases where a task may be created faster than a + // desk is, so just create it here if needed. See b/391984373. + deskByDisplayId.getOrCreate(deskId) override fun getActiveDesk(displayId: Int): Desk { // TODO: 389787966 - consider migrating to an "active" state instead of checking the @@ -927,7 +944,7 @@ class DesktopRepository( // existence of visible desktop windows, among other factors. } - override fun getDefaultDesk(displayId: Int): Desk = getOrCreateDesk(displayId, displayId) + override fun getDefaultDesk(displayId: Int): Desk = getDesk(deskId = displayId) override fun getAllActiveDesks(): Set<Desk> = deskByDisplayId.valueIterator().asSequence().toSet() @@ -943,7 +960,7 @@ class DesktopRepository( } override fun forAllDesks(displayId: Int, consumer: (Desk) -> Unit) { - consumer(getOrCreateDesk(displayId, displayId)) + consumer(getDesk(deskId = displayId)) } override fun desksSequence(): Sequence<Desk> = deskByDisplayId.valueIterator().asSequence() @@ -962,16 +979,14 @@ class DesktopRepository( private class MultiDesktopData : DesktopData { private val desktopDisplays = SparseArray<DesktopDisplay>() - override fun getOrCreateDesk(displayId: Int, deskId: Int): Desk { + override fun createDesk(displayId: Int, deskId: Int) { val display = desktopDisplays[displayId] ?: DesktopDisplay(displayId).also { desktopDisplays[displayId] = it } - val desk = - display.orderedDesks.find { desk -> desk.deskId == deskId } - ?: Desk(deskId = deskId, displayId = displayId).also { - display.orderedDesks.add(it) - } - return desk + check(display.orderedDesks.none { desk -> desk.deskId == deskId }) { + "Attempting to create desk#$deskId that already exists in display#$displayId" + } + display.orderedDesks.add(Desk(deskId = deskId, displayId = displayId)) } override fun getDesk(deskId: Int): Desk? { @@ -999,7 +1014,8 @@ class DesktopRepository( override fun getDefaultDesk(displayId: Int): Desk? { val display = desktopDisplays[displayId] ?: return null - return display.orderedDesks.firstOrNull() + return display.orderedDesks.find { it.deskId == display.activeDeskId } + ?: display.orderedDesks.firstOrNull() } override fun getAllActiveDesks(): Set<Desk> { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 3ae553596631..5b206dedee49 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -16,6 +16,7 @@ package com.android.wm.shell.desktopmode +import android.annotation.UserIdInt import android.app.ActivityManager import android.app.ActivityManager.RunningTaskInfo import android.app.ActivityOptions @@ -55,6 +56,7 @@ import android.view.WindowManager.TRANSIT_TO_FRONT import android.widget.Toast import android.window.DesktopModeFlags import android.window.DesktopModeFlags.DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE +import android.window.DesktopModeFlags.ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY import android.window.DesktopModeFlags.ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS import android.window.RemoteTransition @@ -85,8 +87,7 @@ import com.android.wm.shell.common.RemoteCallable import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SingleInstanceRemoteListener import com.android.wm.shell.common.SyncTransactionQueue -import com.android.wm.shell.compatui.isTopActivityExemptFromDesktopWindowing -import com.android.wm.shell.compatui.isTransparentTask +import com.android.wm.shell.common.UserProfileContexts import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.InputMethod import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.MinimizeReason import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger @@ -102,6 +103,8 @@ import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler.FULLSCR import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider import com.android.wm.shell.desktopmode.minimize.DesktopWindowLimitRemoteHandler +import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer +import com.android.wm.shell.desktopmode.multidesks.OnDeskRemovedListener import com.android.wm.shell.draganddrop.DragAndDropController import com.android.wm.shell.freeform.FreeformTaskTransitionStarter import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE @@ -113,6 +116,7 @@ import com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_ST import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.shared.annotations.ExternalThread import com.android.wm.shell.shared.annotations.ShellMainThread +import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.DESKTOP_DENSITY_OVERRIDE import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.useDesktopOverrideDensity @@ -180,6 +184,9 @@ class DesktopTasksController( private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider, private val bubbleController: Optional<BubbleController>, private val overviewToDesktopTransitionObserver: OverviewToDesktopTransitionObserver, + private val desksOrganizer: DesksOrganizer, + private val userProfileContexts: UserProfileContexts, + private val desktopModeCompatPolicy: DesktopModeCompatPolicy, ) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler, @@ -232,6 +239,9 @@ class DesktopTasksController( // Used to prevent handleRequest from moving the new fullscreen task to freeform. private var dragAndDropFullscreenCookie: Binder? = null + // A listener that is invoked after a desk has been remove from the system. */ + var onDeskRemovedListener: OnDeskRemovedListener? = null + init { desktopMode = DesktopModeImpl() if (DesktopModeStatus.canEnterDesktopMode(context)) { @@ -415,6 +425,18 @@ class DesktopTasksController( return isFreeformDisplay } + /** Creates a new desk in the given display. */ + fun createDesk(displayId: Int) { + if (Flags.enableMultipleDesktopsBackend()) { + desksOrganizer.createDesk(displayId) { deskId -> + taskRepository.addDesk(displayId = displayId, deskId = deskId) + } + } else { + // In single-desk, the desk reuses the display id. + taskRepository.addDesk(displayId = displayId, deskId = displayId) + } + } + /** Moves task to desktop mode if task is running, else launches it in desktop mode. */ @JvmOverloads fun moveTaskToDesktop( @@ -496,10 +518,7 @@ class DesktopTasksController( remoteTransition: RemoteTransition? = null, callback: IMoveToDesktopCallback? = null, ) { - if ( - DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue() && - isTopActivityExemptFromDesktopWindowing(context, task) - ) { + if (desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(task)) { logW("Cannot enter desktop for taskId %d, ineligible top activity found", task.taskId) return } @@ -1468,6 +1487,7 @@ class DesktopTasksController( } private fun addLaunchHomePendingIntent(wct: WindowContainerTransaction, displayId: Int) { + val userHandle = UserHandle.of(userId) val launchHomeIntent = Intent(Intent.ACTION_MAIN).apply { if (displayId != DEFAULT_DISPLAY) { @@ -1483,18 +1503,20 @@ class DesktopTasksController( ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS } val pendingIntent = - PendingIntent.getActivity( + PendingIntent.getActivityAsUser( context, - /* requestCode = */ 0, + /* requestCode= */ 0, launchHomeIntent, PendingIntent.FLAG_IMMUTABLE, + /* options= */ null, + userHandle, ) wct.sendPendingIntent(pendingIntent, launchHomeIntent, options.toBundle()) } private fun addWallpaperActivity(displayId: Int, wct: WindowContainerTransaction) { logV("addWallpaperActivity") - if (Flags.enableDesktopWallpaperActivityForSystemUser()) { + if (ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER.isTrue()) { val intent = Intent(context, DesktopWallpaperActivity::class.java) if ( desktopWallpaperActivityTokenProvider.getToken(displayId) == null && @@ -1557,7 +1579,7 @@ class DesktopTasksController( private fun removeWallpaperActivity(wct: WindowContainerTransaction, displayId: Int) { desktopWallpaperActivityTokenProvider.getToken(displayId)?.let { token -> logV("removeWallpaperActivity") - if (Flags.enableDesktopWallpaperActivityForSystemUser()) { + if (ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER.isTrue()) { wct.reorder(token, /* onTop= */ false) } else { wct.removeTask(token) @@ -1798,8 +1820,7 @@ class DesktopTasksController( taskRepository.isActiveTask(triggerTask.taskId)) private fun isIncompatibleTask(task: RunningTaskInfo) = - DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue() && - isTopActivityExemptFromDesktopWindowing(context, task) + desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(task) private fun shouldHandleTaskClosing(request: TransitionRequestInfo): Boolean = ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue() && @@ -1841,7 +1862,9 @@ class DesktopTasksController( // need updates in some cases. val baseActivity = callingTaskInfo.baseActivity ?: return val fillIn: Intent = - context.packageManager.getLaunchIntentForPackage(baseActivity.packageName) ?: return + userProfileContexts[callingTaskInfo.userId] + ?.packageManager + ?.getLaunchIntentForPackage(baseActivity.packageName) ?: return fillIn.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK) val launchIntent = PendingIntent.getActivity( @@ -2068,11 +2091,11 @@ class DesktopTasksController( */ private fun handleIncompatibleTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? { logV("handleIncompatibleTaskLaunch") - if (!isDesktopModeShowing(task.displayId)) return null + if (!isDesktopModeShowing(task.displayId) && !forceEnterDesktop(task.displayId)) return null // Only update task repository for transparent task. if ( DesktopModeFlags.INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC - .isTrue() && isTransparentTask(task) + .isTrue() && desktopModeCompatPolicy.isTransparentTask(task) ) { taskRepository.setTopTransparentFullscreenTaskId(task.displayId, task.taskId) } @@ -2720,6 +2743,7 @@ class DesktopTasksController( // TODO(b/358114479): Move this implementation into a separate class. override fun onUnhandledDrag( launchIntent: PendingIntent, + @UserIdInt userId: Int, dragEvent: DragEvent, onFinishCallback: Consumer<Boolean>, ): Boolean { @@ -2728,8 +2752,10 @@ class DesktopTasksController( // Not currently in desktop mode, ignore the drop return false } + + // TODO: val launchComponent = getComponent(launchIntent) - if (!multiInstanceHelper.supportsMultiInstanceSplit(launchComponent)) { + if (!multiInstanceHelper.supportsMultiInstanceSplit(launchComponent, userId)) { // TODO(b/320797628): Should only return early if there is an existing running task, and // notify the user as well. But for now, just ignore the drop. logV("Dropped intent does not support multi-instance") @@ -2989,6 +3015,14 @@ class DesktopTasksController( controller = null } + override fun createDesk(displayId: Int) { + // TODO: b/362720497 - Implement this API. + } + + override fun activateDesk(deskId: Int, remoteTransition: RemoteTransition?) { + // TODO: b/362720497 - Implement this API. + } + override fun showDesktopApps(displayId: Int, remoteTransition: RemoteTransition?) { executeRemoteCallWithTaskPermission(controller, "showDesktopApps") { c -> c.showDesktopApps(displayId, remoteTransition) @@ -3016,17 +3050,6 @@ class DesktopTasksController( ) } - override fun getVisibleTaskCount(displayId: Int): Int { - val result = IntArray(1) - executeRemoteCallWithTaskPermission( - controller, - "visibleTaskCount", - { controller -> result[0] = controller.visibleTaskCount(displayId) }, - /* blocking= */ true, - ) - return result[0] - } - override fun onDesktopSplitSelectAnimComplete(taskInfo: RunningTaskInfo) { executeRemoteCallWithTaskPermission(controller, "onDesktopSplitSelectAnimComplete") { c -> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt index 14c8429766cc..b3648699ed0b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt @@ -27,6 +27,7 @@ import android.view.WindowManager.TRANSIT_PIP import android.view.WindowManager.TRANSIT_TO_BACK import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.DesktopModeFlags +import android.window.DesktopModeFlags.ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY import android.window.TransitionInfo import android.window.WindowContainerTransaction @@ -275,7 +276,7 @@ class DesktopTasksTransitionObserver( desktopWallpaperActivityTokenProvider .getToken(lastSeenTransitionToCloseWallpaper.displayId) ?.let { wallpaperActivityToken -> - if (Flags.enableDesktopWallpaperActivityForSystemUser()) { + if (ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER.isTrue()) { transitions.startTransition( TRANSIT_TO_BACK, WindowContainerTransaction() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt index 13576aa42737..a5ba6612bb1a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt @@ -21,9 +21,9 @@ import android.content.Context import android.content.pm.UserInfo import android.os.UserManager import android.util.SparseArray +import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_HSUM import androidx.core.util.forEach import com.android.internal.protolog.ProtoLog -import com.android.window.flags.Flags import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE @@ -68,7 +68,7 @@ class DesktopUserRepositories( if (DesktopModeStatus.canEnterDesktopMode(context)) { shellInit.addInitCallback(::onInit, this) } - if (Flags.enableDesktopWindowingHsum()) { + if (ENABLE_DESKTOP_WINDOWING_HSUM.isTrue()) { userIdToProfileIdsMap[userId] = userManager.getProfiles(userId).map { it.id } } } @@ -80,7 +80,7 @@ class DesktopUserRepositories( /** Returns [DesktopRepository] for the parent user id. */ fun getProfile(profileId: Int): DesktopRepository { - if (Flags.enableDesktopWindowingHsum()) { + if (ENABLE_DESKTOP_WINDOWING_HSUM.isTrue()) { for ((uid, profileIds) in userIdToProfileIdsMap) { if (profileId in profileIds) { return desktopRepoByUserId.getOrCreate(uid) @@ -101,14 +101,14 @@ class DesktopUserRepositories( override fun onUserChanged(newUserId: Int, userContext: Context) { logD("onUserChanged previousUserId=%d, newUserId=%d", userId, newUserId) userId = newUserId - if (Flags.enableDesktopWindowingHsum()) { + if (ENABLE_DESKTOP_WINDOWING_HSUM.isTrue()) { sanitizeUsers() } } override fun onUserProfilesChanged(profiles: MutableList<UserInfo>) { logD("onUserProfilesChanged profiles=%s", profiles.toString()) - if (Flags.enableDesktopWindowingHsum()) { + if (ENABLE_DESKTOP_WINDOWING_HSUM.isTrue()) { // TODO(b/366397912): Remove all persisted profile data when the profile changes. userIdToProfileIdsMap[userId] = profiles.map { it.id } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl index a135e4462150..44f7e16e98c3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl @@ -29,6 +29,11 @@ import com.android.wm.shell.desktopmode.IMoveToDesktopCallback; * Interface that is exposed to remote callers to manipulate desktop mode features. */ interface IDesktopMode { + /** If possible, creates a new desk on the display whose ID is `displayId`. */ + oneway void createDesk(int displayId); + + /** Activates the desk whose ID is `deskId` on whatever display it currently exists on. */ + oneway void activateDesk(int deskId, in RemoteTransition remoteTransition); /** Show apps on the desktop on the given display */ void showDesktopApps(int displayId, in RemoteTransition remoteTransition); @@ -48,9 +53,6 @@ interface IDesktopMode { oneway void showDesktopApp(int taskId, in @nullable RemoteTransition remoteTransition, in DesktopTaskToFrontReason toFrontReason); - /** Get count of visible desktop tasks on the given display */ - int getVisibleTaskCount(int displayId); - /** Perform cleanup transactions after the animation to split select is complete */ oneway void onDesktopSplitSelectAnimComplete(in RunningTaskInfo taskInfo); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/compatui/SystemModalsTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/compatui/SystemModalsTransitionHandler.kt index a428ce18a49e..224ff37a1dca 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/compatui/SystemModalsTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/compatui/SystemModalsTransitionHandler.kt @@ -17,6 +17,7 @@ package com.android.wm.shell.desktopmode.compatui import android.animation.ValueAnimator +import android.app.ActivityManager.RunningTaskInfo import android.content.Context import android.os.IBinder import android.view.Display.DEFAULT_DISPLAY @@ -28,13 +29,14 @@ import androidx.core.animation.addListener import com.android.app.animation.Interpolators import com.android.internal.protolog.ProtoLog import com.android.wm.shell.common.ShellExecutor -import com.android.wm.shell.compatui.isTopActivityExemptFromDesktopWindowing import com.android.wm.shell.desktopmode.DesktopUserRepositories +import com.android.wm.shell.desktopmode.DesktopWallpaperActivity import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.shared.TransitionUtil.isClosingMode import com.android.wm.shell.shared.TransitionUtil.isClosingType import com.android.wm.shell.shared.TransitionUtil.isOpeningMode import com.android.wm.shell.shared.TransitionUtil.isOpeningType +import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TransitionHandler @@ -47,6 +49,7 @@ class SystemModalsTransitionHandler( private val shellInit: ShellInit, private val transitions: Transitions, private val desktopUserRepositories: DesktopUserRepositories, + private val desktopModeCompatPolicy: DesktopModeCompatPolicy, ) : TransitionHandler { private val showingSystemModalsIds = mutableSetOf<Int>() @@ -128,7 +131,7 @@ class SystemModalsTransitionHandler( return@find false } val taskInfo = change.taskInfo ?: return@find false - return@find isTopActivityExemptFromDesktopWindowing(context, taskInfo) + return@find isSystemModal(taskInfo) } private fun getClosingSystemModal(info: TransitionInfo): TransitionInfo.Change? = @@ -137,10 +140,13 @@ class SystemModalsTransitionHandler( return@find false } val taskInfo = change.taskInfo ?: return@find false - return@find isTopActivityExemptFromDesktopWindowing(context, taskInfo) || - showingSystemModalsIds.contains(taskInfo.taskId) + return@find isSystemModal(taskInfo) || showingSystemModalsIds.contains(taskInfo.taskId) } + private fun isSystemModal(taskInfo: RunningTaskInfo): Boolean = + !DesktopWallpaperActivity.isWallpaperTask(taskInfo) && + desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(taskInfo) + private fun createAlphaAnimator( transaction: SurfaceControl.Transaction, leash: SurfaceControl, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt index 5757c6afd196..b614b3f4d025 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt @@ -22,7 +22,6 @@ import android.content.Context import android.content.res.Resources import android.graphics.Point import android.os.SystemProperties -import android.util.Slog import com.android.window.flags.Flags import com.android.wm.shell.R import com.android.wm.shell.desktopmode.CaptionState @@ -32,27 +31,17 @@ import com.android.wm.shell.shared.annotations.ShellBackgroundThread import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopMode import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource -import com.android.wm.shell.windowdecor.common.DecorThemeUtil import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController.TooltipColorScheme import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController.TooltipEducationViewConfig -import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.MainCoroutineDispatcher -import kotlinx.coroutines.TimeoutCancellationException -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.take -import kotlinx.coroutines.flow.timeout import kotlinx.coroutines.launch /** @@ -72,59 +61,75 @@ class AppHandleEducationController( @ShellMainThread private val applicationCoroutineScope: CoroutineScope, @ShellBackgroundThread private val backgroundDispatcher: MainCoroutineDispatcher, ) { - private val decorThemeUtil = DecorThemeUtil(context) private lateinit var openHandleMenuCallback: (Int) -> Unit private lateinit var toDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit + private val onTertiaryFixedColor = + context.getColor(com.android.internal.R.color.materialColorOnTertiaryFixed) + private val tertiaryFixedColor = + context.getColor(com.android.internal.R.color.materialColorTertiaryFixed) init { runIfEducationFeatureEnabled { + // Coroutine block for the first hint that appears on a full-screen app's app handle to + // encourage users to open the app handle menu. applicationCoroutineScope.launch { - // Central block handling the app handle's educational flow end-to-end. - isAppHandleHintViewedFlow() - .flatMapLatest { isAppHandleHintViewed -> - if (isAppHandleHintViewed) { - // If the education is viewed then return emptyFlow() that completes - // immediately. - // This will help us to not listen to [captionHandleStateFlow] after the - // education - // has been viewed already. - emptyFlow() - } else { - // Listen for changes to window decor's caption handle. - windowDecorCaptionHandleRepository.captionStateFlow - // Wait for few seconds before emitting the latest state. - .debounce(APP_HANDLE_EDUCATION_DELAY_MILLIS) - .filter { captionState -> - captionState is CaptionState.AppHandle && - appHandleEducationFilter.shouldShowAppHandleEducation( - captionState - ) - } - } + if (isAppHandleHintViewed()) return@launch + windowDecorCaptionHandleRepository.captionStateFlow + .debounce(APP_HANDLE_EDUCATION_DELAY_MILLIS) + .filter { captionState -> + captionState is CaptionState.AppHandle && + !captionState.isHandleMenuExpanded && + !isAppHandleHintViewed() && + appHandleEducationFilter.shouldShowDesktopModeEducation(captionState) } + .take(1) .flowOn(backgroundDispatcher) .collectLatest { captionState -> - val tooltipColorScheme = tooltipColorScheme(captionState) - - showEducation(captionState, tooltipColorScheme) - // After showing first tooltip, mark education as viewed + showEducation(captionState) appHandleEducationDatastoreRepository .updateAppHandleHintViewedTimestampMillis(true) } } + // Coroutine block for the hint that appears when an app handle is expanded to + // encourage users to enter desktop mode. applicationCoroutineScope.launch { - if (isAppHandleHintUsed()) return@launch + if (isEnterDesktopModeHintViewed()) return@launch windowDecorCaptionHandleRepository.captionStateFlow + .debounce(ENTER_DESKTOP_MODE_EDUCATION_DELAY_MILLIS) .filter { captionState -> - captionState is CaptionState.AppHandle && captionState.isHandleMenuExpanded + captionState is CaptionState.AppHandle && + captionState.isHandleMenuExpanded && + !isEnterDesktopModeHintViewed() && + appHandleEducationFilter.shouldShowDesktopModeEducation(captionState) } .take(1) .flowOn(backgroundDispatcher) - .collect { - // If user expands app handle, mark user has used the app handle hint + .collectLatest { captionState -> + showWindowingImageButtonTooltip(captionState as CaptionState.AppHandle) appHandleEducationDatastoreRepository - .updateAppHandleHintUsedTimestampMillis(true) + .updateEnterDesktopModeHintViewedTimestampMillis(true) + } + } + + // Coroutine block for the hint that appears on the window app header in freeform mode + // to let users know how to exit desktop mode. + applicationCoroutineScope.launch { + if (isExitDesktopModeHintViewed()) return@launch + windowDecorCaptionHandleRepository.captionStateFlow + .debounce(APP_HANDLE_EDUCATION_DELAY_MILLIS) + .filter { captionState -> + captionState is CaptionState.AppHeader && + !captionState.isHeaderMenuExpanded && + !isExitDesktopModeHintViewed() && + appHandleEducationFilter.shouldShowDesktopModeEducation(captionState) + } + .take(1) + .flowOn(backgroundDispatcher) + .collectLatest { captionState -> + showExitWindowingTooltip(captionState as CaptionState.AppHeader) + appHandleEducationDatastoreRepository + .updateExitDesktopModeHintViewedTimestampMillis(true) } } } @@ -135,7 +140,7 @@ class AppHandleEducationController( block() } - private fun showEducation(captionState: CaptionState, tooltipColorScheme: TooltipColorScheme) { + private fun showEducation(captionState: CaptionState) { val appHandleBounds = (captionState as CaptionState.AppHandle).globalAppHandleBounds val tooltipGlobalCoordinates = Point(appHandleBounds.left + appHandleBounds.width() / 2, appHandleBounds.bottom) @@ -145,21 +150,21 @@ class AppHandleEducationController( val appHandleTooltipConfig = TooltipEducationViewConfig( tooltipViewLayout = R.layout.desktop_windowing_education_top_arrow_tooltip, - tooltipColorScheme = tooltipColorScheme, + tooltipColorScheme = + TooltipColorScheme( + tertiaryFixedColor, + onTertiaryFixedColor, + onTertiaryFixedColor, + ), tooltipViewGlobalCoordinates = tooltipGlobalCoordinates, tooltipText = getString(R.string.windowing_app_handle_education_tooltip), arrowDirection = DesktopWindowingEducationTooltipController.TooltipArrowDirection.UP, onEducationClickAction = { - launchWithExceptionHandling { - showWindowingImageButtonTooltip(tooltipColorScheme) - } openHandleMenuCallback(captionState.runningTaskInfo.taskId) }, onDismissAction = { - launchWithExceptionHandling { - showWindowingImageButtonTooltip(tooltipColorScheme) - } + // TODO: b/341320146 - Log previous tooltip was dismissed }, ) @@ -170,7 +175,7 @@ class AppHandleEducationController( } /** Show tooltip that points to windowing image button in app handle menu */ - private suspend fun showWindowingImageButtonTooltip(tooltipColorScheme: TooltipColorScheme) { + private suspend fun showWindowingImageButtonTooltip(captionState: CaptionState.AppHandle) { val appInfoPillHeight = getSize(R.dimen.desktop_mode_handle_menu_app_info_pill_height) val windowingOptionPillHeight = getSize(R.dimen.desktop_mode_handle_menu_windowing_pill_height) @@ -181,128 +186,81 @@ class AppHandleEducationController( getSize(R.dimen.desktop_mode_handle_menu_margin_top) + getSize(R.dimen.desktop_mode_handle_menu_pill_spacing_margin) - windowDecorCaptionHandleRepository.captionStateFlow - // After the first tooltip was dismissed, wait for 400 ms and see if the app handle menu - // has been expanded. - .timeout(APP_HANDLE_EDUCATION_TIMEOUT_MILLIS.milliseconds) - .catchTimeoutAndLog { - // TODO: b/341320146 - Log previous tooltip was dismissed - } - // Wait for few milliseconds before emitting the latest state. - .debounce(APP_HANDLE_EDUCATION_DELAY_MILLIS) - .filter { captionState -> - // Filter out states when app handle is not visible or not expanded. - captionState is CaptionState.AppHandle && captionState.isHandleMenuExpanded - } - // Before showing this tooltip, stop listening to further emissions to avoid - // accidentally - // showing the same tooltip on future emissions. - .take(1) - .flowOn(backgroundDispatcher) - .collectLatest { captionState -> - captionState as CaptionState.AppHandle - val appHandleBounds = captionState.globalAppHandleBounds - val tooltipGlobalCoordinates = - Point( - appHandleBounds.left + appHandleBounds.width() / 2 + appHandleMenuWidth / 2, - appHandleBounds.top + - appHandleMenuMargins + - appInfoPillHeight + - windowingOptionPillHeight / 2, - ) - // Populate information important to inflate windowing image button education - // tooltip. - val windowingImageButtonTooltipConfig = - TooltipEducationViewConfig( - tooltipViewLayout = R.layout.desktop_windowing_education_left_arrow_tooltip, - tooltipColorScheme = tooltipColorScheme, - tooltipViewGlobalCoordinates = tooltipGlobalCoordinates, - tooltipText = - getString( - R.string.windowing_desktop_mode_image_button_education_tooltip - ), - arrowDirection = - DesktopWindowingEducationTooltipController.TooltipArrowDirection.LEFT, - onEducationClickAction = { - launchWithExceptionHandling { - showExitWindowingTooltip(tooltipColorScheme) - } - toDesktopModeCallback( - captionState.runningTaskInfo.taskId, - DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON, - ) - }, - onDismissAction = { - launchWithExceptionHandling { - showExitWindowingTooltip(tooltipColorScheme) - } - }, + val appHandleBounds = captionState.globalAppHandleBounds + val tooltipGlobalCoordinates = + Point( + appHandleBounds.left + appHandleBounds.width() / 2 + appHandleMenuWidth / 2, + appHandleBounds.top + + appHandleMenuMargins + + appInfoPillHeight + + windowingOptionPillHeight / 2, + ) + // Populate information important to inflate windowing image button education + // tooltip. + val windowingImageButtonTooltipConfig = + TooltipEducationViewConfig( + tooltipViewLayout = R.layout.desktop_windowing_education_left_arrow_tooltip, + tooltipColorScheme = + TooltipColorScheme( + tertiaryFixedColor, + onTertiaryFixedColor, + onTertiaryFixedColor, + ), + tooltipViewGlobalCoordinates = tooltipGlobalCoordinates, + tooltipText = + getString(R.string.windowing_desktop_mode_image_button_education_tooltip), + arrowDirection = + DesktopWindowingEducationTooltipController.TooltipArrowDirection.LEFT, + onEducationClickAction = { + toDesktopModeCallback( + captionState.runningTaskInfo.taskId, + DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON, ) + }, + onDismissAction = { + // TODO: b/341320146 - Log previous tooltip was dismissed + }, + ) - windowingEducationViewController.showEducationTooltip( - taskId = captionState.runningTaskInfo.taskId, - tooltipViewConfig = windowingImageButtonTooltipConfig, - ) - } + windowingEducationViewController.showEducationTooltip( + taskId = captionState.runningTaskInfo.taskId, + tooltipViewConfig = windowingImageButtonTooltipConfig, + ) } /** Show tooltip that points to app chip button and educates user on how to exit desktop mode */ - private suspend fun showExitWindowingTooltip(tooltipColorScheme: TooltipColorScheme) { - windowDecorCaptionHandleRepository.captionStateFlow - // After the previous tooltip was dismissed, wait for 400 ms and see if the user entered - // desktop mode. - .timeout(APP_HANDLE_EDUCATION_TIMEOUT_MILLIS.milliseconds) - .catchTimeoutAndLog { - // TODO: b/341320146 - Log previous tooltip was dismissed - } - // Wait for few milliseconds before emitting the latest state. - .debounce(APP_HANDLE_EDUCATION_DELAY_MILLIS) - .filter { captionState -> - // Filter out states when app header is not visible or expanded. - captionState is CaptionState.AppHeader && !captionState.isHeaderMenuExpanded - } - // Before showing this tooltip, stop listening to further emissions to avoid - // accidentally - // showing the same tooltip on future emissions. - .take(1) - .flowOn(backgroundDispatcher) - .collectLatest { captionState -> - captionState as CaptionState.AppHeader - val globalAppChipBounds = captionState.globalAppChipBounds - val tooltipGlobalCoordinates = - Point( - globalAppChipBounds.right, - globalAppChipBounds.top + globalAppChipBounds.height() / 2, - ) - // Populate information important to inflate exit desktop mode education tooltip. - val exitWindowingTooltipConfig = - TooltipEducationViewConfig( - tooltipViewLayout = R.layout.desktop_windowing_education_left_arrow_tooltip, - tooltipColorScheme = tooltipColorScheme, - tooltipViewGlobalCoordinates = tooltipGlobalCoordinates, - tooltipText = - getString(R.string.windowing_desktop_mode_exit_education_tooltip), - arrowDirection = - DesktopWindowingEducationTooltipController.TooltipArrowDirection.LEFT, - onDismissAction = {}, - onEducationClickAction = { - openHandleMenuCallback(captionState.runningTaskInfo.taskId) - }, - ) - windowingEducationViewController.showEducationTooltip( - taskId = captionState.runningTaskInfo.taskId, - tooltipViewConfig = exitWindowingTooltipConfig, - ) - } - } - - private fun tooltipColorScheme(captionState: CaptionState): TooltipColorScheme { - val onTertiaryFixed = - context.getColor(com.android.internal.R.color.materialColorOnTertiaryFixed) - val tertiaryFixed = - context.getColor(com.android.internal.R.color.materialColorTertiaryFixed) - - return TooltipColorScheme(tertiaryFixed, onTertiaryFixed, onTertiaryFixed) + private suspend fun showExitWindowingTooltip(captionState: CaptionState.AppHeader) { + val globalAppChipBounds = captionState.globalAppChipBounds + val tooltipGlobalCoordinates = + Point( + globalAppChipBounds.right, + globalAppChipBounds.top + globalAppChipBounds.height() / 2, + ) + // Populate information important to inflate exit desktop mode education tooltip. + val exitWindowingTooltipConfig = + TooltipEducationViewConfig( + tooltipViewLayout = R.layout.desktop_windowing_education_left_arrow_tooltip, + tooltipColorScheme = + TooltipColorScheme( + tertiaryFixedColor, + onTertiaryFixedColor, + onTertiaryFixedColor, + ), + tooltipViewGlobalCoordinates = tooltipGlobalCoordinates, + tooltipText = getString(R.string.windowing_desktop_mode_exit_education_tooltip), + arrowDirection = + DesktopWindowingEducationTooltipController.TooltipArrowDirection.LEFT, + onDismissAction = { + // TODO: b/341320146 - Log previous tooltip was dismissed + }, + onEducationClickAction = { + openHandleMenuCallback(captionState.runningTaskInfo.taskId) + }, + ) + windowingEducationViewController.showEducationTooltip( + taskId = captionState.runningTaskInfo.taskId, + tooltipViewConfig = exitWindowingTooltipConfig, + ) } /** @@ -319,43 +277,20 @@ class AppHandleEducationController( this.toDesktopModeCallback = toDesktopModeCallback } - private inline fun <T> Flow<T>.catchTimeoutAndLog(crossinline block: () -> Unit) = - catch { exception -> - if (exception is TimeoutCancellationException) block() else throw exception - } - - private fun launchWithExceptionHandling(block: suspend () -> Unit) = - applicationCoroutineScope.launch { - try { - block() - } catch (e: Throwable) { - Slog.e(TAG, "Error: ", e) - } - } + private suspend fun isAppHandleHintViewed(): Boolean = + appHandleEducationDatastoreRepository.dataStoreFlow + .first() + .hasAppHandleHintViewedTimestampMillis() && !FORCE_SHOW_DESKTOP_MODE_EDUCATION - /** - * Listens to the changes to [WindowingEducationProto#hasAppHandleHintViewedTimestampMillis()] - * in datastore proto object. - * - * If [SHOULD_OVERRIDE_EDUCATION_CONDITIONS] is true, this flow will always emit false. That - * means it will always emit app handle hint has not been viewed yet. - */ - private fun isAppHandleHintViewedFlow(): Flow<Boolean> = + private suspend fun isEnterDesktopModeHintViewed(): Boolean = appHandleEducationDatastoreRepository.dataStoreFlow - .map { preferences -> - preferences.hasAppHandleHintViewedTimestampMillis() && - !SHOULD_OVERRIDE_EDUCATION_CONDITIONS - } - .distinctUntilChanged() + .first() + .hasEnterDesktopModeHintViewedTimestampMillis() && !FORCE_SHOW_DESKTOP_MODE_EDUCATION - /** - * Listens to the changes to [WindowingEducationProto#hasAppHandleHintUsedTimestampMillis()] in - * datastore proto object. - */ - private suspend fun isAppHandleHintUsed(): Boolean = + private suspend fun isExitDesktopModeHintViewed(): Boolean = appHandleEducationDatastoreRepository.dataStoreFlow .first() - .hasAppHandleHintUsedTimestampMillis() + .hasExitDesktopModeHintViewedTimestampMillis() && !FORCE_SHOW_DESKTOP_MODE_EDUCATION private fun getSize(@DimenRes resourceId: Int): Int { if (resourceId == Resources.ID_NULL) return 0 @@ -369,13 +304,17 @@ class AppHandleEducationController( val APP_HANDLE_EDUCATION_DELAY_MILLIS: Long get() = SystemProperties.getLong("persist.windowing_app_handle_education_delay", 3000L) - val APP_HANDLE_EDUCATION_TIMEOUT_MILLIS: Long - get() = SystemProperties.getLong("persist.windowing_app_handle_education_timeout", 400L) + val ENTER_DESKTOP_MODE_EDUCATION_DELAY_MILLIS: Long + get() = + SystemProperties.getLong( + "persist.windowing_enter_desktop_mode_education_timeout", + 400L, + ) - val SHOULD_OVERRIDE_EDUCATION_CONDITIONS: Boolean + val FORCE_SHOW_DESKTOP_MODE_EDUCATION: Boolean get() = SystemProperties.getBoolean( - "persist.desktop_windowing_app_handle_education_override_conditions", + "persist.windowing_force_show_desktop_mode_education", false, ) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilter.kt index 9990846fc92e..4d219b5544aa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilter.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilter.kt @@ -17,13 +17,14 @@ package com.android.wm.shell.desktopmode.education import android.annotation.IntegerRes +import android.app.ActivityManager.RunningTaskInfo import android.app.usage.UsageStatsManager import android.content.Context import android.os.SystemClock import android.provider.Settings.Secure import com.android.wm.shell.R import com.android.wm.shell.desktopmode.CaptionState -import com.android.wm.shell.desktopmode.education.AppHandleEducationController.Companion.SHOULD_OVERRIDE_EDUCATION_CONDITIONS +import com.android.wm.shell.desktopmode.education.AppHandleEducationController.Companion.FORCE_SHOW_DESKTOP_MODE_EDUCATION import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository import com.android.wm.shell.desktopmode.education.data.WindowingEducationProto import java.time.Duration @@ -37,26 +38,28 @@ class AppHandleEducationFilter( private val usageStatsManager = context.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager + suspend fun shouldShowDesktopModeEducation(captionState: CaptionState.AppHeader): Boolean = + shouldShowDesktopModeEducation(captionState.runningTaskInfo) + + suspend fun shouldShowDesktopModeEducation(captionState: CaptionState.AppHandle): Boolean = + shouldShowDesktopModeEducation(captionState.runningTaskInfo) + /** - * Returns true if conditions to show app handle education are met, returns false otherwise. + * Returns true if conditions to show app handle, enter desktop mode and exit desktop mode + * education are met based on the app info and usage, returns false otherwise. * - * If [SHOULD_OVERRIDE_EDUCATION_CONDITIONS] is true, this method will always return - * ![captionState.isHandleMenuExpanded]. + * If [FORCE_SHOW_DESKTOP_MODE_EDUCATION] is true, this method will always return true. */ - suspend fun shouldShowAppHandleEducation(captionState: CaptionState): Boolean { - if ((captionState as CaptionState.AppHandle).isHandleMenuExpanded) return false - if (SHOULD_OVERRIDE_EDUCATION_CONDITIONS) return true + private suspend fun shouldShowDesktopModeEducation(taskInfo: RunningTaskInfo): Boolean { + if (FORCE_SHOW_DESKTOP_MODE_EDUCATION) return true - val focusAppPackageName = - captionState.runningTaskInfo.topActivityInfo?.packageName ?: return false + val focusAppPackageName = taskInfo.topActivityInfo?.packageName ?: return false val windowingEducationProto = appHandleEducationDatastoreRepository.windowingEducationProto() return isFocusAppInAllowlist(focusAppPackageName) && !isOtherEducationShowing() && hasSufficientTimeSinceSetup() && - !isAppHandleHintViewedBefore(windowingEducationProto) && - !isAppHandleHintUsedBefore(windowingEducationProto) && hasMinAppUsage(windowingEducationProto, focusAppPackageName) } @@ -79,14 +82,6 @@ class AppHandleEducationFilter( R.integer.desktop_windowing_education_required_time_since_setup_seconds ) - private fun isAppHandleHintViewedBefore( - windowingEducationProto: WindowingEducationProto - ): Boolean = windowingEducationProto.hasAppHandleHintViewedTimestampMillis() - - private fun isAppHandleHintUsedBefore( - windowingEducationProto: WindowingEducationProto - ): Boolean = windowingEducationProto.hasAppHandleHintUsedTimestampMillis() - private suspend fun hasMinAppUsage( windowingEducationProto: WindowingEducationProto, focusAppPackageName: String, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt index 3e120b09a0b6..d061e03b9be5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt @@ -91,6 +91,40 @@ constructor(private val dataStore: DataStore<WindowingEducationProto>) { } /** + * Updates [WindowingEducationProto.enterDesktopModeHintViewedTimestampMillis_] field in + * datastore with current timestamp if [isViewed] is true, if not then clears the field. + */ + suspend fun updateEnterDesktopModeHintViewedTimestampMillis(isViewed: Boolean) { + dataStore.updateData { preferences -> + if (isViewed) { + preferences + .toBuilder() + .setEnterDesktopModeHintViewedTimestampMillis(System.currentTimeMillis()) + .build() + } else { + preferences.toBuilder().clearEnterDesktopModeHintViewedTimestampMillis().build() + } + } + } + + /** + * Updates [WindowingEducationProto.exitDesktopModeHintViewedTimestampMillis_] field in + * datastore with current timestamp if [isViewed] is true, if not then clears the field. + */ + suspend fun updateExitDesktopModeHintViewedTimestampMillis(isViewed: Boolean) { + dataStore.updateData { preferences -> + if (isViewed) { + preferences + .toBuilder() + .setExitDesktopModeHintViewedTimestampMillis(System.currentTimeMillis()) + .build() + } else { + preferences.toBuilder().clearExitDesktopModeHintViewedTimestampMillis().build() + } + } + } + + /** * Updates [WindowingEducationProto.appHandleHintUsedTimestampMillis_] field in datastore with * current timestamp if [isViewed] is true, if not then clears the field. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt new file mode 100644 index 000000000000..5cbb59fbf323 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.desktopmode.multidesks + +import android.app.ActivityManager +import android.window.TransitionInfo +import android.window.WindowContainerTransaction + +/** An organizer of desk containers in which to host child desktop windows. */ +interface DesksOrganizer { + /** Creates a new desk container in the given display. */ + fun createDesk(displayId: Int, callback: OnCreateCallback) + + /** Activates the given desk, making it visible in its display. */ + fun activateDesk(wct: WindowContainerTransaction, deskId: Int) + + /** Removes the given desk and its desktop windows. */ + fun removeDesk(wct: WindowContainerTransaction, deskId: Int) + + /** Moves the given task to the given desk. */ + fun moveTaskToDesk( + wct: WindowContainerTransaction, + deskId: Int, + task: ActivityManager.RunningTaskInfo, + ) + + /** + * Returns the desk id in which the task in the given change is located at the end of a + * transition, if any. + */ + fun getDeskAtEnd(change: TransitionInfo.Change): Int? + + /** A callback that is invoked when the desk container is created. */ + fun interface OnCreateCallback { + /** Calls back when the [deskId] has been created. */ + fun onCreated(deskId: Int) + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/OnDeskRemovedListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/OnDeskRemovedListener.kt new file mode 100644 index 000000000000..452ddb1ff8fb --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/OnDeskRemovedListener.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.desktopmode.multidesks + +/** A listener for removals of desks. */ +fun interface OnDeskRemovedListener { + /** Called when a desk has been removed from the system. */ + fun onDeskRemoved(lastDisplayId: Int, deskId: Int) +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt new file mode 100644 index 000000000000..79c48c5e9594 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.desktopmode.multidesks + +import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD +import android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED +import android.util.SparseArray +import android.view.SurfaceControl +import android.window.TransitionInfo +import android.window.WindowContainerTransaction +import androidx.core.util.forEach +import com.android.internal.annotations.VisibleForTesting +import com.android.internal.protolog.ProtoLog +import com.android.window.flags.Flags +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer.OnCreateCallback +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE +import com.android.wm.shell.sysui.ShellCommandHandler +import com.android.wm.shell.sysui.ShellInit +import java.io.PrintWriter + +/** A [DesksOrganizer] that uses root tasks as the container of each desk. */ +class RootTaskDesksOrganizer( + shellInit: ShellInit, + shellCommandHandler: ShellCommandHandler, + private val shellTaskOrganizer: ShellTaskOrganizer, +) : DesksOrganizer, ShellTaskOrganizer.TaskListener { + + private val deskCreateRequests = mutableListOf<CreateRequest>() + @VisibleForTesting val roots = SparseArray<DeskRoot>() + + init { + if (Flags.enableMultipleDesktopsBackend()) { + shellInit.addInitCallback( + { shellCommandHandler.addDumpCallback(this::dump, this) }, + this, + ) + } + } + + override fun createDesk(displayId: Int, callback: OnCreateCallback) { + logV("createDesk in display: %d", displayId) + deskCreateRequests += CreateRequest(displayId, callback) + shellTaskOrganizer.createRootTask( + displayId, + WINDOWING_MODE_FREEFORM, + /* listener = */ this, + /* removeWithTaskOrganizer = */ true, + ) + } + + override fun removeDesk(wct: WindowContainerTransaction, deskId: Int) { + logV("removeDesk %d", deskId) + val desk = checkNotNull(roots[deskId]) { "Root not found for desk: $deskId" } + wct.removeRootTask(desk.taskInfo.token) + } + + override fun activateDesk(wct: WindowContainerTransaction, deskId: Int) { + logV("activateDesk %d", deskId) + val root = checkNotNull(roots[deskId]) { "Root not found for desk: $deskId" } + wct.reorder(root.taskInfo.token, /* onTop= */ true) + wct.setLaunchRoot( + /* container= */ root.taskInfo.token, + /* windowingModes= */ intArrayOf(WINDOWING_MODE_FREEFORM, WINDOWING_MODE_UNDEFINED), + /* activityTypes= */ intArrayOf(ACTIVITY_TYPE_UNDEFINED, ACTIVITY_TYPE_STANDARD), + ) + } + + override fun moveTaskToDesk( + wct: WindowContainerTransaction, + deskId: Int, + task: RunningTaskInfo, + ) { + val root = roots[deskId] ?: error("Root not found for desk: $deskId") + wct.reparent(task.token, root.taskInfo.token, /* onTop= */ true) + } + + override fun getDeskAtEnd(change: TransitionInfo.Change): Int? = + change.taskInfo?.parentTaskId?.takeIf { it in roots } + + override fun onTaskAppeared(taskInfo: RunningTaskInfo, leash: SurfaceControl) { + if (taskInfo.parentTaskId in roots) { + val deskId = taskInfo.parentTaskId + val taskId = taskInfo.taskId + logV("Task #$taskId appeared in desk #$deskId") + addChildToDesk(taskId = taskId, deskId = deskId) + return + } + val deskId = taskInfo.taskId + check(deskId !in roots) { "A root already exists for desk: $deskId" } + val request = + checkNotNull(deskCreateRequests.firstOrNull { it.displayId == taskInfo.displayId }) { + "Task ${taskInfo.taskId} appeared without pending create request" + } + logV("Desk #$deskId appeared") + roots[deskId] = DeskRoot(deskId, taskInfo, leash) + deskCreateRequests.remove(request) + request.onCreateCallback.onCreated(deskId) + } + + override fun onTaskInfoChanged(taskInfo: RunningTaskInfo) { + if (roots.contains(taskInfo.taskId)) { + val deskId = taskInfo.taskId + roots[deskId] = roots[deskId].copy(taskInfo = taskInfo) + } + } + + override fun onTaskVanished(taskInfo: RunningTaskInfo) { + if (roots.contains(taskInfo.taskId)) { + val deskId = taskInfo.taskId + val deskRoot = roots[deskId] + // Use the last saved taskInfo to obtain the displayId. Using the local one here will + // return -1 since the task is not unassociated with a display. + val displayId = deskRoot.taskInfo.displayId + logV("Desk #$deskId vanished from display #$displayId") + roots.remove(deskId) + return + } + // At this point, [parentTaskId] may be unset even if this is a task vanishing from a desk, + // so search through each root to remove this if it's a child. + roots.forEach { deskId, deskRoot -> + if (deskRoot.children.remove(taskInfo.taskId)) { + logV("Task #${taskInfo.taskId} vanished from desk #$deskId") + return + } + } + } + + @VisibleForTesting + data class DeskRoot( + val deskId: Int, + val taskInfo: RunningTaskInfo, + val leash: SurfaceControl, + val children: MutableSet<Int> = mutableSetOf(), + ) + + override fun dump(pw: PrintWriter, prefix: String) { + val innerPrefix = "$prefix " + pw.println("$prefix$TAG") + pw.println("${innerPrefix}Desk Roots:") + roots.forEach { deskId, root -> + pw.println("$innerPrefix #$deskId visible=${root.taskInfo.isVisible}") + pw.println("$innerPrefix children=${root.children}") + } + } + + private fun addChildToDesk(taskId: Int, deskId: Int) { + roots.forEach { _, deskRoot -> + if (deskRoot.deskId == deskId) { + deskRoot.children.add(taskId) + } else { + deskRoot.children.remove(taskId) + } + } + } + + private data class CreateRequest(val displayId: Int, val onCreateCallback: OnCreateCallback) + + private fun logV(msg: String, vararg arguments: Any?) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + + companion object { + private const val TAG = "RootTaskDesksOrganizer" + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt index 58a49a035bb6..5a89451ffdbc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt @@ -18,6 +18,7 @@ package com.android.wm.shell.desktopmode.persistence import android.content.Context import android.window.DesktopModeFlags +import com.android.window.flags.Flags import com.android.wm.shell.desktopmode.DesktopRepository import com.android.wm.shell.desktopmode.DesktopUserRepositories import com.android.wm.shell.shared.annotations.ShellMainThread @@ -54,10 +55,22 @@ class DesktopRepositoryInitializerImpl( DesktopModeStatus.getMaxTaskLimit(context).takeIf { it > 0 } ?: persistentDesktop.zOrderedTasksCount var visibleTasksCount = 0 + repository.addDesk( + displayId = persistentDesktop.displayId, + deskId = + if (Flags.enableMultipleDesktopsBackend()) { + persistentDesktop.desktopId + } else { + // When disabled, desk ids are always the display id. + persistentDesktop.displayId + }, + ) persistentDesktop.zOrderedTasksList // Reverse it so we initialize the repo from bottom to top. .reversed() .mapNotNull { taskId -> persistentDesktop.tasksByTaskIdMap[taskId] } + // TODO: b/362720497 - add tasks to their respective desk when multi-desk + // persistence is implemented. .forEach { task -> if ( task.desktopTaskState == DesktopTaskState.VISIBLE && diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java index e24b2c5f0134..e8996bc03eeb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java @@ -31,6 +31,7 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMA import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; +import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.PendingIntent; @@ -125,6 +126,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll * drag. */ default boolean onUnhandledDrag(@NonNull PendingIntent launchIntent, + @UserIdInt int userId, @NonNull DragEvent dragEvent, @NonNull Consumer<Boolean> onFinishCallback) { return false; @@ -444,8 +446,10 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll return; } + // TODO(b/391624027): Consider piping through launch intent user if needed later + final int userId = launchIntent.getCreatorUserHandle().getIdentifier(); final boolean handled = notifyListeners( - l -> l.onUnhandledDrag(launchIntent, dragEvent, onFinishCallback)); + l -> l.onUnhandledDrag(launchIntent, userId, dragEvent, onFinishCallback)); if (!handled) { // Nobody handled this, we still have to notify WM onFinishCallback.accept(false); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java index b38a853321a7..897e2d1601a5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java @@ -29,6 +29,7 @@ import android.window.DesktopModeFlags; import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.LaunchAdjacentController; +import com.android.wm.shell.desktopmode.DesktopModeLoggerTransitionObserver; import com.android.wm.shell.desktopmode.DesktopRepository; import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.desktopmode.DesktopUserRepositories; @@ -52,6 +53,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, private final ShellTaskOrganizer mShellTaskOrganizer; private final Optional<DesktopUserRepositories> mDesktopUserRepositories; private final Optional<DesktopTasksController> mDesktopTasksController; + private final DesktopModeLoggerTransitionObserver mDesktopModeLoggerTransitionObserver; private final WindowDecorViewModel mWindowDecorationViewModel; private final LaunchAdjacentController mLaunchAdjacentController; private final Optional<TaskChangeListener> mTaskChangeListener; @@ -64,6 +66,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, ShellTaskOrganizer shellTaskOrganizer, Optional<DesktopUserRepositories> desktopUserRepositories, Optional<DesktopTasksController> desktopTasksController, + DesktopModeLoggerTransitionObserver desktopModeLoggerTransitionObserver, LaunchAdjacentController launchAdjacentController, WindowDecorViewModel windowDecorationViewModel, Optional<TaskChangeListener> taskChangeListener) { @@ -72,6 +75,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, mWindowDecorationViewModel = windowDecorationViewModel; mDesktopUserRepositories = desktopUserRepositories; mDesktopTasksController = desktopTasksController; + mDesktopModeLoggerTransitionObserver = desktopModeLoggerTransitionObserver; mLaunchAdjacentController = launchAdjacentController; mTaskChangeListener = taskChangeListener; if (shellInit != null) { @@ -130,6 +134,9 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, repository.removeTask(taskInfo.displayId, taskInfo.taskId); } } + // TODO: b/367268649 - This listener shouldn't need to call the transition observer directly + // for logging once the logic in the observer is moved. + mDesktopModeLoggerTransitionObserver.onTaskVanished(taskInfo); mWindowDecorationViewModel.onTaskVanished(taskInfo); updateLaunchAdjacentController(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java index 44900ce1db8a..65099c2dfb9d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java @@ -38,6 +38,7 @@ import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.common.pip.PipMediaController; import com.android.wm.shell.common.pip.PipMediaController.ActionListener; import com.android.wm.shell.common.pip.PipMenuController; @@ -121,6 +122,9 @@ public class PhonePipMenuController implements PipMenuController, @NonNull private final PipTransitionState mPipTransitionState; + @NonNull + private final PipDisplayLayoutState mPipDisplayLayoutState; + private SurfaceControl mLeash; private ActionListener mMediaActionListener = new ActionListener() { @@ -134,7 +138,8 @@ public class PhonePipMenuController implements PipMenuController, public PhonePipMenuController(Context context, PipBoundsState pipBoundsState, PipMediaController mediaController, SystemWindows systemWindows, PipUiEventLogger pipUiEventLogger, PipTaskListener pipTaskListener, - @NonNull PipTransitionState pipTransitionState, ShellExecutor mainExecutor, + @NonNull PipTransitionState pipTransitionState, + @NonNull PipDisplayLayoutState pipDisplayLayoutState, ShellExecutor mainExecutor, Handler mainHandler) { mContext = context; mPipBoundsState = pipBoundsState; @@ -142,6 +147,7 @@ public class PhonePipMenuController implements PipMenuController, mSystemWindows = systemWindows; mPipTaskListener = pipTaskListener; mPipTransitionState = pipTransitionState; + mPipDisplayLayoutState = pipDisplayLayoutState; mMainExecutor = mainExecutor; mMainHandler = mainHandler; mPipUiEventLogger = pipUiEventLogger; @@ -218,7 +224,7 @@ public class PhonePipMenuController implements PipMenuController, mSystemWindows.addView(mPipMenuView, getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, 0 /* width */, 0 /* height */), - 0, SHELL_ROOT_LAYER_PIP); + mPipDisplayLayoutState.getDisplayId(), SHELL_ROOT_LAYER_PIP); setShellRootAccessibilityWindow(); // Make sure the initial actions are set diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java index b1984ccef4cb..99c9302edb75 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java @@ -19,6 +19,7 @@ package com.android.wm.shell.pip2.phone; import static android.app.WindowConfiguration.ROTATION_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE; +import static android.view.Display.DEFAULT_DISPLAY; import android.annotation.NonNull; import android.app.ActivityManager; @@ -219,6 +220,7 @@ public class PipController implements ConfigurationChangeListener, mPipDisplayLayoutState.setDisplayLayout(layout); mDisplayController.addDisplayChangingController(this); + mDisplayController.addDisplayWindowListener(this); mDisplayInsetsController.addInsetsChangedListener(mPipDisplayLayoutState.getDisplayId(), new ImeListener(mDisplayController, mPipDisplayLayoutState.getDisplayId()) { @Override @@ -297,6 +299,22 @@ public class PipController implements ConfigurationChangeListener, setDisplayLayout(mDisplayController.getDisplayLayout(displayId)); } + @Override + public void onDisplayRemoved(int displayId) { + // If PiP was active on an external display that is removed, clean up states and set + // {@link PipDisplayLayoutState} to DEFAULT_DISPLAY. + if (Flags.enableConnectedDisplaysPip() && mPipTransitionState.isInPip() + && displayId == mPipDisplayLayoutState.getDisplayId() + && displayId != DEFAULT_DISPLAY) { + mPipTransitionState.setState(PipTransitionState.EXITING_PIP); + mPipTransitionState.setState(PipTransitionState.EXITED_PIP); + + mPipDisplayLayoutState.setDisplayId(DEFAULT_DISPLAY); + mPipDisplayLayoutState.setDisplayLayout( + mDisplayController.getDisplayLayout(DEFAULT_DISPLAY)); + } + } + /** * A callback for any observed transition that contains a display change in its * {@link android.window.TransitionRequestInfo}, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java index b3070f29c6e2..71697596afd3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java @@ -23,6 +23,7 @@ import android.content.res.Resources; import android.graphics.PixelFormat; import android.graphics.Point; import android.graphics.Rect; +import android.view.Display; import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.View; @@ -34,7 +35,9 @@ import androidx.annotation.NonNull; import com.android.wm.shell.R; import com.android.wm.shell.bubbles.DismissViewUtils; +import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.shared.bubbles.DismissCircleView; import com.android.wm.shell.shared.bubbles.DismissView; @@ -50,6 +53,9 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen /* The multiplier to apply scale the target size by when applying the magnetic field radius */ private static final float MAGNETIC_FIELD_RADIUS_MULTIPLIER = 1.25f; + /* The window type to apply to the display */ + private static final int WINDOW_TYPE = WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; + /** * MagnetizedObject wrapper for PIP. This allows the magnetic target library to locate and move * PIP. @@ -84,16 +90,22 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen private final Context mContext; private final PipMotionHelper mMotionHelper; private final PipUiEventLogger mPipUiEventLogger; - private final WindowManager mWindowManager; + private WindowManager mWindowManager; + /** The display id for the display that is associated with mWindowManager. */ + private int mWindowManagerDisplayId = -1; + private final PipDisplayLayoutState mPipDisplayLayoutState; + private final DisplayController mDisplayController; private final ShellExecutor mMainExecutor; public PipDismissTargetHandler(Context context, PipUiEventLogger pipUiEventLogger, - PipMotionHelper motionHelper, ShellExecutor mainExecutor) { + PipMotionHelper motionHelper, PipDisplayLayoutState pipDisplayLayoutState, + DisplayController displayController, ShellExecutor mainExecutor) { mContext = context; mPipUiEventLogger = pipUiEventLogger; mMotionHelper = motionHelper; + mPipDisplayLayoutState = pipDisplayLayoutState; + mDisplayController = displayController; mMainExecutor = mainExecutor; - mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); } void init() { @@ -240,6 +252,8 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen /** Adds the magnetic target view to the WindowManager so it's ready to be animated in. */ public void createOrUpdateDismissTarget() { + getWindowManager(); + if (mTargetViewContainer.getParent() == null) { mTargetViewContainer.cancelAnimators(); @@ -262,7 +276,7 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen WindowManager.LayoutParams.MATCH_PARENT, height, 0, windowSize.y - height, - WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, + WINDOW_TYPE, WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, @@ -308,4 +322,16 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen mWindowManager.removeViewImmediate(mTargetViewContainer); } } + + /** Sets mWindowManager to WindowManager associated with the display where PiP is active on. */ + private void getWindowManager() { + final int pipDisplayId = mPipDisplayLayoutState.getDisplayId(); + if (mWindowManager != null && pipDisplayId == mWindowManagerDisplayId) { + return; + } + mWindowManagerDisplayId = pipDisplayId; + Display display = mDisplayController.getDisplay(mWindowManagerDisplayId); + Context uiContext = mContext.createWindowContext(display, WINDOW_TYPE, null); + mWindowManager = uiContext.getSystemService(WindowManager.class); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipInputConsumer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipInputConsumer.java index ffda56d89276..0a0ecffbea1f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipInputConsumer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipInputConsumer.java @@ -16,8 +16,6 @@ package com.android.wm.shell.pip2.phone; -import static android.view.Display.DEFAULT_DISPLAY; - import android.os.Binder; import android.os.IBinder; import android.os.Looper; @@ -30,6 +28,7 @@ import android.view.InputEvent; import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.protolog.ShellProtoLogGroup; import java.io.PrintWriter; @@ -84,6 +83,7 @@ public class PipInputConsumer { private final IWindowManager mWindowManager; private final IBinder mToken; private final String mName; + private final PipDisplayLayoutState mPipDisplayLayoutState; private final ShellExecutor mMainExecutor; private InputEventReceiver mInputEventReceiver; @@ -94,10 +94,11 @@ public class PipInputConsumer { * @param name the name corresponding to the input consumer that is defined in the system. */ public PipInputConsumer(IWindowManager windowManager, String name, - ShellExecutor mainExecutor) { + PipDisplayLayoutState pipDisplayLayoutState, ShellExecutor mainExecutor) { mWindowManager = windowManager; mToken = new Binder(); mName = name; + mPipDisplayLayoutState = pipDisplayLayoutState; mMainExecutor = mainExecutor; } @@ -138,9 +139,9 @@ public class PipInputConsumer { } final InputChannel inputChannel = new InputChannel(); try { - // TODO(b/113087003): Support Picture-in-picture in multi-display. - mWindowManager.destroyInputConsumer(mToken, DEFAULT_DISPLAY); - mWindowManager.createInputConsumer(mToken, mName, DEFAULT_DISPLAY, inputChannel); + final int displayId = mPipDisplayLayoutState.getDisplayId(); + mWindowManager.destroyInputConsumer(mToken, displayId); + mWindowManager.createInputConsumer(mToken, mName, displayId, inputChannel); } catch (RemoteException e) { ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Failed to create input consumer, %s", TAG, e); @@ -162,8 +163,7 @@ public class PipInputConsumer { return; } try { - // TODO(b/113087003): Support Picture-in-picture in multi-display. - mWindowManager.destroyInputConsumer(mToken, DEFAULT_DISPLAY); + mWindowManager.destroyInputConsumer(mToken, mPipDisplayLayoutState.getDisplayId()); } catch (RemoteException e) { ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Failed to destroy input consumer, %s", TAG, e); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java index d98be55f28e1..e4be3f60f86e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java @@ -44,6 +44,7 @@ import com.android.wm.shell.R; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.common.pip.PipPerfHintController; import com.android.wm.shell.common.pip.PipPinchResizingAlgorithm; import com.android.wm.shell.common.pip.PipUiEventLogger; @@ -70,9 +71,9 @@ public class PipResizeGestureHandler implements private final PipScheduler mPipScheduler; private final PipTransitionState mPipTransitionState; private final PhonePipMenuController mPhonePipMenuController; + private final PipDisplayLayoutState mPipDisplayLayoutState; private final PipUiEventLogger mPipUiEventLogger; private final PipPinchResizingAlgorithm mPinchResizingAlgorithm; - private final int mDisplayId; private final ShellExecutor mMainExecutor; private final PointF mDownPoint = new PointF(); @@ -120,10 +121,10 @@ public class PipResizeGestureHandler implements PipTransitionState pipTransitionState, PipUiEventLogger pipUiEventLogger, PhonePipMenuController menuActivityController, + PipDisplayLayoutState pipDisplayLayoutState, ShellExecutor mainExecutor, @Nullable PipPerfHintController pipPerfHintController) { mContext = context; - mDisplayId = context.getDisplayId(); mMainExecutor = mainExecutor; mPipPerfHintController = pipPerfHintController; mPipBoundsAlgorithm = pipBoundsAlgorithm; @@ -135,6 +136,7 @@ public class PipResizeGestureHandler implements mPipTransitionState.addPipTransitionStateChangedListener(this); mPhonePipMenuController = menuActivityController; + mPipDisplayLayoutState = pipDisplayLayoutState; mPipUiEventLogger = pipUiEventLogger; mPinchResizingAlgorithm = new PipPinchResizingAlgorithm(); } @@ -197,7 +199,7 @@ public class PipResizeGestureHandler implements if (mIsEnabled) { // Register input event receiver mInputMonitor = mContext.getSystemService(InputManager.class).monitorGestureInput( - "pip-resize", mDisplayId); + "pip-resize", mPipDisplayLayoutState.getDisplayId()); try { mMainExecutor.executeBlocking(() -> { mInputEventReceiver = new PipResizeInputEventReceiver( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java index fc3fbe299605..35cd1a2e681f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java @@ -54,10 +54,12 @@ import android.view.accessibility.AccessibilityWindowInfo; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.R; +import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.common.pip.PipDoubleTapHelper; import com.android.wm.shell.common.pip.PipPerfHintController; import com.android.wm.shell.common.pip.PipUiEventLogger; @@ -91,6 +93,7 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha @NonNull private final PipTransitionState mPipTransitionState; @NonNull private final PipScheduler mPipScheduler; @NonNull private final SizeSpecSource mSizeSpecSource; + @NonNull private final PipDisplayLayoutState mPipDisplayLayoutState; private final PipUiEventLogger mPipUiEventLogger; private final PipDismissTargetHandler mPipDismissTargetHandler; private final ShellExecutor mMainExecutor; @@ -183,6 +186,8 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha @NonNull PipTransitionState pipTransitionState, @NonNull PipScheduler pipScheduler, @NonNull SizeSpecSource sizeSpecSource, + @NonNull PipDisplayLayoutState pipDisplayLayoutState, + DisplayController displayController, PipMotionHelper pipMotionHelper, FloatingContentCoordinator floatingContentCoordinator, PipUiEventLogger pipUiEventLogger, @@ -200,6 +205,7 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha mPipTransitionState.addPipTransitionStateChangedListener(this::onPipTransitionStateChanged); mPipScheduler = pipScheduler; mSizeSpecSource = sizeSpecSource; + mPipDisplayLayoutState = pipDisplayLayoutState; mMenuController = menuController; mPipUiEventLogger = pipUiEventLogger; mFloatingContentCoordinator = floatingContentCoordinator; @@ -208,7 +214,7 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha mMotionHelper = pipMotionHelper; mPipScheduler.setUpdateMovementBoundsRunnable(this::updateMovementBounds); mPipDismissTargetHandler = new PipDismissTargetHandler(context, pipUiEventLogger, - mMotionHelper, mainExecutor); + mMotionHelper, mPipDisplayLayoutState, displayController, mainExecutor); mTouchState = new PipTouchState(ViewConfiguration.get(context), () -> { mMenuController.showMenuWithPossibleDelay(MENU_STATE_FULL, @@ -220,8 +226,7 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha mainExecutor); mPipResizeGestureHandler = new PipResizeGestureHandler(context, pipBoundsAlgorithm, pipBoundsState, mTouchState, mPipScheduler, mPipTransitionState, pipUiEventLogger, - menuController, mainExecutor, - mPipPerfHintController); + menuController, mPipDisplayLayoutState, mainExecutor, mPipPerfHintController); mPipBoundsState.addOnAspectRatioChangedCallback(aspectRatio -> { updateMinMaxSize(aspectRatio); onAspectRatioChanged(); @@ -264,7 +269,7 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha mPipDismissTargetHandler.init(); mPipInputConsumer = new PipInputConsumer(WindowManagerGlobal.getWindowManagerService(), - INPUT_CONSUMER_PIP, mMainExecutor); + INPUT_CONSUMER_PIP, mPipDisplayLayoutState, mMainExecutor); mPipInputConsumer.setInputListener(this::handleTouchEvent); mPipInputConsumer.setRegistrationListener(this::onRegistrationChanged); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index 272cb4372acf..03327bf463e3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -70,6 +70,7 @@ import com.android.wm.shell.desktopmode.DesktopRepository; import com.android.wm.shell.desktopmode.DesktopUserRepositories; import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider; import com.android.wm.shell.pip.PipTransitionController; +import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; import com.android.wm.shell.pip2.animation.PipAlphaAnimator; import com.android.wm.shell.pip2.animation.PipEnterAnimator; import com.android.wm.shell.pip2.animation.PipExpandAnimator; @@ -115,6 +116,7 @@ public class PipTransition extends PipTransitionController implements private final PipTransitionState mPipTransitionState; private final PipDisplayLayoutState mPipDisplayLayoutState; private final DisplayController mDisplayController; + private final PipSurfaceTransactionHelper mPipSurfaceTransactionHelper; private final Optional<DesktopUserRepositories> mDesktopUserRepositoriesOptional; private final Optional<DesktopWallpaperActivityTokenProvider> mDesktopWallpaperActivityTokenProviderOptional; @@ -171,6 +173,7 @@ public class PipTransition extends PipTransitionController implements mPipTransitionState.addPipTransitionStateChangedListener(this); mPipDisplayLayoutState = pipDisplayLayoutState; mDisplayController = displayController; + mPipSurfaceTransactionHelper = new PipSurfaceTransactionHelper(mContext); mDesktopUserRepositoriesOptional = desktopUserRepositoriesOptional; mDesktopWallpaperActivityTokenProviderOptional = desktopWallpaperActivityTokenProviderOptional; @@ -343,6 +346,25 @@ public class PipTransition extends PipTransitionController implements } } + @Override + public boolean syncPipSurfaceState(@NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction) { + final TransitionInfo.Change pipChange = getPipChange(info); + if (pipChange == null) return false; + + // add shadow and corner radii + final SurfaceControl leash = pipChange.getLeash(); + final boolean isInPip = mPipTransitionState.isInPip(); + + mPipSurfaceTransactionHelper.round(startTransaction, leash, isInPip) + .shadow(startTransaction, leash, isInPip); + mPipSurfaceTransactionHelper.round(finishTransaction, leash, isInPip) + .shadow(finishTransaction, leash, isInPip); + + return true; + } + // // Animation schedulers and entry points // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java index 2d4d458292ea..4f2e028a1df0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java @@ -311,6 +311,9 @@ public class RecentTasksController implements TaskStackListenerCallback, public void onTaskAdded(RunningTaskInfo taskInfo) { notifyRunningTaskAppeared(taskInfo); + if (!enableShellTopTaskTracking()) { + notifyRecentTasksChanged(); + } } public void onTaskRemoved(RunningTaskInfo taskInfo) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index afc6fee2eca3..55133780f517 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -222,7 +222,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, RecentsMixedHandler mixer = null; Consumer<IBinder> setTransitionForMixer = null; for (int i = 0; i < mMixers.size(); ++i) { - setTransitionForMixer = mMixers.get(i).handleRecentsRequest(wct); + setTransitionForMixer = mMixers.get(i).handleRecentsRequest(); if (setTransitionForMixer != null) { mixer = mMixers.get(i); break; @@ -1455,6 +1455,11 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, } } + // Notify the mixers of the pending finish + for (int i = 0; i < mMixers.size(); ++i) { + mMixers.get(i).handleFinishRecents(returningToApp, wct, t); + } + if (Flags.enableRecentsBookendTransition()) { if (!wct.isEmpty()) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, @@ -1653,15 +1658,22 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, */ public interface RecentsMixedHandler extends Transitions.TransitionHandler { /** - * Called when a recents request comes in. The handler can add operations to outWCT. If - * the handler wants to "accept" the transition, it should return a Consumer accepting the - * IBinder for the transition. If not, it should return `null`. + * Called when a recents request comes in. If the handler wants to "accept" the transition, + * it should return a Consumer accepting the IBinder for the transition. If not, it should + * return `null`. * * If a mixed-handler accepts this recents, it will be the de-facto handler for this * transition and is required to call the associated {@link #startAnimation}, * {@link #mergeAnimation}, and {@link #onTransitionConsumed} methods. */ @Nullable - Consumer<IBinder> handleRecentsRequest(WindowContainerTransaction outWCT); + Consumer<IBinder> handleRecentsRequest(); + + /** + * Called when a recents transition has finished, with a WCT and SurfaceControl Transaction + * that can be used to add to any changes needed to restore the state. + */ + void handleFinishRecents(boolean returnToApp, @NonNull WindowContainerTransaction finishWct, + @NonNull SurfaceControl.Transaction finishT); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index 99a89a6b884f..ae0159263364 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -649,11 +649,12 @@ public class SplitScreenController implements SplitDragPolicy.Starter, @Nullable Bundle options, UserHandle user) { if (options == null) options = new Bundle(); final ActivityOptions activityOptions = ActivityOptions.fromBundle(options); + final int userId = user.getIdentifier(); if (samePackage(packageName, getPackageName(reverseSplitPosition(position), null), - user.getIdentifier(), getUserId(reverseSplitPosition(position), null))) { + userId, getUserId(reverseSplitPosition(position), null))) { if (mMultiInstanceHelpher.supportsMultiInstanceSplit( - getShortcutComponent(packageName, shortcutId, user, mLauncherApps))) { + getShortcutComponent(packageName, shortcutId, user, mLauncherApps), userId)) { activityOptions.setApplyMultipleTaskFlagForShortcut(true); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK"); } else if (isSplitScreenVisible()) { @@ -687,7 +688,8 @@ public class SplitScreenController implements SplitDragPolicy.Starter, final int userId1 = shortcutInfo.getUserId(); final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer); if (samePackage(packageName1, packageName2, userId1, userId2)) { - if (mMultiInstanceHelpher.supportsMultiInstanceSplit(shortcutInfo.getActivity())) { + if (mMultiInstanceHelpher.supportsMultiInstanceSplit(shortcutInfo.getActivity(), + userId1)) { activityOptions.setApplyMultipleTaskFlagForShortcut(true); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK"); } else { @@ -735,7 +737,8 @@ public class SplitScreenController implements SplitDragPolicy.Starter, final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer); boolean setSecondIntentMultipleTask = false; if (samePackage(packageName1, packageName2, userId1, userId2)) { - if (mMultiInstanceHelpher.supportsMultiInstanceSplit(getComponent(pendingIntent))) { + if (mMultiInstanceHelpher.supportsMultiInstanceSplit(getComponent(pendingIntent), + userId1)) { setSecondIntentMultipleTask = true; ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK"); } else { @@ -775,7 +778,8 @@ public class SplitScreenController implements SplitDragPolicy.Starter, ? ActivityOptions.fromBundle(options2) : ActivityOptions.makeBasic(); boolean setSecondIntentMultipleTask = false; if (samePackage(packageName1, packageName2, userId1, userId2)) { - if (mMultiInstanceHelpher.supportsMultiInstanceSplit(getComponent(pendingIntent1))) { + if (mMultiInstanceHelpher.supportsMultiInstanceSplit(getComponent(pendingIntent1), + userId1)) { fillInIntent1 = new Intent(); fillInIntent1.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); setSecondIntentMultipleTask = true; @@ -858,7 +862,7 @@ public class SplitScreenController implements SplitDragPolicy.Starter, return; } if (samePackage(packageName1, packageName2, userId1, userId2)) { - if (mMultiInstanceHelpher.supportsMultiInstanceSplit(getComponent(intent))) { + if (mMultiInstanceHelpher.supportsMultiInstanceSplit(getComponent(intent), userId1)) { // Flag with MULTIPLE_TASK if this is launching the same activity into both sides of // the split and there is no reusable background task. fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 511e426cc681..722494c05e32 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -129,6 +129,7 @@ import com.android.internal.logging.InstanceId; import com.android.internal.policy.FoldLockSettingsObserver; import com.android.internal.protolog.ProtoLog; import com.android.launcher3.icons.IconProvider; +import com.android.wm.shell.Flags; import com.android.wm.shell.R; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; @@ -3766,13 +3767,31 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mTaskOrganizer.applyTransaction(wct); } + public void onRecentsInSplitAnimationFinishing(boolean returnToApp, + @NonNull WindowContainerTransaction finishWct, + @NonNull SurfaceControl.Transaction finishT) { + if (!Flags.enableRecentsBookendTransition()) { + // The non-bookend recents transition case will be handled by + // RecentsMixedTransition wrapping the finish callback and calling + // onRecentsInSplitAnimationFinish() + return; + } + + onRecentsInSplitAnimationFinishInner(returnToApp, finishWct, finishT); + } + /** Call this when the recents animation during split-screen finishes. */ - public void onRecentsInSplitAnimationFinish(WindowContainerTransaction finishWct, - SurfaceControl.Transaction finishT) { - ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRecentsInSplitAnimationFinish"); - mPausingTasks.clear(); + public void onRecentsInSplitAnimationFinish(@NonNull WindowContainerTransaction finishWct, + @NonNull SurfaceControl.Transaction finishT) { + if (Flags.enableRecentsBookendTransition()) { + // The bookend recents transition case will be handled by + // onRecentsInSplitAnimationFinishing above + return; + } + // Check if the recent transition is finished by returning to the current // split, so we can restore the divider bar. + boolean returnToApp = false; for (int i = 0; i < finishWct.getHierarchyOps().size(); ++i) { final WindowContainerTransaction.HierarchyOp op = finishWct.getHierarchyOps().get(i); @@ -3787,13 +3806,26 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } if (op.getType() == HIERARCHY_OP_TYPE_REORDER && op.getToTop() && anyStageContainsContainer) { - updateSurfaceBounds(mSplitLayout, finishT, - false /* applyResizingOffset */); - finishT.reparent(mSplitLayout.getDividerLeash(), mRootTaskLeash); - setDividerVisibility(true, finishT); - return; + returnToApp = true; } } + onRecentsInSplitAnimationFinishInner(returnToApp, finishWct, finishT); + } + + /** Call this when the recents animation during split-screen finishes. */ + public void onRecentsInSplitAnimationFinishInner(boolean returnToApp, + @NonNull WindowContainerTransaction finishWct, + @NonNull SurfaceControl.Transaction finishT) { + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRecentsInSplitAnimationFinish: returnToApp=%b", + returnToApp); + mPausingTasks.clear(); + if (returnToApp) { + updateSurfaceBounds(mSplitLayout, finishT, + false /* applyResizingOffset */); + finishT.reparent(mSplitLayout.getDividerLeash(), mRootTaskLeash); + setDividerVisibility(true, finishT); + return; + } setSplitsVisible(false); finishWct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java index 0445add9cba9..13d87eda085b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java @@ -92,6 +92,10 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, getHolder().addCallback(this); } + public TaskViewTaskController getController() { + return mTaskViewTaskController; + } + /** * Launch a new activity. * diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java index d19a7eac6ad2..a0cc2bc8887b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java @@ -417,7 +417,8 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { } } - void notifyTaskRemovalStarted(@NonNull ActivityManager.RunningTaskInfo taskInfo) { + /** Notifies listeners of a task being removed. */ + public void notifyTaskRemovalStarted(@NonNull ActivityManager.RunningTaskInfo taskInfo) { if (mListener == null) return; final int taskId = taskInfo.taskId; mListenerExecutor.execute(() -> mListener.onTaskRemovalStarted(taskId)); @@ -448,7 +449,7 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { * have the pending info, we'll do it when we receive it in * {@link #onTaskAppeared(ActivityManager.RunningTaskInfo, SurfaceControl)}. */ - void setTaskNotFound() { + public void setTaskNotFound() { mTaskNotFound = true; if (mPendingInfo != null) { cleanUpPendingTask(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java index 6c90a9060523..1eaae7ec83d9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java @@ -20,6 +20,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_NONE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; @@ -66,7 +67,7 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV static final String TAG = "TaskViewTransitions"; /** - * Map of {@link TaskViewTaskController} to {@link TaskViewRequestedState}. + * Map of {@link TaskViewTaskController} to {@link TaskViewRepository.TaskViewState}. * <p> * {@link TaskView} keeps a reference to the {@link TaskViewTaskController} instance and * manages its lifecycle. @@ -95,6 +96,7 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV final @WindowManager.TransitionType int mType; final WindowContainerTransaction mWct; final @NonNull TaskViewTaskController mTaskView; + ExternalTransition mExternalTransition; IBinder mClaimed; /** @@ -182,6 +184,32 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV } /** + * Starts or queues an "external" runnable into the pending queue. This means it will run + * in order relative to the local transitions. + * + * The external operation *must* call {@link #onExternalDone} once it has finished. + * + * In practice, the external is usually another transition on a different handler. + */ + public void enqueueExternal(@NonNull TaskViewTaskController taskView, ExternalTransition ext) { + final PendingTransition pending = new PendingTransition( + TRANSIT_NONE, null /* wct */, taskView, null /* cookie */); + pending.mExternalTransition = ext; + mPending.add(pending); + startNextTransition(); + } + + /** + * An external transition run in this "queue" is required to call this once it becomes ready. + */ + public void onExternalDone(IBinder key) { + final PendingTransition pending = findPending(key); + if (pending == null) return; + mPending.remove(pending); + startNextTransition(); + } + + /** * Looks through the pending transitions for a opening transaction that matches the provided * `taskView`. * @@ -191,6 +219,7 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV PendingTransition findPendingOpeningTransition(TaskViewTaskController taskView) { for (int i = mPending.size() - 1; i >= 0; --i) { if (mPending.get(i).mTaskView != taskView) continue; + if (mPending.get(i).mExternalTransition != null) continue; if (TransitionUtil.isOpeningType(mPending.get(i).mType)) { return mPending.get(i); } @@ -207,6 +236,7 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV PendingTransition findPending(TaskViewTaskController taskView, int type) { for (int i = mPending.size() - 1; i >= 0; --i) { if (mPending.get(i).mTaskView != taskView) continue; + if (mPending.get(i).mExternalTransition != null) continue; if (mPending.get(i).mType == type) { return mPending.get(i); } @@ -518,7 +548,11 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV // Wait for this to start animating. return; } - pending.mClaimed = mTransitions.startTransition(pending.mType, pending.mWct, this); + if (pending.mExternalTransition != null) { + pending.mClaimed = pending.mExternalTransition.start(); + } else { + pending.mClaimed = mTransitions.startTransition(pending.mType, pending.mWct, this); + } } @Override @@ -641,7 +675,7 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV } @VisibleForTesting - void prepareOpenAnimation(TaskViewTaskController taskView, + public void prepareOpenAnimation(TaskViewTaskController taskView, final boolean newTask, SurfaceControl.Transaction startTransaction, SurfaceControl.Transaction finishTransaction, @@ -695,4 +729,10 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV taskView.notifyAppeared(newTask); } + + /** Interface for running an external transition in this object's pending queue. */ + public interface ExternalTransition { + /** Starts a transition and returns an identifying key for lookup. */ + IBinder start(); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java index 2177986bccd5..d8e7c2ccb15f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java @@ -367,7 +367,7 @@ public class DefaultMixedHandler implements MixedTransitionHandler, } @Override - public Consumer<IBinder> handleRecentsRequest(WindowContainerTransaction outWCT) { + public Consumer<IBinder> handleRecentsRequest() { if (mRecentsHandler != null) { if (mSplitHandler.isSplitScreenVisible()) { return this::setRecentsTransitionDuringSplit; @@ -383,6 +383,21 @@ public class DefaultMixedHandler implements MixedTransitionHandler, return null; } + @Override + public void handleFinishRecents(boolean returnToApp, + @NonNull WindowContainerTransaction finishWct, + @NonNull SurfaceControl.Transaction finishT) { + if (mRecentsHandler != null) { + for (int i = mActiveTransitions.size() - 1; i >= 0; --i) { + final MixedTransition mixed = mActiveTransitions.get(i); + if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_SPLIT) { + ((RecentsMixedTransition) mixed).onAnimateRecentsDuringSplitFinishing( + returnToApp, finishWct, finishT); + } + } + } + } + private void setRecentsTransitionDuringSplit(IBinder transition) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a recents request while " + "Split-Screen is foreground, so treat it as Mixed."); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java index b0547a2a47b1..29a58d7f75dc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java @@ -407,8 +407,6 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { mLeftoversHandler.mergeAnimation( transition, info, t, mergeTarget, finishCallback); } - } else { - mPipHandler.end(); } return; case TYPE_KEYGUARD: diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index 36c3e9711f5c..ac6e4c5cd69e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -306,10 +306,10 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } // Early check if the transition doesn't warrant an animation. - if (Transitions.isAllNoAnimation(info) || Transitions.isAllOrderOnly(info) + if (TransitionUtil.isAllNoAnimation(info) || TransitionUtil.isAllOrderOnly(info) || (info.getFlags() & WindowManager.TRANSIT_FLAG_INVISIBLE) != 0) { startTransaction.apply(); - finishTransaction.apply(); + // As a contract, finishTransaction should only be applied in Transitions#onFinish finishCallback.onTransitionFinished(null /* wct */); return true; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java index 30ffdac5cbba..357861cc3ca7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java @@ -161,13 +161,10 @@ public class MixedTransitionHelper { pipHandler.startEnterAnimation(pipChange, startTransaction, finishTransaction, finishCB); } - // make a new finishTransaction because pip's startEnterAnimation "consumes" it so - // we need a separate one to send over to launcher. - SurfaceControl.Transaction otherFinishT = new SurfaceControl.Transaction(); // Dispatch the rest of the transition normally. This will most-likely be taken by // recents or default handler. mixed.mLeftoversHandler = player.dispatchTransition(mixed.mTransition, everythingElse, - otherStartT, otherFinishT, finishCB, mixedHandler); + otherStartT, finishTransaction, finishCB, mixedHandler); } else { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Not leaving split, so just " + "forward animation to Pip-Handler."); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java index 8cdbe26a2c76..1847af07f275 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java @@ -159,6 +159,8 @@ class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition { // If pair-to-pair switching, the post-recents clean-up isn't needed. wct = wct != null ? wct : new WindowContainerTransaction(); if (mAnimType != ANIM_TYPE_PAIR_TO_PAIR) { + // TODO(b/346588978): Only called if !enableRecentsBookendTransition(), can remove + // once that rolls out mSplitHandler.onRecentsInSplitAnimationFinish(wct, finishTransaction); } else { // notify pair-to-pair recents animation finish @@ -177,6 +179,17 @@ class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition { return handled; } + /** + * Called when the recents animation during split is about to finish. + */ + void onAnimateRecentsDuringSplitFinishing(boolean returnToApp, + @NonNull WindowContainerTransaction finishWct, + @NonNull SurfaceControl.Transaction finishT) { + if (mAnimType != ANIM_TYPE_PAIR_TO_PAIR) { + mSplitHandler.onRecentsInSplitAnimationFinishing(returnToApp, finishWct, finishT); + } + } + @Override void mergeAnimation( @NonNull IBinder transition, @NonNull TransitionInfo info, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 8c9407b38d9e..b83b7e2f07a3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -672,46 +672,6 @@ public class Transitions implements RemoteCallable<Transitions>, return -1; } - /** - * Look through a transition and see if all non-closing changes are no-animation. If so, no - * animation should play. - */ - static boolean isAllNoAnimation(TransitionInfo info) { - if (isClosingType(info.getType())) { - // no-animation is only relevant for launching (open) activities. - return false; - } - boolean hasNoAnimation = false; - final int changeSize = info.getChanges().size(); - for (int i = changeSize - 1; i >= 0; --i) { - final TransitionInfo.Change change = info.getChanges().get(i); - if (isClosingType(change.getMode())) { - // ignore closing apps since they are a side-effect of the transition and don't - // animate. - continue; - } - if (change.hasFlags(FLAG_NO_ANIMATION)) { - hasNoAnimation = true; - } else if (!TransitionUtil.isOrderOnly(change) && !change.hasFlags(FLAG_IS_OCCLUDED)) { - // Ignore the order only or occluded changes since they shouldn't be visible during - // animation. For anything else, we need to animate if at-least one relevant - // participant *is* animated, - return false; - } - } - return hasNoAnimation; - } - - /** - * Check if all changes in this transition are only ordering changes. If so, we won't animate. - */ - static boolean isAllOrderOnly(TransitionInfo info) { - for (int i = info.getChanges().size() - 1; i >= 0; --i) { - if (!TransitionUtil.isOrderOnly(info.getChanges().get(i))) return false; - } - return true; - } - private Track getOrCreateTrack(int trackId) { while (trackId >= mTracks.size()) { mTracks.add(new Track()); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecorViewModel.java index 7948eadb28f4..2b2cdf84005c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecorViewModel.java @@ -16,13 +16,17 @@ package com.android.wm.shell.windowdecor; import android.app.ActivityManager.RunningTaskInfo; +import android.app.ActivityTaskManager; +import android.app.IActivityTaskManager; import android.content.Context; import android.hardware.input.InputManager; +import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; import android.util.Log; import android.util.SparseArray; import android.view.InputDevice; +import android.view.InsetsState; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.SurfaceControl; @@ -33,6 +37,7 @@ import android.window.WindowContainerTransaction; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; @@ -49,7 +54,8 @@ import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSuppl * Works with decorations that extend {@link CarWindowDecoration}. */ public abstract class CarWindowDecorViewModel - implements WindowDecorViewModel, FocusTransitionListener { + implements WindowDecorViewModel, FocusTransitionListener, + DisplayInsetsController.OnInsetsChangedListener { private static final String TAG = "CarWindowDecorViewModel"; private final ShellTaskOrganizer mTaskOrganizer; @@ -57,31 +63,37 @@ public abstract class CarWindowDecorViewModel private final @ShellBackgroundThread ShellExecutor mBgExecutor; private final ShellExecutor mMainExecutor; private final DisplayController mDisplayController; + private final DisplayInsetsController mDisplayInsetsController; private final FocusTransitionObserver mFocusTransitionObserver; private final SyncTransactionQueue mSyncQueue; private final SparseArray<CarWindowDecoration> mWindowDecorByTaskId = new SparseArray<>(); private final WindowDecorViewHostSupplier<WindowDecorViewHost> mWindowDecorViewHostSupplier; + private final IActivityTaskManager mActivityTaskManager; public CarWindowDecorViewModel( Context context, + @ShellMainThread ShellExecutor mainExecutor, @ShellBackgroundThread ShellExecutor bgExecutor, - @ShellMainThread ShellExecutor shellExecutor, ShellInit shellInit, ShellTaskOrganizer taskOrganizer, DisplayController displayController, + DisplayInsetsController displayInsetsController, SyncTransactionQueue syncQueue, FocusTransitionObserver focusTransitionObserver, WindowDecorViewHostSupplier<WindowDecorViewHost> windowDecorViewHostSupplier) { mContext = context; - mMainExecutor = shellExecutor; + mMainExecutor = mainExecutor; mBgExecutor = bgExecutor; mTaskOrganizer = taskOrganizer; mDisplayController = displayController; + mDisplayInsetsController = displayInsetsController; mFocusTransitionObserver = focusTransitionObserver; mSyncQueue = syncQueue; mWindowDecorViewHostSupplier = windowDecorViewHostSupplier; + mActivityTaskManager = ActivityTaskManager.getService(); shellInit.addInitCallback(this::onInit, this); + displayInsetsController.addGlobalInsetsChangedListener(this); } private void onInit() { @@ -187,6 +199,26 @@ public abstract class CarWindowDecorViewModel decoration.close(); } + @Override + public void insetsChanged(int displayId, InsetsState insetsState) { + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + try { + mActivityTaskManager.getTasks(/* maxNum= */ Integer.MAX_VALUE, + /* filterOnlyVisibleRecents= */ false, /* keepIntentExtra= */ false, + displayId) + .stream().filter(taskInfo -> taskInfo.isVisible && taskInfo.isRunning) + .forEach(taskInfo -> { + final CarWindowDecoration decoration = mWindowDecorByTaskId.get( + taskInfo.taskId); + if (decoration != null) { + decoration.relayout(taskInfo, t, t); + } + }); + } catch (RemoteException e) { + Log.e(TAG, "Cannot update decoration on inset change on displayId: " + displayId); + } + } + /** * @return {@code true} if the task/activity associated with {@code taskInfo} should show * window decoration. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecoration.java index 39437845301e..3182745d813e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecoration.java @@ -30,7 +30,6 @@ import android.view.WindowInsets; import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; @@ -47,6 +46,7 @@ public class CarWindowDecoration extends WindowDecoration<WindowDecorLinearLayou private WindowDecorLinearLayout mRootView; private @ShellBackgroundThread final ShellExecutor mBgExecutor; private final View.OnClickListener mClickListener; + private final RelayoutParams mRelayoutParams = new RelayoutParams(); private final RelayoutResult<WindowDecorLinearLayout> mResult = new RelayoutResult<>(); CarWindowDecoration( @@ -75,7 +75,8 @@ public class CarWindowDecoration extends WindowDecoration<WindowDecorLinearLayou @SuppressLint("MissingPermission") void relayout(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) { - relayout(taskInfo, startT, finishT, /* isCaptionVisible= */ true); + relayout(taskInfo, startT, finishT, + /* isCaptionVisible= */ mRelayoutParams.mIsCaptionVisible); } @SuppressLint("MissingPermission") @@ -84,12 +85,9 @@ public class CarWindowDecoration extends WindowDecoration<WindowDecorLinearLayou boolean isCaptionVisible) { final WindowContainerTransaction wct = new WindowContainerTransaction(); - RelayoutParams relayoutParams = new RelayoutParams(); + updateRelayoutParams(mRelayoutParams, taskInfo, isCaptionVisible); - updateRelayoutParams(relayoutParams, taskInfo, - mDisplayController.getInsetsState(taskInfo.displayId), isCaptionVisible); - - relayout(relayoutParams, startT, finishT, wct, mRootView, mResult); + relayout(mRelayoutParams, startT, finishT, wct, mRootView, mResult); // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo mBgExecutor.execute(() -> mTaskOrganizer.applyTransaction(wct)); @@ -118,7 +116,6 @@ public class CarWindowDecoration extends WindowDecoration<WindowDecorLinearLayou private void updateRelayoutParams( RelayoutParams relayoutParams, ActivityManager.RunningTaskInfo taskInfo, - @Nullable InsetsState displayInsetsState, boolean isCaptionVisible) { relayoutParams.reset(); relayoutParams.mRunningTaskInfo = taskInfo; @@ -127,16 +124,19 @@ public class CarWindowDecoration extends WindowDecoration<WindowDecorLinearLayou relayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_height; relayoutParams.mIsCaptionVisible = isCaptionVisible && mIsStatusBarVisible && !mIsKeyguardVisibleAndOccluded; - if (displayInsetsState != null) { - relayoutParams.mCaptionTopPadding = getTopPadding( - taskInfo.getConfiguration().windowConfiguration.getBounds(), - displayInsetsState); - } + relayoutParams.mCaptionTopPadding = getTopPadding(taskInfo, relayoutParams); + relayoutParams.mInsetSourceFlags |= FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR; relayoutParams.mApplyStartTransactionOnDraw = true; } - private static int getTopPadding(Rect taskBounds, @NonNull InsetsState insetsState) { + private int getTopPadding(ActivityManager.RunningTaskInfo taskInfo, + RelayoutParams relayoutParams) { + Rect taskBounds = taskInfo.getConfiguration().windowConfiguration.getBounds(); + InsetsState insetsState = mDisplayController.getInsetsState(taskInfo.displayId); + if (insetsState == null) { + return relayoutParams.mCaptionTopPadding; + } Insets systemDecor = insetsState.calculateInsets(taskBounds, WindowInsets.Type.systemBars() & ~WindowInsets.Type.captionBar(), false /* ignoreVisibility */); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index fad1c9f848ea..7e7a793298e2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -32,7 +32,6 @@ import static android.view.WindowInsets.Type.statusBars; import static com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU; import static com.android.window.flags.Flags.enableDisplayFocusInShellTransitions; -import static com.android.wm.shell.compatui.AppCompatUtils.isTopActivityExemptFromDesktopWindowing; import static com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.InputMethod; import static com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.MinimizeReason; import static com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger; @@ -133,6 +132,7 @@ import com.android.wm.shell.recents.RecentsTransitionStateListener; import com.android.wm.shell.shared.FocusTransitionListener; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition; @@ -254,6 +254,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, private final DesktopModeUiEventLogger mDesktopModeUiEventLogger; private final WindowDecorTaskResourceLoader mTaskResourceLoader; private final RecentsTransitionHandler mRecentsTransitionHandler; + private final DesktopModeCompatPolicy mDesktopModeCompatPolicy; public DesktopModeWindowDecorViewModel( Context context, @@ -290,7 +291,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, DesktopModeEventLogger desktopModeEventLogger, DesktopModeUiEventLogger desktopModeUiEventLogger, WindowDecorTaskResourceLoader taskResourceLoader, - RecentsTransitionHandler recentsTransitionHandler) { + RecentsTransitionHandler recentsTransitionHandler, + DesktopModeCompatPolicy desktopModeCompatPolicy) { this( context, shellExecutor, @@ -332,7 +334,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, desktopModeEventLogger, desktopModeUiEventLogger, taskResourceLoader, - recentsTransitionHandler); + recentsTransitionHandler, + desktopModeCompatPolicy); } @VisibleForTesting @@ -377,7 +380,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, DesktopModeEventLogger desktopModeEventLogger, DesktopModeUiEventLogger desktopModeUiEventLogger, WindowDecorTaskResourceLoader taskResourceLoader, - RecentsTransitionHandler recentsTransitionHandler) { + RecentsTransitionHandler recentsTransitionHandler, + DesktopModeCompatPolicy desktopModeCompatPolicy) { mContext = context; mMainExecutor = shellExecutor; mMainHandler = mainHandler; @@ -447,6 +451,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mDesktopModeUiEventLogger = desktopModeUiEventLogger; mTaskResourceLoader = taskResourceLoader; mRecentsTransitionHandler = recentsTransitionHandler; + mDesktopModeCompatPolicy = desktopModeCompatPolicy; shellInit.addInitCallback(this::onInit, this); } @@ -461,7 +466,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, new DesktopModeOnTaskResizeAnimationListener()); mDesktopTasksController.setOnTaskRepositionAnimationListener( new DesktopModeOnTaskRepositionAnimationListener()); - if (Flags.enableDesktopRecentsTransitionsCornersBugfix()) { + if (DesktopModeFlags.ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX.isTrue()) { mRecentsTransitionHandler.addTransitionStateListener( new DesktopModeRecentsTransitionStateListener()); } @@ -1652,8 +1657,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, && mSplitScreenController.isTaskRootOrStageRoot(taskInfo.taskId)) { return false; } - if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue() - && isTopActivityExemptFromDesktopWindowing(mContext, taskInfo)) { + if (mDesktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(taskInfo)) { return false; } if (isPartOfDefaultHomePackage(taskInfo)) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index b179741b1259..4e125d001076 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -542,6 +542,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin if (appHeader != null) { appHeader.setAppName(name); appHeader.setAppIcon(icon); + if (canEnterDesktopMode(mContext) && isEducationEnabled()) { + notifyCaptionStateChanged(); + } } }); } @@ -969,7 +972,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin final RelayoutParams.OccludingCaptionElement controlsElement = new RelayoutParams.OccludingCaptionElement(); controlsElement.mWidthResId = R.dimen.desktop_mode_customizable_caption_margin_end; - if (Flags.enableMinimizeButton()) { + if (DesktopModeFlags.ENABLE_MINIMIZE_BUTTON.isTrue()) { controlsElement.mWidthResId = R.dimen.desktop_mode_customizable_caption_with_minimize_button_margin_end; } @@ -1355,7 +1358,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mWebUri = assistContent == null ? null : AppToWebUtils.getSessionWebUri(assistContent); updateGenericLink(); final boolean supportsMultiInstance = mMultiInstanceHelper - .supportsMultiInstanceSplit(mTaskInfo.baseActivity) + .supportsMultiInstanceSplit(mTaskInfo.baseActivity, mTaskInfo.userId) && Flags.enableDesktopWindowingMultiInstanceFeatures(); final boolean shouldShowManageWindowsButton = supportsMultiInstance && mMinimumInstancesFound; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt index e5c989ed5f97..053850480ecc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt @@ -49,6 +49,7 @@ import com.android.window.flags.Flags import com.android.wm.shell.R import com.android.wm.shell.shared.annotations.ShellBackgroundThread import com.android.wm.shell.shared.annotations.ShellMainThread +import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper import com.android.wm.shell.shared.split.SplitScreenConstants import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer @@ -645,7 +646,7 @@ class HandleMenu( private fun bindWindowingPill(style: MenuStyle) { windowingPill.background.setTint(style.backgroundColor) - if (!com.android.wm.shell.Flags.enableBubbleAnything()) { + if (!BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { floatingBtn.visibility = View.GONE } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt index 70c0b54462e3..22bc9782170b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt @@ -5,6 +5,7 @@ import android.app.ActivityManager.RunningTaskInfo import android.content.Context import android.graphics.PointF import android.graphics.Rect +import android.view.Choreographer import android.view.MotionEvent import android.view.SurfaceControl import android.view.VelocityTracker @@ -48,7 +49,7 @@ class MoveToDesktopAnimator @JvmOverloads constructor( t.setScale(taskSurface, scale, scale) .setCornerRadius(taskSurface, cornerRadius) .setScale(taskSurface, scale, scale) - .setCornerRadius(taskSurface, cornerRadius) + .setFrameTimeline(Choreographer.getInstance().vsyncId) .setPosition(taskSurface, position.x, position.y) .apply() } @@ -96,6 +97,7 @@ class MoveToDesktopAnimator @JvmOverloads constructor( setTaskPosition(ev.rawX, ev.rawY) val t = transactionFactory() t.setPosition(taskSurface, position.x, position.y) + t.setFrameTimeline(Choreographer.getInstance().vsyncId) t.apply() } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt index d87da092cccf..1bc48f89ea6d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.windowdecor.common import android.annotation.DimenRes -import android.app.ActivityManager import android.app.ActivityManager.RunningTaskInfo import android.content.Context import android.content.pm.ActivityInfo @@ -29,6 +28,7 @@ import com.android.launcher3.icons.BaseIconFactory import com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT import com.android.launcher3.icons.IconProvider import com.android.wm.shell.R +import com.android.wm.shell.common.UserProfileContexts import com.android.wm.shell.shared.annotations.ShellBackgroundThread import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellController @@ -41,10 +41,10 @@ import java.util.concurrent.ConcurrentHashMap * A utility and cache for window decoration UI resources. */ class WindowDecorTaskResourceLoader( - private val context: Context, shellInit: ShellInit, private val shellController: ShellController, private val shellCommandHandler: ShellCommandHandler, + private val userProfilesContexts: UserProfileContexts, private val iconProvider: IconProvider, private val headerIconFactory: BaseIconFactory, private val veilIconFactory: BaseIconFactory, @@ -54,11 +54,12 @@ class WindowDecorTaskResourceLoader( shellInit: ShellInit, shellController: ShellController, shellCommandHandler: ShellCommandHandler, + userProfileContexts: UserProfileContexts, ) : this( - context, shellInit, shellController, shellCommandHandler, + userProfileContexts, IconProvider(context), headerIconFactory = context.createIconFactory(R.dimen.desktop_mode_caption_icon_radius), veilIconFactory = context.createIconFactory(R.dimen.desktop_mode_resize_veil_icon_size), @@ -79,9 +80,6 @@ class WindowDecorTaskResourceLoader( */ private val existingTasks = mutableSetOf<Int>() - @VisibleForTesting - lateinit var currentUserContext: Context - init { shellInit.addInitCallback(this::onInit, this) } @@ -90,14 +88,10 @@ class WindowDecorTaskResourceLoader( shellCommandHandler.addDumpCallback(this::dump, this) shellController.addUserChangeListener(object : UserChangeListener { override fun onUserChanged(newUserId: Int, userContext: Context) { - currentUserContext = userContext // No need to hold on to resources for tasks of another profile. taskToResourceCache.clear() } }) - currentUserContext = context.createContextAsUser( - UserHandle.of(ActivityManager.getCurrentUser()), /* flags= */ 0 - ) } /** Returns the user readable name for this task. */ @@ -158,15 +152,20 @@ class WindowDecorTaskResourceLoader( private fun loadAppResources(taskInfo: RunningTaskInfo): AppResources { Trace.beginSection("$TAG#loadAppResources") - val pm = currentUserContext.packageManager - val activityInfo = getActivityInfo(taskInfo, pm) - val appName = pm.getApplicationLabel(activityInfo.applicationInfo) - val appIconDrawable = iconProvider.getIcon(activityInfo) - val badgedAppIconDrawable = pm.getUserBadgedIcon(appIconDrawable, taskInfo.userHandle()) - val appIcon = headerIconFactory.createIconBitmap(badgedAppIconDrawable, /* scale= */ 1f) - val veilIcon = veilIconFactory.createScaledBitmap(appIconDrawable, MODE_DEFAULT) - Trace.endSection() - return AppResources(appName = appName, appIcon = appIcon, veilIcon = veilIcon) + try { + val pm = checkNotNull(userProfilesContexts[taskInfo.userId]?.packageManager) { + "Could not get context for user ${taskInfo.userId}" + } + val activityInfo = getActivityInfo(taskInfo, pm) + val appName = pm.getApplicationLabel(activityInfo.applicationInfo) + val appIconDrawable = iconProvider.getIcon(activityInfo) + val badgedAppIconDrawable = pm.getUserBadgedIcon(appIconDrawable, taskInfo.userHandle()) + val appIcon = headerIconFactory.createIconBitmap(badgedAppIconDrawable, /* scale= */ 1f) + val veilIcon = veilIconFactory.createScaledBitmap(appIconDrawable, MODE_DEFAULT) + return AppResources(appName = appName, appIcon = appIcon, veilIcon = veilIcon) + } finally { + Trace.endSection() + } } private fun getActivityInfo(taskInfo: RunningTaskInfo, pm: PackageManager): ActivityInfo { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt index d72da3a08de5..8747f63e789f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt @@ -29,6 +29,7 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.common.DisplayChangeController import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.desktopmode.DesktopModeEventLogger import com.android.wm.shell.desktopmode.DesktopTasksController @@ -37,6 +38,7 @@ import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler import com.android.wm.shell.shared.annotations.ShellBackgroundThread import com.android.wm.shell.shared.annotations.ShellMainThread +import com.android.wm.shell.transition.FocusTransitionObserver import com.android.wm.shell.transition.Transitions import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader @@ -58,6 +60,8 @@ class DesktopTilingDecorViewModel( private val desktopUserRepositories: DesktopUserRepositories, private val desktopModeEventLogger: DesktopModeEventLogger, private val taskResourceLoader: WindowDecorTaskResourceLoader, + private val focusTransitionObserver: FocusTransitionObserver, + private val mainExecutor: ShellExecutor, ) : DisplayChangeController.OnDisplayChangingListener { @VisibleForTesting var tilingTransitionHandlerByDisplayId = SparseArray<DesktopTilingWindowDecoration>() @@ -94,6 +98,8 @@ class DesktopTilingDecorViewModel( returnToDragStartAnimator, desktopUserRepositories, desktopModeEventLogger, + focusTransitionObserver, + mainExecutor, ) tilingTransitionHandlerByDisplayId.put(displayId, newHandler) newHandler @@ -112,9 +118,10 @@ class DesktopTilingDecorViewModel( } fun moveTaskToFrontIfTiled(taskInfo: RunningTaskInfo): Boolean { + // Always pass focus=true because taskInfo.isFocused is not updated yet. return tilingTransitionHandlerByDisplayId .get(taskInfo.displayId) - ?.moveTiledPairToFront(taskInfo, isTaskFocused = true) ?: false + ?.moveTiledPairToFront(taskInfo.taskId, isFocusedOnDisplay = true) ?: false } fun onOverviewAnimationStateChange(isRunning: Boolean) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt index 6f2323347468..666d4bd046dc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt @@ -35,11 +35,14 @@ import android.window.TransitionRequestInfo import android.window.WindowContainerTransaction import com.android.internal.annotations.VisibleForTesting import com.android.launcher3.icons.BaseIconFactory +import com.android.window.flags.Flags +import com.android.wm.shell.shared.FocusTransitionListener import com.android.wm.shell.R import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout +import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.desktopmode.DesktopModeEventLogger import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger @@ -49,6 +52,7 @@ import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler import com.android.wm.shell.shared.annotations.ShellBackgroundThread import com.android.wm.shell.shared.annotations.ShellMainThread +import com.android.wm.shell.transition.FocusTransitionObserver import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TRANSIT_MINIMIZE import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration @@ -78,13 +82,16 @@ class DesktopTilingWindowDecoration( private val returnToDragStartAnimator: ReturnToDragStartAnimator, private val desktopUserRepositories: DesktopUserRepositories, private val desktopModeEventLogger: DesktopModeEventLogger, + private val focusTransitionObserver: FocusTransitionObserver, + @ShellMainThread private val mainExecutor: ShellExecutor, private val transactionSupplier: Supplier<Transaction> = Supplier { Transaction() }, ) : Transitions.TransitionHandler, ShellTaskOrganizer.FocusListener, ShellTaskOrganizer.TaskVanishedListener, DragEventListener, - Transitions.TransitionObserver { + Transitions.TransitionObserver, + FocusTransitionListener { companion object { private val TAG: String = DesktopTilingWindowDecoration::class.java.simpleName private const val TILING_DIVIDER_TAG = "Tiling Divider" @@ -176,8 +183,13 @@ class DesktopTilingWindowDecoration( if (!isTilingManagerInitialised) { desktopTilingDividerWindowManager = initTilingManagerForDisplay(displayId, config) isTilingManagerInitialised = true - shellTaskOrganizer.addFocusListener(this) - isTilingFocused = true + + if (Flags.enableDisplayFocusInShellTransitions()) { + focusTransitionObserver.setLocalFocusTransitionListener(this, mainExecutor) + } else { + shellTaskOrganizer.addFocusListener(this) + isTilingFocused = true + } } leftTaskResizingHelper?.initIfNeeded() rightTaskResizingHelper?.initIfNeeded() @@ -474,23 +486,33 @@ class DesktopTilingWindowDecoration( } } - // Only called if [taskInfo] relates to a focused task - private fun isTilingFocusRemoved(taskInfo: RunningTaskInfo): Boolean { + // Only called if [taskId] relates to a focused task + private fun isTilingFocusRemoved(taskId: Int): Boolean { return isTilingFocused && - taskInfo.taskId != leftTaskResizingHelper?.taskInfo?.taskId && - taskInfo.taskId != rightTaskResizingHelper?.taskInfo?.taskId + taskId != leftTaskResizingHelper?.taskInfo?.taskId && + taskId != rightTaskResizingHelper?.taskInfo?.taskId } + // Overriding ShellTaskOrganizer.FocusListener override fun onFocusTaskChanged(taskInfo: RunningTaskInfo?) { + if (Flags.enableDisplayFocusInShellTransitions()) return if (taskInfo != null) { - moveTiledPairToFront(taskInfo) + moveTiledPairToFront(taskInfo.taskId, taskInfo.isFocused) } } + // Overriding FocusTransitionListener + override fun onFocusedTaskChanged(taskId: Int, + isFocusedOnDisplay: Boolean, + isFocusedGlobally: Boolean) { + if (!Flags.enableDisplayFocusInShellTransitions()) return + moveTiledPairToFront(taskId, isFocusedOnDisplay) + } + // Only called if [taskInfo] relates to a focused task - private fun isTilingRefocused(taskInfo: RunningTaskInfo): Boolean { - return taskInfo.taskId == leftTaskResizingHelper?.taskInfo?.taskId || - taskInfo.taskId == rightTaskResizingHelper?.taskInfo?.taskId + private fun isTilingRefocused(taskId: Int): Boolean { + return taskId == leftTaskResizingHelper?.taskInfo?.taskId || + taskId == rightTaskResizingHelper?.taskInfo?.taskId } private fun buildTiledTasksMoveToFront(leftOnTop: Boolean): WindowContainerTransaction { @@ -582,14 +604,13 @@ class DesktopTilingWindowDecoration( * If specified, [isTaskFocused] will override [RunningTaskInfo.isFocused]. This is to be used * when called when the task will be focused, but the [taskInfo] hasn't been updated yet. */ - fun moveTiledPairToFront(taskInfo: RunningTaskInfo, isTaskFocused: Boolean? = null): Boolean { + fun moveTiledPairToFront(taskId: Int, isFocusedOnDisplay: Boolean): Boolean { if (!isTilingManagerInitialised) return false - val isFocused = isTaskFocused ?: taskInfo.isFocused - if (!isFocused) return false + if (!isFocusedOnDisplay) return false // If a task that isn't tiled is being focused, let the generic handler do the work. - if (isTilingFocusRemoved(taskInfo)) { + if (!Flags.enableDisplayFocusInShellTransitions() && isTilingFocusRemoved(taskId)) { isTilingFocused = false return false } @@ -597,31 +618,29 @@ class DesktopTilingWindowDecoration( val leftTiledTask = leftTaskResizingHelper ?: return false val rightTiledTask = rightTaskResizingHelper ?: return false if (!allTiledTasksVisible()) return false - val isLeftOnTop = taskInfo.taskId == leftTiledTask.taskInfo.taskId - if (isTilingRefocused(taskInfo)) { - val t = transactionSupplier.get() - isTilingFocused = true - if (taskInfo.taskId == leftTaskResizingHelper?.taskInfo?.taskId) { - desktopTilingDividerWindowManager?.onRelativeLeashChanged( - leftTiledTask.getLeash(), - t, - ) - } - if (taskInfo.taskId == rightTaskResizingHelper?.taskInfo?.taskId) { - desktopTilingDividerWindowManager?.onRelativeLeashChanged( - rightTiledTask.getLeash(), - t, - ) - } - transitions.startTransition( - TRANSIT_TO_FRONT, - buildTiledTasksMoveToFront(isLeftOnTop), - null, - ) - t.apply() - return true + val isLeftOnTop = taskId == leftTiledTask.taskInfo.taskId + if (!isTilingRefocused(taskId)) return false + val t = transactionSupplier.get() + if (!Flags.enableDisplayFocusInShellTransitions()) isTilingFocused = true + if (taskId == leftTaskResizingHelper?.taskInfo?.taskId) { + desktopTilingDividerWindowManager?.onRelativeLeashChanged( + leftTiledTask.getLeash(), + t, + ) } - return false + if (taskId == rightTaskResizingHelper?.taskInfo?.taskId) { + desktopTilingDividerWindowManager?.onRelativeLeashChanged( + rightTiledTask.getLeash(), + t, + ) + } + transitions.startTransition( + TRANSIT_TO_FRONT, + buildTiledTasksMoveToFront(isLeftOnTop), + null, + ) + t.apply() + return true } private fun allTiledTasksVisible(): Boolean { @@ -706,7 +725,13 @@ class DesktopTilingWindowDecoration( } private fun tearDownTiling() { - if (isTilingManagerInitialised) shellTaskOrganizer.removeFocusListener(this) + if (isTilingManagerInitialised) { + if (Flags.enableDisplayFocusInShellTransitions()) { + focusTransitionObserver.unsetLocalFocusTransitionListener(this) + } else { + shellTaskOrganizer.removeFocusListener(this) + } + } if (leftTaskResizingHelper == null && rightTaskResizingHelper == null) { shellTaskOrganizer.removeTaskVanishedListener(this) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt index dc4fa3788778..9f8ca7740182 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt @@ -47,7 +47,6 @@ import com.android.internal.R.color.materialColorSurfaceContainerHigh import com.android.internal.R.color.materialColorSurfaceContainerLow import com.android.internal.R.color.materialColorSurfaceDim import com.android.window.flags.Flags -import com.android.window.flags.Flags.enableMinimizeButton import com.android.wm.shell.R import android.window.DesktopModeFlags import com.android.wm.shell.windowdecor.MaximizeButtonView @@ -226,7 +225,7 @@ class AppHeaderViewHolder( minimizeWindowButton.background = getDrawable(1) } maximizeButtonView.setAnimationTints(isDarkMode()) - minimizeWindowButton.isGone = !enableMinimizeButton() + minimizeWindowButton.isGone = !DesktopModeFlags.ENABLE_MINIMIZE_BUTTON.isTrue() } private fun bindDataWithThemedHeaders( @@ -276,7 +275,7 @@ class AppHeaderViewHolder( drawableInsets = minimizeDrawableInsets ) } - minimizeWindowButton.isGone = !enableMinimizeButton() + minimizeWindowButton.isGone = !DesktopModeFlags.ENABLE_MINIMIZE_BUTTON.isTrue() // Maximize button. maximizeButtonView.apply { setAnimationTints( @@ -329,11 +328,6 @@ class AppHeaderViewHolder( } fun runOnAppChipGlobalLayout(runnable: () -> Unit) { - if (openMenuButton.isAttachedToWindow) { - // App chip is already inflated. - runnable() - return - } // Wait for app chip to be inflated before notifying repository. openMenuButton.viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener { diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppFromAllAppsLandscape.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppFromAllAppsLandscape.kt new file mode 100644 index 000000000000..6b159a4152ac --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppFromAllAppsLandscape.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker + +import android.tools.Rotation.ROTATION_90 +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.CASCADE_APP +import com.android.wm.shell.scenarios.OpenAppFromAllApps +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class OpenAppFromAllAppsLandscape : OpenAppFromAllApps(rotation = ROTATION_90) { + + @ExpectedScenarios(["CASCADE_APP"]) + @Test + override fun openApp() = super.openApp() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(CASCADE_APP) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppFromAllAppsPortrait.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppFromAllAppsPortrait.kt new file mode 100644 index 000000000000..07b439284680 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppFromAllAppsPortrait.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker + +import android.tools.Rotation.ROTATION_0 +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.CASCADE_APP +import com.android.wm.shell.scenarios.OpenAppFromAllApps +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class OpenAppFromAllAppsPortrait : OpenAppFromAllApps(rotation = ROTATION_0) { + + @ExpectedScenarios(["CASCADE_APP"]) + @Test + override fun openApp() = super.openApp() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(CASCADE_APP) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppFromTaskbarLandscape.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppFromTaskbarLandscape.kt new file mode 100644 index 000000000000..caadd3be0b9c --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppFromTaskbarLandscape.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker + +import android.tools.Rotation.ROTATION_90 +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.CASCADE_APP +import com.android.wm.shell.scenarios.OpenAppFromTaskbar +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class OpenAppFromTaskbarLandscape : OpenAppFromTaskbar(rotation = ROTATION_90) { + + @ExpectedScenarios(["CASCADE_APP"]) + @Test + override fun openApp() = super.openApp() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(CASCADE_APP) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppFromTaskbarPortrait.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppFromTaskbarPortrait.kt new file mode 100644 index 000000000000..77f5ab290e20 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppFromTaskbarPortrait.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker + +import android.tools.Rotation.ROTATION_0 +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.CASCADE_APP +import com.android.wm.shell.scenarios.OpenAppFromTaskbar +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class OpenAppFromTaskbarPortrait : OpenAppFromTaskbar(rotation = ROTATION_0) { + + @ExpectedScenarios(["CASCADE_APP"]) + @Test + override fun openApp() = super.openApp() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(CASCADE_APP) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt index 966aea3088c4..7855698d0151 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt @@ -21,6 +21,7 @@ import android.tools.NavBar import android.tools.Rotation import android.tools.flicker.rules.ChangeDisplayOrientationRule import android.tools.traces.parsers.WindowManagerStateHelper +import android.window.DesktopModeFlags import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation @@ -59,7 +60,7 @@ constructor( fun setup() { Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) if (usingKeyboard) { - Assume.assumeTrue(Flags.enableTaskResizingKeyboardShortcuts()) + Assume.assumeTrue(DesktopModeFlags.ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS.isTrue) } tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeAppWindows.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeAppWindows.kt index 46c97b0a1397..2f99fbaba078 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeAppWindows.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeAppWindows.kt @@ -21,6 +21,7 @@ import android.tools.NavBar import android.tools.Rotation import android.tools.flicker.rules.ChangeDisplayOrientationRule import android.tools.traces.parsers.WindowManagerStateHelper +import android.window.DesktopModeFlags import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation @@ -62,7 +63,7 @@ constructor( Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) Assume.assumeTrue(Flags.enableMinimizeButton()) if (usingKeyboard) { - Assume.assumeTrue(Flags.enableTaskResizingKeyboardShortcuts()) + Assume.assumeTrue(DesktopModeFlags.ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS.isTrue) } tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppFromAllApps.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppFromAllApps.kt index 36cdd5b26992..348219631245 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppFromAllApps.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppFromAllApps.kt @@ -20,12 +20,12 @@ import android.app.Instrumentation import android.tools.NavBar import android.tools.flicker.rules.ChangeDisplayOrientationRule import android.tools.Rotation +import android.tools.device.apphelpers.CalculatorAppHelper import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation import com.android.server.wm.flicker.helpers.DesktopModeAppHelper -import com.android.server.wm.flicker.helpers.MailAppHelper import com.android.server.wm.flicker.helpers.SimpleAppHelper import com.android.window.flags.Flags import com.android.wm.shell.Utils @@ -44,7 +44,7 @@ abstract class OpenAppFromAllApps(val rotation: Rotation = Rotation.ROTATION_0) private val wmHelper = WindowManagerStateHelper(instrumentation) private val device = UiDevice.getInstance(instrumentation) private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) - private val mailApp = MailAppHelper(instrumentation) + private val calculatorApp = CalculatorAppHelper(instrumentation) @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) @@ -64,13 +64,13 @@ abstract class OpenAppFromAllApps(val rotation: Rotation = Rotation.ROTATION_0) open fun openApp() { tapl.launchedAppState.taskbar .openAllApps() - .getAppIcon(mailApp.appName) - .launch(mailApp.packageName) + .getAppIcon(calculatorApp.appName) + .launch(calculatorApp.packageName) } @After fun teardown() { - mailApp.exit(wmHelper) + calculatorApp.exit(wmHelper) testApp.exit(wmHelper) } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithKeyboardShortcuts.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithKeyboardShortcuts.kt index 068064402ee5..59d15ca4fa6b 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithKeyboardShortcuts.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithKeyboardShortcuts.kt @@ -20,6 +20,7 @@ import android.app.Instrumentation import android.tools.NavBar import android.tools.Rotation import android.tools.traces.parsers.WindowManagerStateHelper +import android.window.DesktopModeFlags import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation @@ -59,7 +60,7 @@ abstract class SnapResizeAppWindowWithKeyboardShortcuts( @Before fun setup() { Assume.assumeTrue(Flags.enableDesktopWindowingMode() && - Flags.enableTaskResizingKeyboardShortcuts() && tapl.isTablet) + DesktopModeFlags.ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS.isTrue && tapl.isTablet) testApp.enterDesktopMode(wmHelper, device) } diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/CopyContentInSplit.kt index 31d89f92f744..9d501d32fbc7 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/CopyContentInSplit.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/CopyContentInSplit.kt @@ -23,8 +23,8 @@ import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.RecentTasksUtils import com.android.wm.shell.Utils -import com.android.wm.shell.flicker.utils.RecentTasksUtils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByDivider.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByDivider.kt index 1af6cac39085..f574f02ac3b3 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByDivider.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByDivider.kt @@ -23,8 +23,8 @@ import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.RecentTasksUtils import com.android.wm.shell.Utils -import com.android.wm.shell.flicker.utils.RecentTasksUtils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByGoHome.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByGoHome.kt index 8ad8c7bd7a7f..60fcce2fbf18 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByGoHome.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByGoHome.kt @@ -23,8 +23,8 @@ import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.RecentTasksUtils import com.android.wm.shell.Utils -import com.android.wm.shell.flicker.utils.RecentTasksUtils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DragDividerToResize.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DragDividerToResize.kt index da0ace472153..e6a080b2d258 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DragDividerToResize.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DragDividerToResize.kt @@ -23,8 +23,8 @@ import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.RecentTasksUtils import com.android.wm.shell.Utils -import com.android.wm.shell.flicker.utils.RecentTasksUtils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenAutoEnterPipOnGoToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenAutoEnterPipOnGoToHomeTest.kt index f9c60ad14fae..aa893ed65e7c 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenAutoEnterPipOnGoToHomeTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenAutoEnterPipOnGoToHomeTest.kt @@ -19,7 +19,6 @@ package com.android.wm.shell.flicker.pip import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.platform.test.annotations.RequiresDevice -import android.platform.test.annotations.RequiresFlagsDisabled import android.tools.Rotation import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder @@ -29,7 +28,6 @@ import android.tools.helpers.WindowUtils import android.tools.traces.parsers.toFlickerComponent import com.android.server.wm.flicker.helpers.SimpleAppHelper import com.android.server.wm.flicker.testapp.ActivityOptions -import com.android.wm.shell.Flags import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.Assume import org.junit.FixMethodOrder @@ -64,7 +62,6 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -@RequiresFlagsDisabled(Flags.FLAG_ENABLE_PIP2) open class FromSplitScreenAutoEnterPipOnGoToHomeTest(flicker: LegacyFlickerTest) : AutoEnterPipOnGoToHomeTest(flicker) { private val portraitDisplayBounds = WindowUtils.getDisplayBounds(Rotation.ROTATION_0) diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt index 805f4c2fe7f8..8e7cb56c0f07 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt @@ -19,7 +19,6 @@ package com.android.wm.shell.flicker.pip import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.platform.test.annotations.RequiresDevice -import android.platform.test.annotations.RequiresFlagsDisabled import android.tools.Rotation import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder @@ -30,7 +29,6 @@ import android.tools.traces.parsers.toFlickerComponent import com.android.server.wm.flicker.helpers.PipAppHelper import com.android.server.wm.flicker.helpers.SimpleAppHelper import com.android.server.wm.flicker.testapp.ActivityOptions -import com.android.wm.shell.Flags import com.android.wm.shell.flicker.pip.common.EnterPipTransition import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.Assume @@ -66,7 +64,6 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -@RequiresFlagsDisabled(Flags.FLAG_ENABLE_PIP2) @FlakyTest(bugId = 386333280) open class FromSplitScreenEnterPipOnUserLeaveHintTest(flicker: LegacyFlickerTest) : EnterPipTransition(flicker) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java new file mode 100644 index 000000000000..9d0ddbc6de12 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.bubbles; + +import static android.view.WindowManager.TRANSIT_CHANGE; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager; +import android.os.IBinder; +import android.view.SurfaceControl; +import android.view.ViewRootImpl; +import android.window.IWindowContainerToken; +import android.window.TransitionInfo; +import android.window.WindowContainerToken; + +import androidx.test.filters.SmallTest; + +import com.android.launcher3.icons.BubbleIconFactory; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestSyncExecutor; +import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView; +import com.android.wm.shell.bubbles.bar.BubbleBarLayerView; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.taskview.TaskView; +import com.android.wm.shell.taskview.TaskViewRepository; +import com.android.wm.shell.taskview.TaskViewTaskController; +import com.android.wm.shell.taskview.TaskViewTransitions; +import com.android.wm.shell.transition.Transitions; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests of {@link BubbleTransitions}. + */ +@SmallTest +public class BubbleTransitionsTest extends ShellTestCase { + @Mock + private BubbleData mBubbleData; + @Mock + private Bubble mBubble; + @Mock + private Transitions mTransitions; + @Mock + private SyncTransactionQueue mSyncQueue; + @Mock + private BubbleExpandedViewManager mExpandedViewManager; + @Mock + private BubblePositioner mBubblePositioner; + @Mock + private BubbleLogger mBubbleLogger; + @Mock + private BubbleStackView mStackView; + @Mock + private BubbleBarLayerView mLayerView; + @Mock + private BubbleIconFactory mIconFactory; + + @Mock private ShellTaskOrganizer mTaskOrganizer; + private TaskViewTransitions mTaskViewTransitions; + private TaskViewRepository mRepository; + private BubbleTransitions mBubbleTransitions; + private BubbleTaskViewFactory mTaskViewFactory; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mRepository = new TaskViewRepository(); + ShellExecutor syncExecutor = new TestSyncExecutor(); + + when(mTransitions.getMainExecutor()).thenReturn(syncExecutor); + when(mTransitions.isRegistered()).thenReturn(true); + mTaskViewTransitions = new TaskViewTransitions(mTransitions, mRepository, mTaskOrganizer, + mSyncQueue); + mBubbleTransitions = new BubbleTransitions(mTransitions, mTaskOrganizer, mRepository, + mBubbleData, mTaskViewTransitions, mContext); + mTaskViewFactory = () -> { + TaskViewTaskController taskViewTaskController = new TaskViewTaskController( + mContext, mTaskOrganizer, mTaskViewTransitions, mSyncQueue); + TaskView taskView = new TaskView(mContext, mTaskViewTransitions, + taskViewTaskController); + return new BubbleTaskView(taskView, syncExecutor); + }; + final BubbleBarExpandedView bbev = mock(BubbleBarExpandedView.class); + final ViewRootImpl vri = mock(ViewRootImpl.class); + when(bbev.getViewRootImpl()).thenReturn(vri); + when(mBubble.getBubbleBarExpandedView()).thenReturn(bbev); + } + + private ActivityManager.RunningTaskInfo setupBubble() { + ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); + final IWindowContainerToken itoken = mock(IWindowContainerToken.class); + final IBinder asBinder = mock(IBinder.class); + when(itoken.asBinder()).thenReturn(asBinder); + WindowContainerToken token = new WindowContainerToken(itoken); + taskInfo.token = token; + final TaskView tv = mock(TaskView.class); + final TaskViewTaskController tvtc = mock(TaskViewTaskController.class); + when(tvtc.getTaskInfo()).thenReturn(taskInfo); + when(tv.getController()).thenReturn(tvtc); + when(mBubble.getTaskView()).thenReturn(tv); + mRepository.add(tvtc); + return taskInfo; + } + + @Test + public void testConvertToBubble() { + // Basic walk-through of convert-to-bubble transition stages + ActivityManager.RunningTaskInfo taskInfo = setupBubble(); + final BubbleTransitions.BubbleTransition bt = mBubbleTransitions.startConvertToBubble( + mBubble, taskInfo, mExpandedViewManager, mTaskViewFactory, mBubblePositioner, + mBubbleLogger, mStackView, mLayerView, mIconFactory, false); + final BubbleTransitions.ConvertToBubble ctb = (BubbleTransitions.ConvertToBubble) bt; + ctb.onInflated(mBubble); + when(mLayerView.canExpandView(any())).thenReturn(true); + verify(mTransitions).startTransition(anyInt(), any(), eq(ctb)); + verify(mBubble).setPreparingTransition(eq(bt)); + // Ensure we are communicating with the taskviewtransitions queue + assertTrue(mTaskViewTransitions.hasPending()); + + final TransitionInfo info = new TransitionInfo(TRANSIT_CHANGE, 0); + final TransitionInfo.Change chg = new TransitionInfo.Change(taskInfo.token, + mock(SurfaceControl.class)); + chg.setTaskInfo(taskInfo); + chg.setMode(TRANSIT_CHANGE); + info.addChange(chg); + info.addRoot(new TransitionInfo.Root(0, mock(SurfaceControl.class), 0, 0)); + SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); + SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + final boolean[] finishCalled = new boolean[]{false}; + Transitions.TransitionFinishCallback finishCb = wct -> { + assertFalse(finishCalled[0]); + finishCalled[0] = true; + }; + ctb.startAnimation(ctb.mTransition, info, startT, finishT, finishCb); + assertFalse(mTaskViewTransitions.hasPending()); + + verify(mBubbleData).notificationEntryUpdated(eq(mBubble), anyBoolean(), anyBoolean()); + ctb.continueExpand(); + + clearInvocations(mBubble); + verify(mBubble, never()).setPreparingTransition(any()); + + ctb.surfaceCreated(); + verify(mBubble).setPreparingTransition(isNull()); + ArgumentCaptor<Runnable> animCb = ArgumentCaptor.forClass(Runnable.class); + verify(mLayerView).animateConvert(any(), any(), any(), any(), animCb.capture()); + assertFalse(finishCalled[0]); + animCb.getValue().run(); + assertTrue(finishCalled[0]); + } + + @Test + public void testConvertFromBubble() { + ActivityManager.RunningTaskInfo taskInfo = setupBubble(); + final BubbleTransitions.BubbleTransition bt = mBubbleTransitions.startConvertFromBubble( + mBubble, taskInfo); + final BubbleTransitions.ConvertFromBubble cfb = (BubbleTransitions.ConvertFromBubble) bt; + verify(mTransitions).startTransition(anyInt(), any(), eq(cfb)); + verify(mBubble).setPreparingTransition(eq(bt)); + assertTrue(mTaskViewTransitions.hasPending()); + + final TransitionInfo info = new TransitionInfo(TRANSIT_CHANGE, 0); + final TransitionInfo.Change chg = new TransitionInfo.Change(taskInfo.token, + mock(SurfaceControl.class)); + chg.setMode(TRANSIT_CHANGE); + chg.setTaskInfo(taskInfo); + info.addChange(chg); + info.addRoot(new TransitionInfo.Root(0, mock(SurfaceControl.class), 0, 0)); + SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); + SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + Transitions.TransitionFinishCallback finishCb = wct -> {}; + cfb.startAnimation(cfb.mTransition, info, startT, finishT, finishCb); + + // Can really only verify that it interfaces with the taskViewTransitions queue. + // The actual functioning of this is tightly-coupled with SurfaceFlinger and renderthread + // in order to properly synchronize surface manipulation with drawing and thus can't be + // directly tested. + assertFalse(mTaskViewTransitions.hasPending()); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt index bec91e910cf7..6b0c390ac239 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt @@ -33,8 +33,6 @@ import org.junit.runner.RunWith import org.mockito.ArgumentMatchers import org.mockito.ArgumentMatchers.eq import org.mockito.kotlin.any -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.doThrow import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify @@ -80,17 +78,19 @@ class MultiInstanceHelperTest : ShellTestCase() { @Test fun supportsMultiInstanceSplit_inStaticAllowList() { val allowList = arrayOf(TEST_PACKAGE) - val helper = MultiInstanceHelper(mContext, context.packageManager, allowList, true) + val helper = MultiInstanceHelper(mContext, context.packageManager, allowList, + mock(), mock(), true) val component = ComponentName(TEST_PACKAGE, TEST_ACTIVITY) - assertEquals(true, helper.supportsMultiInstanceSplit(component)) + assertEquals(true, helper.supportsMultiInstanceSplit(component, TEST_OTHER_USER_ID)) } @Test fun supportsMultiInstanceSplit_notInStaticAllowList() { val allowList = arrayOf(TEST_PACKAGE) - val helper = MultiInstanceHelper(mContext, context.packageManager, allowList, true) + val helper = MultiInstanceHelper(mContext, context.packageManager, allowList, + mock(), mock(), true) val component = ComponentName(TEST_NOT_ALLOWED_PACKAGE, TEST_ACTIVITY) - assertEquals(false, helper.supportsMultiInstanceSplit(component)) + assertEquals(false, helper.supportsMultiInstanceSplit(component, TEST_OTHER_USER_ID)) } @Test @@ -99,17 +99,17 @@ class MultiInstanceHelperTest : ShellTestCase() { val component = ComponentName(TEST_PACKAGE, TEST_ACTIVITY) val pm = mock<PackageManager>() val activityProp = PackageManager.Property("", true, "", "") - whenever(pm.getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), - eq(component))) + whenever(pm.getPropertyAsUser(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), + eq(component.packageName), eq(component.className), eq(TEST_OTHER_USER_ID))) .thenReturn(activityProp) val appProp = PackageManager.Property("", false, "", "") - whenever(pm.getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), - eq(component.packageName))) + whenever(pm.getPropertyAsUser(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), + eq(component.packageName), eq(null), eq(TEST_OTHER_USER_ID))) .thenReturn(appProp) - val helper = MultiInstanceHelper(mContext, pm, emptyArray(), true) + val helper = MultiInstanceHelper(mContext, pm, emptyArray(), mock(), mock(), true) // Expect activity property to override application property - assertEquals(true, helper.supportsMultiInstanceSplit(component)) + assertEquals(true, helper.supportsMultiInstanceSplit(component, TEST_OTHER_USER_ID)) } @Test @@ -118,17 +118,17 @@ class MultiInstanceHelperTest : ShellTestCase() { val component = ComponentName(TEST_PACKAGE, TEST_ACTIVITY) val pm = mock<PackageManager>() val activityProp = PackageManager.Property("", false, "", "") - whenever(pm.getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), - eq(component))) + whenever(pm.getPropertyAsUser(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), + eq(component.packageName), eq(component.className), eq(TEST_OTHER_USER_ID))) .thenReturn(activityProp) val appProp = PackageManager.Property("", true, "", "") - whenever(pm.getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), - eq(component.packageName))) + whenever(pm.getPropertyAsUser(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), + eq(component.packageName), eq(null), eq(TEST_OTHER_USER_ID))) .thenReturn(appProp) - val helper = MultiInstanceHelper(mContext, pm, emptyArray(), true) + val helper = MultiInstanceHelper(mContext, pm, emptyArray(), mock(), mock(), true) // Expect activity property to override application property - assertEquals(false, helper.supportsMultiInstanceSplit(component)) + assertEquals(false, helper.supportsMultiInstanceSplit(component, TEST_OTHER_USER_ID)) } @Test @@ -136,17 +136,17 @@ class MultiInstanceHelperTest : ShellTestCase() { fun supportsMultiInstanceSplit_noActivityPropertyApplicationPropertyTrue() { val component = ComponentName(TEST_PACKAGE, TEST_ACTIVITY) val pm = mock<PackageManager>() - whenever(pm.getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), - eq(component))) + whenever(pm.getPropertyAsUser(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), + eq(component.packageName), eq(component.className), eq(TEST_OTHER_USER_ID))) .thenThrow(PackageManager.NameNotFoundException()) val appProp = PackageManager.Property("", true, "", "") - whenever(pm.getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), - eq(component.packageName))) + whenever(pm.getPropertyAsUser(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), + eq(component.packageName), eq(null), eq(TEST_OTHER_USER_ID))) .thenReturn(appProp) - val helper = MultiInstanceHelper(mContext, pm, emptyArray(), true) + val helper = MultiInstanceHelper(mContext, pm, emptyArray(), mock(), mock(), true) // Expect fall through to app property - assertEquals(true, helper.supportsMultiInstanceSplit(component)) + assertEquals(true, helper.supportsMultiInstanceSplit(component, TEST_OTHER_USER_ID)) } @Test @@ -154,15 +154,15 @@ class MultiInstanceHelperTest : ShellTestCase() { fun supportsMultiInstanceSplit_noActivityOrAppProperty() { val component = ComponentName(TEST_PACKAGE, TEST_ACTIVITY) val pm = mock<PackageManager>() - whenever(pm.getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), - eq(component))) + whenever(pm.getPropertyAsUser(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), + eq(component.packageName), eq(component.className), eq(TEST_OTHER_USER_ID))) .thenThrow(PackageManager.NameNotFoundException()) - whenever(pm.getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), - eq(component.packageName))) + whenever(pm.getPropertyAsUser(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), + eq(component.packageName), eq(null), eq(TEST_OTHER_USER_ID))) .thenThrow(PackageManager.NameNotFoundException()) - val helper = MultiInstanceHelper(mContext, pm, emptyArray(), true) - assertEquals(false, helper.supportsMultiInstanceSplit(component)) + val helper = MultiInstanceHelper(mContext, pm, emptyArray(), mock(), mock(), true) + assertEquals(false, helper.supportsMultiInstanceSplit(component, TEST_OTHER_USER_ID)) } @Test @@ -171,24 +171,25 @@ class MultiInstanceHelperTest : ShellTestCase() { val component = ComponentName(TEST_PACKAGE, TEST_ACTIVITY) val pm = mock<PackageManager>() val activityProp = PackageManager.Property("", true, "", "") - whenever(pm.getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), - eq(component))) + whenever(pm.getPropertyAsUser(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), + eq(component.packageName), eq(component.className), eq(TEST_OTHER_USER_ID))) .thenReturn(activityProp) val appProp = PackageManager.Property("", true, "", "") - whenever(pm.getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), - eq(component.packageName))) + whenever(pm.getPropertyAsUser(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), + eq(component.packageName), eq(null), eq(TEST_OTHER_USER_ID))) .thenReturn(appProp) - val helper = MultiInstanceHelper(mContext, pm, emptyArray(), false) + val helper = MultiInstanceHelper(mContext, pm, emptyArray(), mock(), mock(), false) // Expect we only check the static list and not the property - assertEquals(false, helper.supportsMultiInstanceSplit(component)) + assertEquals(false, helper.supportsMultiInstanceSplit(component, TEST_OTHER_USER_ID)) verify(pm, never()).getProperty(any(), any<ComponentName>()) } companion object { val TEST_PACKAGE = "com.android.wm.shell.common" - val TEST_NOT_ALLOWED_PACKAGE = "com.android.wm.shell.common.fake"; - val TEST_ACTIVITY = "TestActivity"; + val TEST_NOT_ALLOWED_PACKAGE = "com.android.wm.shell.common.fake" + val TEST_ACTIVITY = "TestActivity" val TEST_SHORTCUT_ID = "test_shortcut_1" + val TEST_OTHER_USER_ID = 1234 } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/UserProfileContextsTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/UserProfileContextsTest.kt new file mode 100644 index 000000000000..ef0b8ab14c81 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/UserProfileContextsTest.kt @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common + +import android.app.ActivityManager +import android.content.Context +import android.content.pm.UserInfo +import android.os.UserHandle +import android.os.UserManager +import android.testing.AndroidTestingRunner +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestShellExecutor +import com.android.wm.shell.sysui.ShellController +import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.sysui.UserChangeListener +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.anyInt +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +/** + * Tests for [UserProfileContexts]. + */ +@RunWith(AndroidTestingRunner::class) +class UserProfileContextsTest : ShellTestCase() { + + private val testExecutor = TestShellExecutor() + private val shellInit = ShellInit(testExecutor) + private val activityManager = mock<ActivityManager>() + private val userManager = mock<UserManager>() + private val shellController = mock<ShellController>() + private val baseContext = mock<Context>() + + private lateinit var userProfilesContexts: UserProfileContexts + + @Before + fun setUp() { + doReturn(activityManager) + .whenever(baseContext) + .getSystemService(eq(ActivityManager::class.java)) + doReturn(userManager).whenever(baseContext).getSystemService(eq(UserManager::class.java)) + doAnswer { invocation -> + val userHandle = invocation.getArgument<UserHandle>(0) + createContextForUser(userHandle.identifier) + } + .whenever(baseContext) + .createContextAsUser(any<UserHandle>(), anyInt()) + // Define users and profiles + val currentUser = ActivityManager.getCurrentUser() + whenever(userManager.getProfiles(eq(currentUser))) + .thenReturn( + listOf(UserInfo(currentUser, "Current", 0), UserInfo(MAIN_PROFILE, "Work", 0)) + ) + whenever(userManager.getProfiles(eq(SECOND_USER))).thenReturn(SECOND_PROFILES) + userProfilesContexts = UserProfileContexts(baseContext, shellController, shellInit) + shellInit.init() + } + + @Test + fun onInit_registerUserChangeAndInit() { + val currentUser = ActivityManager.getCurrentUser() + + verify(shellController, times(1)).addUserChangeListener(any()) + assertThat(userProfilesContexts.userContext.userId).isEqualTo(currentUser) + assertThat(userProfilesContexts[currentUser]?.userId).isEqualTo(currentUser) + assertThat(userProfilesContexts[MAIN_PROFILE]?.userId).isEqualTo(MAIN_PROFILE) + assertThat(userProfilesContexts[SECOND_USER]).isNull() + } + + @Test + fun onUserChanged_updateUserContext() { + val userChangeListener = retrieveUserChangeListener() + val newUserContext = createContextForUser(SECOND_USER) + + userChangeListener.onUserChanged(SECOND_USER, newUserContext) + + assertThat(userProfilesContexts.userContext).isEqualTo(newUserContext) + assertThat(userProfilesContexts[SECOND_USER]).isEqualTo(newUserContext) + } + + @Test + fun onUserProfilesChanged_updateAllContexts() { + val userChangeListener = retrieveUserChangeListener() + val newUserContext = createContextForUser(SECOND_USER) + userChangeListener.onUserChanged(SECOND_USER, newUserContext) + + userChangeListener.onUserProfilesChanged(SECOND_PROFILES) + + assertThat(userProfilesContexts.userContext).isEqualTo(newUserContext) + assertThat(userProfilesContexts[SECOND_USER]).isEqualTo(newUserContext) + assertThat(userProfilesContexts[SECOND_PROFILE]?.userId).isEqualTo(SECOND_PROFILE) + assertThat(userProfilesContexts[SECOND_PROFILE_2]?.userId).isEqualTo(SECOND_PROFILE_2) + } + + @Test + fun onUserProfilesChanged_keepOnlyNewProfiles() { + val userChangeListener = retrieveUserChangeListener() + val newUserContext = createContextForUser(SECOND_USER) + userChangeListener.onUserChanged(SECOND_USER, newUserContext) + userChangeListener.onUserProfilesChanged(SECOND_PROFILES) + val newProfiles = listOf( + UserInfo(SECOND_USER, "Second", 0), + UserInfo(SECOND_PROFILE, "Second Profile", 0), + UserInfo(MAIN_PROFILE, "Main profile", 0), + ) + + userChangeListener.onUserProfilesChanged(newProfiles) + + assertThat(userProfilesContexts[SECOND_PROFILE_2]).isNull() + assertThat(userProfilesContexts[MAIN_PROFILE]?.userId).isEqualTo(MAIN_PROFILE) + assertThat(userProfilesContexts[SECOND_USER]?.userId).isEqualTo(SECOND_USER) + assertThat(userProfilesContexts[SECOND_PROFILE]?.userId).isEqualTo(SECOND_PROFILE) + } + + private fun retrieveUserChangeListener(): UserChangeListener { + val captor = argumentCaptor<UserChangeListener>() + + verify(shellController, times(1)).addUserChangeListener(captor.capture()) + + return captor.firstValue + } + + private fun createContextForUser(userId: Int): Context { + val newContext = mock<Context>() + whenever(newContext.userId).thenReturn(userId) + return newContext + } + + private companion object { + const val SECOND_USER = 3 + const val MAIN_PROFILE = 11 + const val SECOND_PROFILE = 15 + const val SECOND_PROFILE_2 = 17 + + val SECOND_PROFILES = + listOf( + UserInfo(SECOND_USER, "Second", 0), + UserInfo(SECOND_PROFILE, "Second Profile", 0), + UserInfo(SECOND_PROFILE_2, "Second Profile 2", 0), + ) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt index ecad5217b87f..957fdf995776 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt @@ -183,6 +183,8 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { @Test fun handleActivityOrientationChange_resizeable_doNothing() { + userRepositories.current.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + userRepositories.current.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val task = setUpFreeformTask() taskStackListener.onActivityRequestedOrientationChanged( @@ -195,6 +197,8 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { @Test fun handleActivityOrientationChange_nonResizeableFullscreen_doNothing() { + userRepositories.current.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + userRepositories.current.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val task = createFullscreenTask() task.isResizeable = false val activityInfo = ActivityInfo() @@ -214,6 +218,8 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { @Test fun handleActivityOrientationChange_nonResizeablePortrait_requestSameOrientation_doNothing() { + userRepositories.current.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + userRepositories.current.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val task = setUpFreeformTask(isResizeable = false) val newTask = setUpFreeformTask( @@ -228,6 +234,8 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { @Test fun handleActivityOrientationChange_notInDesktopMode_doNothing() { + userRepositories.current.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + userRepositories.current.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val task = setUpFreeformTask(isResizeable = false) userRepositories.current.updateTask(task.displayId, task.taskId, isVisible = false) @@ -241,6 +249,8 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { @Test fun handleActivityOrientationChange_nonResizeablePortrait_respectLandscapeRequest() { + userRepositories.current.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + userRepositories.current.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val task = setUpFreeformTask(isResizeable = false) val oldBounds = task.configuration.windowConfiguration.bounds val newTask = @@ -263,6 +273,8 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { @Test fun handleActivityOrientationChange_nonResizeableLandscape_respectPortraitRequest() { + userRepositories.current.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + userRepositories.current.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val oldBounds = Rect(0, 0, 500, 200) val task = setUpFreeformTask( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt index 6a3717427e93..fae7363e0676 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt @@ -16,10 +16,14 @@ package com.android.wm.shell.desktopmode +import android.app.ActivityManager.RunningTaskInfo import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.content.ContentResolver import android.os.Binder +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule import android.provider.Settings import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS import android.testing.AndroidTestingRunner @@ -29,20 +33,27 @@ import android.view.WindowManager.TRANSIT_CHANGE import android.window.DisplayAreaInfo import android.window.WindowContainerTransaction import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.dx.mockito.inline.extended.ExtendedMockito.never +import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.window.flags.Flags import com.android.wm.shell.MockToken import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestRunningTaskInfoBuilder import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions import com.google.common.truth.Truth.assertThat +import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.isNull import org.mockito.Mock import org.mockito.Mockito.anyInt @@ -53,6 +64,7 @@ import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness /** * Test class for [DesktopDisplayEventHandler] @@ -63,21 +75,44 @@ import org.mockito.kotlin.whenever @RunWith(AndroidTestingRunner::class) class DesktopDisplayEventHandlerTest : ShellTestCase() { + @JvmField @Rule val setFlagsRule = SetFlagsRule() + @Mock lateinit var testExecutor: ShellExecutor @Mock lateinit var transitions: Transitions @Mock lateinit var displayController: DisplayController @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer @Mock private lateinit var mockWindowManager: IWindowManager + @Mock private lateinit var mockDesktopUserRepositories: DesktopUserRepositories + @Mock private lateinit var mockDesktopRepository: DesktopRepository + @Mock private lateinit var mockDesktopTasksController: DesktopTasksController + @Mock private lateinit var shellTaskOrganizer: ShellTaskOrganizer + private lateinit var mockitoSession: StaticMockitoSession private lateinit var shellInit: ShellInit private lateinit var handler: DesktopDisplayEventHandler + private val onDisplaysChangedListenerCaptor = argumentCaptor<OnDisplaysChangedListener>() + private val runningTasks = mutableListOf<RunningTaskInfo>() + private val externalDisplayId = 100 + private val freeformTask = + TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build() + private val fullscreenTask = + TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FULLSCREEN).build() + private val defaultTDA = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) + @Before fun setUp() { + mockitoSession = + mockitoSession() + .strictness(Strictness.LENIENT) + .spyStatic(DesktopModeStatus::class.java) + .startMocking() + shellInit = spy(ShellInit(testExecutor)) + whenever(mockDesktopUserRepositories.current).thenReturn(mockDesktopRepository) whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() } - val tda = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) - whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda) + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .thenReturn(defaultTDA) handler = DesktopDisplayEventHandler( context, @@ -86,8 +121,21 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { displayController, rootTaskDisplayAreaOrganizer, mockWindowManager, + mockDesktopUserRepositories, + mockDesktopTasksController, + shellTaskOrganizer, ) + whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks } + runningTasks.add(freeformTask) + runningTasks.add(fullscreenTask) shellInit.init() + verify(displayController) + .addDisplayWindowListener(onDisplaysChangedListenerCaptor.capture()) + } + + @After + fun tearDown() { + mockitoSession.finishMocking() } private fun testDisplayWindowingModeSwitch( @@ -95,11 +143,7 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { extendedDisplayEnabled: Boolean, expectTransition: Boolean, ) { - val externalDisplayId = 100 - val captor = ArgumentCaptor.forClass(OnDisplaysChangedListener::class.java) - verify(displayController).addDisplayWindowListener(captor.capture()) - val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! - tda.configuration.windowConfiguration.windowingMode = defaultWindowingMode + defaultTDA.configuration.windowConfiguration.windowingMode = defaultWindowingMode whenever(mockWindowManager.getWindowingMode(anyInt())).thenAnswer { defaultWindowingMode } val settingsSession = ExtendedDisplaySettingsSession( @@ -108,23 +152,17 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { ) settingsSession.use { - // The external display connected. - whenever(rootTaskDisplayAreaOrganizer.getDisplayIds()) - .thenReturn(intArrayOf(DEFAULT_DISPLAY, externalDisplayId)) - captor.value.onDisplayAdded(externalDisplayId) - tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM - // The external display disconnected. - whenever(rootTaskDisplayAreaOrganizer.getDisplayIds()) - .thenReturn(intArrayOf(DEFAULT_DISPLAY)) - captor.value.onDisplayRemoved(externalDisplayId) + connectExternalDisplay() + defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM + disconnectExternalDisplay() if (expectTransition) { val arg = argumentCaptor<WindowContainerTransaction>() verify(transitions, times(2)) .startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull()) - assertThat(arg.firstValue.changes[tda.token.asBinder()]?.windowingMode) + assertThat(arg.firstValue.changes[defaultTDA.token.asBinder()]?.windowingMode) .isEqualTo(WINDOWING_MODE_FREEFORM) - assertThat(arg.secondValue.changes[tda.token.asBinder()]?.windowingMode) + assertThat(arg.secondValue.changes[defaultTDA.token.asBinder()]?.windowingMode) .isEqualTo(defaultWindowingMode) } else { verify(transitions, never()).startTransition(eq(TRANSIT_CHANGE), any(), isNull()) @@ -159,6 +197,96 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { ) } + @Test + fun testDisplayAdded_supportsDesks_createsDesk() { + whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true) + + onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY) + + verify(mockDesktopTasksController).createDesk(DEFAULT_DISPLAY) + } + + @Test + fun testDisplayAdded_cannotEnterDesktopMode_doesNotCreateDesk() { + whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(false) + + onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY) + + verify(mockDesktopTasksController, never()).createDesk(DEFAULT_DISPLAY) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun testDeskRemoved_noDesksRemain_createsDesk() { + whenever(mockDesktopRepository.getNumberOfDesks(DEFAULT_DISPLAY)).thenReturn(0) + + handler.onDeskRemoved(DEFAULT_DISPLAY, deskId = 1) + + verify(mockDesktopTasksController).createDesk(DEFAULT_DISPLAY) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun testDeskRemoved_desksRemain_doesNotCreateDesk() { + whenever(mockDesktopRepository.getNumberOfDesks(DEFAULT_DISPLAY)).thenReturn(1) + + handler.onDeskRemoved(DEFAULT_DISPLAY, deskId = 1) + + verify(mockDesktopTasksController, never()).createDesk(DEFAULT_DISPLAY) + } + + @Test + fun displayWindowingModeSwitch_existingTasksOnConnected() { + defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + whenever(mockWindowManager.getWindowingMode(anyInt())).thenAnswer { + WINDOWING_MODE_FULLSCREEN + } + + ExtendedDisplaySettingsSession(context.contentResolver, 1).use { + connectExternalDisplay() + + val arg = argumentCaptor<WindowContainerTransaction>() + verify(transitions, times(1)) + .startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull()) + assertThat(arg.firstValue.changes[freeformTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) + assertThat(arg.firstValue.changes[fullscreenTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FULLSCREEN) + } + } + + @Test + fun displayWindowingModeSwitch_existingTasksOnDisconnected() { + defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM + whenever(mockWindowManager.getWindowingMode(anyInt())).thenAnswer { + WINDOWING_MODE_FULLSCREEN + } + + ExtendedDisplaySettingsSession(context.contentResolver, 1).use { + disconnectExternalDisplay() + + val arg = argumentCaptor<WindowContainerTransaction>() + verify(transitions, times(1)) + .startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull()) + assertThat(arg.firstValue.changes[freeformTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + assertThat(arg.firstValue.changes[fullscreenTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) + } + } + + private fun connectExternalDisplay() { + whenever(rootTaskDisplayAreaOrganizer.getDisplayIds()) + .thenReturn(intArrayOf(DEFAULT_DISPLAY, externalDisplayId)) + onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(externalDisplayId) + } + + private fun disconnectExternalDisplay() { + whenever(rootTaskDisplayAreaOrganizer.getDisplayIds()) + .thenReturn(intArrayOf(DEFAULT_DISPLAY)) + onDisplaysChangedListenerCaptor.lastValue.onDisplayRemoved(externalDisplayId) + } + private class ExtendedDisplaySettingsSession( private val contentResolver: ContentResolver, private val overrideValue: Int, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt index 8d73f3f59afd..f5c93ee8ffe4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt @@ -17,13 +17,15 @@ package com.android.wm.shell.desktopmode import android.graphics.Rect +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.FlagsParameterization import android.platform.test.flag.junit.SetFlagsRule -import android.testing.AndroidTestingRunner import android.util.ArraySet import android.view.Display.DEFAULT_DISPLAY import android.view.Display.INVALID_DISPLAY import androidx.test.filters.SmallTest +import com.android.window.flags.Flags import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP import com.android.wm.shell.ShellTestCase @@ -56,6 +58,8 @@ import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters /** * Tests for [@link DesktopRepository]. @@ -63,11 +67,11 @@ import org.mockito.kotlin.whenever * Build/Install/Run: atest WMShellUnitTests:DesktopRepositoryTest */ @SmallTest -@RunWith(AndroidTestingRunner::class) +@RunWith(ParameterizedAndroidJunit4::class) @ExperimentalCoroutinesApi -class DesktopRepositoryTest : ShellTestCase() { +class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { - @JvmField @Rule val setFlagsRule = SetFlagsRule() + @JvmField @Rule val setFlagsRule = SetFlagsRule(flags) private lateinit var repo: DesktopRepository private lateinit var shellInit: ShellInit @@ -86,6 +90,8 @@ class DesktopRepositoryTest : ShellTestCase() { whenever(runBlocking { persistentRepository.readDesktop(any(), any()) }) .thenReturn(Desktop.getDefaultInstance()) shellInit.init() + repo.addDesk(displayId = DEFAULT_DISPLAY, deskId = DEFAULT_DESKTOP_ID) + repo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = DEFAULT_DESKTOP_ID) } @After @@ -137,6 +143,7 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun addTask_multipleDisplays_notifiesCorrectListener() { + repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val listener = TestListener() repo.addActiveTaskListener(listener) @@ -150,6 +157,7 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun addTask_multipleDisplays_moveToAnotherDisplay() { + repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) repo.addTask(SECOND_DISPLAY, taskId = 1, isVisible = true) assertThat(repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY)).isEmpty() @@ -310,19 +318,21 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun isOnlyVisibleNonClosingTask_multipleDisplays() { + repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) + repo.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) repo.updateTask(DEFAULT_DISPLAY, taskId = 2, isVisible = true) repo.updateTask(SECOND_DISPLAY, taskId = 3, isVisible = true) // Not the only task on DEFAULT_DISPLAY assertThat(repo.isVisibleTask(1)).isTrue() - assertThat(repo.isOnlyVisibleNonClosingTask(1)).isFalse() + assertThat(repo.isOnlyVisibleNonClosingTask(1, DEFAULT_DISPLAY)).isFalse() // Not the only task on DEFAULT_DISPLAY assertThat(repo.isVisibleTask(2)).isTrue() - assertThat(repo.isOnlyVisibleNonClosingTask(2)).isFalse() + assertThat(repo.isOnlyVisibleNonClosingTask(2, DEFAULT_DISPLAY)).isFalse() // The only visible task on SECOND_DISPLAY assertThat(repo.isVisibleTask(3)).isTrue() - assertThat(repo.isOnlyVisibleNonClosingTask(3)).isTrue() + assertThat(repo.isOnlyVisibleNonClosingTask(3, SECOND_DISPLAY)).isTrue() // Not a visible task assertThat(repo.isVisibleTask(99)).isFalse() assertThat(repo.isOnlyVisibleNonClosingTask(99)).isFalse() @@ -343,6 +353,7 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun addListener_tasksOnDifferentDisplay_doesNotNotify() { + repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) repo.updateTask(SECOND_DISPLAY, taskId = 1, isVisible = true) val listener = TestVisibilityListener() val executor = TestShellExecutor() @@ -351,7 +362,7 @@ class DesktopRepositoryTest : ShellTestCase() { assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(0) // One call as adding listener notifies it - assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(0) + assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1) } @Test @@ -365,11 +376,14 @@ class DesktopRepositoryTest : ShellTestCase() { executor.flushAll() assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(2) - assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(2) + // 1 from registration, 2 for the updates. + assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(3) } @Test fun updateTask_visibleTask_addVisibleTaskNotifiesListenerForThatDisplay() { + repo.addDesk(displayId = 1, deskId = 1) + repo.setActiveDesk(displayId = 1, deskId = 1) val listener = TestVisibilityListener() val executor = TestShellExecutor() repo.addVisibleTasksListener(listener, executor) @@ -378,22 +392,27 @@ class DesktopRepositoryTest : ShellTestCase() { executor.flushAll() assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(1) - assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1) + // 1 for the registration, 1 for the update. + assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(2) assertThat(listener.visibleTasksCountOnSecondaryDisplay).isEqualTo(0) - assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(0) + // 1 for the registration. + assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(1) repo.updateTask(displayId = 1, taskId = 2, isVisible = true) executor.flushAll() // Listener for secondary display is notified assertThat(listener.visibleTasksCountOnSecondaryDisplay).isEqualTo(1) - assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(1) + // 1 for the registration, 1 for the update. + assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(2) // No changes to listener for default display - assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1) + assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(2) } @Test fun updateTask_taskOnDefaultBecomesVisibleOnSecondDisplay_listenersNotified() { + repo.addDesk(displayId = 1, deskId = 1) + repo.setActiveDesk(displayId = 1, deskId = 1) val listener = TestVisibilityListener() val executor = TestShellExecutor() repo.addVisibleTasksListener(listener, executor) @@ -406,14 +425,15 @@ class DesktopRepositoryTest : ShellTestCase() { repo.updateTask(displayId = 1, taskId = 1, isVisible = true) executor.flushAll() - // Default display should have 2 calls - // 1 - visible task added - // 2 - visible task removed - assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(2) + // Default display should have 3 calls + // 1 - listener registered + // 2 - visible task added + // 3 - visible task removed + assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(3) assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(0) - // Secondary display should have 1 call for visible task added - assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(1) + // Secondary display should have 2 calls for registration + visible task added + assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(2) assertThat(listener.visibleTasksCountOnSecondaryDisplay).isEqualTo(1) } @@ -431,13 +451,13 @@ class DesktopRepositoryTest : ShellTestCase() { repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = false) executor.flushAll() - assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(3) + assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(4) repo.updateTask(DEFAULT_DISPLAY, taskId = 2, isVisible = false) executor.flushAll() assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(0) - assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(4) + assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(5) } /** @@ -458,7 +478,8 @@ class DesktopRepositoryTest : ShellTestCase() { repo.updateTask(INVALID_DISPLAY, taskId = 1, isVisible = false) executor.flushAll() - assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(3) + // 1 from registering, 1x3 for each update including the one to the invalid display. + assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(4) assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(1) } @@ -497,6 +518,8 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun getVisibleTaskCount_multipleDisplays_returnsCorrectCount() { + repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) + repo.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(0) @@ -674,8 +697,6 @@ class DesktopRepositoryTest : ShellTestCase() { repo.removeTask(INVALID_DISPLAY, taskId = 1) - val invalidDisplayTasks = repo.getFreeformTasksInZOrder(INVALID_DISPLAY) - assertThat(invalidDisplayTasks).isEmpty() val validDisplayTasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY) assertThat(validDisplayTasks).isEmpty() } @@ -746,6 +767,7 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun removeTask_validDisplay_differentDisplay_doesNotRemovesTask() { + repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) repo.removeTask(SECOND_DISPLAY, taskId = 1) @@ -758,6 +780,7 @@ class DesktopRepositoryTest : ShellTestCase() { @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) fun removeTask_validDisplayButDifferentDisplay_persistenceEnabled_doesNotRemoveTask() { runTest(StandardTestDispatcher()) { + repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) repo.removeTask(SECOND_DISPLAY, taskId = 1) @@ -784,10 +807,10 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun removeTask_removesTaskBoundsBeforeMaximize() { val taskId = 1 - repo.addTask(THIRD_DISPLAY, taskId, isVisible = true) + repo.addTask(DEFAULT_DISPLAY, taskId, isVisible = true) repo.saveBoundsBeforeMaximize(taskId, Rect(0, 0, 200, 200)) - repo.removeTask(THIRD_DISPLAY, taskId) + repo.removeTask(DEFAULT_DISPLAY, taskId) assertThat(repo.removeBoundsBeforeMaximize(taskId)).isNull() } @@ -795,16 +818,17 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun removeTask_removesTaskBoundsBeforeImmersive() { val taskId = 1 - repo.addTask(THIRD_DISPLAY, taskId, isVisible = true) + repo.addTask(DEFAULT_DISPLAY, taskId, isVisible = true) repo.saveBoundsBeforeFullImmersive(taskId, Rect(0, 0, 200, 200)) - repo.removeTask(THIRD_DISPLAY, taskId) + repo.removeTask(DEFAULT_DISPLAY, taskId) assertThat(repo.removeBoundsBeforeFullImmersive(taskId)).isNull() } @Test fun removeTask_removesActiveTask() { + repo.addDesk(THIRD_DISPLAY, THIRD_DISPLAY) val taskId = 1 val listener = TestListener() repo.addActiveTaskListener(listener) @@ -829,6 +853,7 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun removeTask_updatesTaskVisibility() { + repo.addDesk(displayId = THIRD_DISPLAY, deskId = THIRD_DISPLAY) val taskId = 1 repo.addTask(DEFAULT_DISPLAY, taskId, isVisible = true) @@ -930,8 +955,8 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun updateTask_minimizedTaskBecomesVisible_unminimizesTask() { - repo.minimizeTask(displayId = 10, taskId = 2) - repo.updateTask(displayId = 10, taskId = 2, isVisible = true) + repo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = 2) + repo.updateTask(displayId = DEFAULT_DISPLAY, taskId = 2, isVisible = true) val isMinimizedTask = repo.isMinimizedTask(taskId = 2) @@ -1003,34 +1028,34 @@ class DesktopRepositoryTest : ShellTestCase() { fun setTaskInFullImmersiveState_savedAsInImmersiveState() { assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isFalse() - repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true) + repo.setTaskInFullImmersiveState(DEFAULT_DISPLAY, taskId = 1, immersive = true) assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isTrue() } @Test fun removeTaskInFullImmersiveState_removedAsInImmersiveState() { - repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true) + repo.setTaskInFullImmersiveState(DEFAULT_DISPLAY, taskId = 1, immersive = true) assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isTrue() - repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = false) + repo.setTaskInFullImmersiveState(DEFAULT_DISPLAY, taskId = 1, immersive = false) assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isFalse() } @Test fun removeTaskInFullImmersiveState_otherWasImmersive_otherRemainsImmersive() { - repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true) + repo.setTaskInFullImmersiveState(DEFAULT_DISPLAY, taskId = 1, immersive = true) - repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 2, immersive = false) + repo.setTaskInFullImmersiveState(DEFAULT_DISPLAY, taskId = 2, immersive = false) assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isTrue() } @Test fun setTaskInFullImmersiveState_sameDisplay_overridesExistingFullImmersiveTask() { - repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true) - repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 2, immersive = true) + repo.setTaskInFullImmersiveState(DEFAULT_DISPLAY, taskId = 1, immersive = true) + repo.setTaskInFullImmersiveState(DEFAULT_DISPLAY, taskId = 2, immersive = true) assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isFalse() assertThat(repo.isTaskInFullImmersiveState(taskId = 2)).isTrue() @@ -1038,8 +1063,10 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun setTaskInFullImmersiveState_differentDisplay_bothAreImmersive() { - repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true) - repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID + 1, taskId = 2, immersive = true) + repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) + repo.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) + repo.setTaskInFullImmersiveState(DEFAULT_DISPLAY, taskId = 1, immersive = true) + repo.setTaskInFullImmersiveState(SECOND_DISPLAY, taskId = 2, immersive = true) assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isTrue() assertThat(repo.isTaskInFullImmersiveState(taskId = 2)).isTrue() @@ -1061,11 +1088,13 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun getTaskInFullImmersiveState_byDisplay() { + repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) + repo.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true) - repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID + 1, taskId = 2, immersive = true) + repo.setTaskInFullImmersiveState(SECOND_DISPLAY, taskId = 2, immersive = true) assertThat(repo.getTaskInFullImmersiveState(DEFAULT_DESKTOP_ID)).isEqualTo(1) - assertThat(repo.getTaskInFullImmersiveState(DEFAULT_DESKTOP_ID + 1)).isEqualTo(2) + assertThat(repo.getTaskInFullImmersiveState(SECOND_DISPLAY)).isEqualTo(2) } @Test @@ -1089,11 +1118,13 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun setTaskInPip_multipleDisplays_bothAreInPip() { + repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) + repo.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true) - repo.setTaskInPip(DEFAULT_DESKTOP_ID + 1, taskId = 2, enterPip = true) + repo.setTaskInPip(SECOND_DISPLAY, taskId = 2, enterPip = true) assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isTrue() - assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID + 1, taskId = 2)).isTrue() + assertThat(repo.isTaskMinimizedPipInDisplay(SECOND_DISPLAY, taskId = 2)).isTrue() } @Test @@ -1129,6 +1160,14 @@ class DesktopRepositoryTest : ShellTestCase() { assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isFalse() } + @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun addTask_deskDoesNotExists_createsDesk() { + repo.addTask(displayId = 999, taskId = 6, isVisible = true) + + assertThat(repo.getActiveTaskIdsInDesk(999)).contains(6) + } + class TestListener : DesktopRepository.ActiveTasksListener { var activeChangesOnDefaultDisplay = 0 var activeChangesOnSecondaryDisplay = 0 @@ -1169,5 +1208,10 @@ class DesktopRepositoryTest : ShellTestCase() { const val THIRD_DISPLAY = 345 private const val DEFAULT_USER_ID = 1000 private const val DEFAULT_DESKTOP_ID = 0 + + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> = + FlagsParameterization.allCombinationsOf(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index fffaab36c9ad..40c0e3610da2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -48,8 +48,8 @@ import android.os.IBinder import android.os.UserManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.FlagsParameterization import android.platform.test.flag.junit.SetFlagsRule -import android.testing.AndroidTestingRunner import android.view.Display.DEFAULT_DISPLAY import android.view.DragEvent import android.view.Gravity @@ -100,6 +100,7 @@ import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.common.MultiInstanceHelper import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue +import com.android.wm.shell.common.UserProfileContexts import com.android.wm.shell.desktopmode.DesktopImmersiveController.ExitResult import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.InputMethod import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.MinimizeReason @@ -117,6 +118,7 @@ import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler.FULLSCR import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider import com.android.wm.shell.desktopmode.minimize.DesktopWindowLimitRemoteHandler +import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer import com.android.wm.shell.desktopmode.persistence.Desktop import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer @@ -127,6 +129,7 @@ import com.android.wm.shell.recents.RecentsTransitionHandler import com.android.wm.shell.recents.RecentsTransitionStateListener import com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_ANIMATING import com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED +import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.UNKNOWN import com.android.wm.shell.shared.split.SplitScreenConstants @@ -184,6 +187,8 @@ import org.mockito.kotlin.capture import org.mockito.kotlin.eq import org.mockito.kotlin.whenever import org.mockito.quality.Strictness +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters /** * Test class for {@link DesktopTasksController} @@ -191,12 +196,12 @@ import org.mockito.quality.Strictness * Usage: atest WMShellUnitTests:DesktopTasksControllerTest */ @SmallTest -@RunWith(AndroidTestingRunner::class) +@RunWith(ParameterizedAndroidJunit4::class) @ExperimentalCoroutinesApi @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) -class DesktopTasksControllerTest : ShellTestCase() { +class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() { - @JvmField @Rule val setFlagsRule = SetFlagsRule() + @JvmField @Rule val setFlagsRule = SetFlagsRule(flags) @Mock lateinit var testExecutor: ShellExecutor @Mock lateinit var shellCommandHandler: ShellCommandHandler @@ -247,6 +252,8 @@ class DesktopTasksControllerTest : ShellTestCase() { DesktopWallpaperActivityTokenProvider @Mock private lateinit var overviewToDesktopTransitionObserver: OverviewToDesktopTransitionObserver + @Mock private lateinit var desksOrganizer: DesksOrganizer + @Mock private lateinit var userProfileContexts: UserProfileContexts private lateinit var controller: DesktopTasksController private lateinit var shellInit: ShellInit @@ -255,6 +262,7 @@ class DesktopTasksControllerTest : ShellTestCase() { private lateinit var desktopTasksLimiter: DesktopTasksLimiter private lateinit var recentsTransitionStateListener: RecentsTransitionStateListener private lateinit var testScope: CoroutineScope + private lateinit var desktopModeCompatPolicy: DesktopModeCompatPolicy private val shellExecutor = TestShellExecutor() @@ -305,6 +313,7 @@ class DesktopTasksControllerTest : ShellTestCase() { mContext, mockHandler, ) + desktopModeCompatPolicy = DesktopModeCompatPolicy(context) whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks } whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() } @@ -341,6 +350,7 @@ class DesktopTasksControllerTest : ShellTestCase() { ) .thenReturn(ExitResult.NoExit) whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(wallpaperToken) + whenever(userProfileContexts[anyInt()]).thenReturn(context) controller = createController() controller.setSplitScreenController(splitScreenController) @@ -358,6 +368,8 @@ class DesktopTasksControllerTest : ShellTestCase() { assumeTrue(ENABLE_SHELL_TRANSITIONS) taskRepository = userRepositories.current + taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = DEFAULT_DISPLAY) + taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = DEFAULT_DISPLAY) } private fun createController() = @@ -395,6 +407,9 @@ class DesktopTasksControllerTest : ShellTestCase() { desktopWallpaperActivityTokenProvider, Optional.of(bubbleController), overviewToDesktopTransitionObserver, + desksOrganizer, + userProfileContexts, + desktopModeCompatPolicy, ) @After @@ -613,7 +628,12 @@ class DesktopTasksControllerTest : ShellTestCase() { Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY, ) + @DisableFlags( + /** TODO: b/362720497 - re-enable when activation is implemented. */ + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND + ) fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_perDisplayWallpaperEnabled_shouldShowWallpaper() { + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val homeTask = setUpHomeTask(SECOND_DISPLAY) val task1 = setUpFreeformTask(SECOND_DISPLAY) val task2 = setUpFreeformTask(SECOND_DISPLAY) @@ -634,8 +654,13 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - @DisableFlags(Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY) + @DisableFlags( + Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY, + /** TODO: b/362720497 - re-enable when activation is implemented. */ + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_shouldNotShowWallpaper() { + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val homeTask = setUpHomeTask(SECOND_DISPLAY) val task1 = setUpFreeformTask(SECOND_DISPLAY) val task2 = setUpFreeformTask(SECOND_DISPLAY) @@ -674,8 +699,13 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + /** TODO: b/362720497 - re-enable when activation is implemented. */ + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) fun showDesktopApps_onSecondaryDisplay_desktopWallpaperDisabled_shouldNotMoveLauncher() { + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val homeTask = setUpHomeTask(SECOND_DISPLAY) val task1 = setUpFreeformTask(SECOND_DISPLAY) val task2 = setUpFreeformTask(SECOND_DISPLAY) @@ -777,6 +807,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperDisabled() { + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY) val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY) setUpHomeTask(SECOND_DISPLAY) @@ -797,6 +828,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperEnabled() { + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY) val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY) setUpHomeTask(SECOND_DISPLAY) @@ -883,6 +915,8 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun visibleTaskCount_twoTasksVisibleOnDifferentDisplays_returnsOne() { + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) + taskRepository.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) setUpHomeTask() setUpFreeformTask(DEFAULT_DISPLAY).also(::markTaskVisible) setUpFreeformTask(SECOND_DISPLAY).also(::markTaskVisible) @@ -1476,6 +1510,7 @@ class DesktopTasksControllerTest : ShellTestCase() { val fullscreenTaskDefault = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) markTaskHidden(freeformTaskDefault) + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val homeTaskSecond = setUpHomeTask(displayId = SECOND_DISPLAY) val freeformTaskSecond = setUpFreeformTask(displayId = SECOND_DISPLAY) markTaskHidden(freeformTaskSecond) @@ -1673,6 +1708,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun moveToFullscreen_secondDisplayTaskHasFreeform_secondDisplayNotAffected() { val taskDefaultDisplay = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val taskSecondDisplay = setUpFreeformTask(displayId = SECOND_DISPLAY) controller.moveToFullscreen(taskDefaultDisplay.taskId, transitionSource = UNKNOWN) @@ -1853,6 +1889,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun moveToNextDisplay_moveFromFirstToSecondDisplay() { // Set up two display ids + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) whenever(rootTaskDisplayAreaOrganizer.displayIds) .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) // Create a mock for the target display area: second display @@ -1882,6 +1919,7 @@ class DesktopTasksControllerTest : ShellTestCase() { whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) .thenReturn(defaultDisplayArea) + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val task = setUpFreeformTask(displayId = SECOND_DISPLAY) controller.moveToNextDisplay(task.taskId) @@ -1901,6 +1939,7 @@ class DesktopTasksControllerTest : ShellTestCase() { ) fun moveToNextDisplay_wallpaperOnSystemUser_reorderWallpaperToBack() { // Set up two display ids + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) whenever(rootTaskDisplayAreaOrganizer.displayIds) .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) // Create a mock for the target display area: second display @@ -1925,6 +1964,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER) fun moveToNextDisplay_wallpaperNotOnSystemUser_removeWallpaper() { // Set up two display ids + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) whenever(rootTaskDisplayAreaOrganizer.displayIds) .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) // Create a mock for the target display area: second display @@ -2049,6 +2089,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT) fun moveToNextDisplay_defaultBoundsWhenDestinationTooSmall() { + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) // Set up two display ids whenever(rootTaskDisplayAreaOrganizer.displayIds) .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) @@ -2090,6 +2131,7 @@ class DesktopTasksControllerTest : ShellTestCase() { FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT, ) fun moveToNextDisplay_destinationGainGlobalFocus() { + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) // Set up two display ids whenever(rootTaskDisplayAreaOrganizer.displayIds) .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) @@ -2977,6 +3019,27 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun handleRequest_systemUIActivityWithDisplay_returnSwitchToFullscreenWCT_enforcedDesktop() { + whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM + // Set task as systemUI package + val systemUIPackageName = + context.resources.getString(com.android.internal.R.string.config_systemUi) + val baseComponent = ComponentName(systemUIPackageName, /* cls= */ "") + val task = + createFreeformTask().apply { + baseActivity = baseComponent + isTopActivityNoDisplay = false + } + + assertThat(controller.isDesktopModeShowing(DEFAULT_DISPLAY)).isFalse() + val result = controller.handleRequest(Binder(), createTransition(task)) + assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode) + .isEqualTo(WINDOWING_MODE_FULLSCREEN) + } + + @Test @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_backTransition_singleTaskNoToken_noWallpaper_doesNotHandle() { val task = setUpFreeformTask() @@ -3158,6 +3221,8 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_closeTransition_singleTaskNoToken_secondaryDisplay_launchesHome() { + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) + taskRepository.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val task = setUpFreeformTask(displayId = SECOND_DISPLAY) whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) @@ -4933,7 +4998,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) fun shouldPlayDesktopAnimation_notShowingDesktop_doesNotPlay() { - val triggerTask = setUpFullscreenTask(displayId = 5) + val triggerTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) taskRepository.setTaskInFullImmersiveState( displayId = triggerTask.displayId, taskId = triggerTask.taskId, @@ -4951,7 +5016,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) fun shouldPlayDesktopAnimation_notOpening_doesNotPlay() { - val triggerTask = setUpFreeformTask(displayId = 5) + val triggerTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY) taskRepository.setTaskInFullImmersiveState( displayId = triggerTask.displayId, taskId = triggerTask.taskId, @@ -4969,7 +5034,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) fun shouldPlayDesktopAnimation_notImmersive_doesNotPlay() { - val triggerTask = setUpFreeformTask(displayId = 5) + val triggerTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY) taskRepository.setTaskInFullImmersiveState( displayId = triggerTask.displayId, taskId = triggerTask.taskId, @@ -4988,8 +5053,8 @@ class DesktopTasksControllerTest : ShellTestCase() { @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) fun shouldPlayDesktopAnimation_fullscreenEntersDesktop_plays() { // At least one freeform task to be in a desktop. - val existingTask = setUpFreeformTask(displayId = 5) - val triggerTask = setUpFullscreenTask(displayId = 5) + val existingTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val triggerTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isTrue() taskRepository.setTaskInFullImmersiveState( displayId = existingTask.displayId, @@ -5008,7 +5073,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) fun shouldPlayDesktopAnimation_fullscreenStaysFullscreen_doesNotPlay() { - val triggerTask = setUpFullscreenTask(displayId = 5) + val triggerTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isFalse() assertThat( @@ -5023,8 +5088,8 @@ class DesktopTasksControllerTest : ShellTestCase() { @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) fun shouldPlayDesktopAnimation_freeformStaysInDesktop_plays() { // At least one freeform task to be in a desktop. - val existingTask = setUpFreeformTask(displayId = 5) - val triggerTask = setUpFreeformTask(displayId = 5, active = false) + val existingTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val triggerTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, active = false) assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isTrue() taskRepository.setTaskInFullImmersiveState( displayId = existingTask.displayId, @@ -5043,7 +5108,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) fun shouldPlayDesktopAnimation_freeformExitsDesktop_doesNotPlay() { - val triggerTask = setUpFreeformTask(displayId = 5, active = false) + val triggerTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, active = false) assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isFalse() assertThat( @@ -5054,6 +5119,19 @@ class DesktopTasksControllerTest : ShellTestCase() { .isFalse() } + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun testCreateDesk() { + val currentDeskCount = taskRepository.getNumberOfDesks(DEFAULT_DISPLAY) + whenever(desksOrganizer.createDesk(eq(DEFAULT_DISPLAY), any())).thenAnswer { invocation -> + (invocation.arguments[1] as DesksOrganizer.OnCreateCallback).onCreated(deskId = 5) + } + + controller.createDesk(DEFAULT_DISPLAY) + + assertThat(taskRepository.getNumberOfDesks(DEFAULT_DISPLAY)).isEqualTo(currentDeskCount + 1) + } + private class RunOnStartTransitionCallback : ((IBinder) -> Unit) { var invocations = 0 private set @@ -5098,7 +5176,8 @@ class DesktopTasksControllerTest : ShellTestCase() { whenever(mockDragEvent.dragSurface).thenReturn(dragSurface) whenever(mockDragEvent.x).thenReturn(inputCoordinate.x) whenever(mockDragEvent.y).thenReturn(inputCoordinate.y) - whenever(multiInstanceHelper.supportsMultiInstanceSplit(anyOrNull())).thenReturn(true) + whenever(multiInstanceHelper.supportsMultiInstanceSplit(anyOrNull(), anyInt())) + .thenReturn(true) whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) doReturn(indicatorType) .whenever(spyController) @@ -5112,6 +5191,7 @@ class DesktopTasksControllerTest : ShellTestCase() { spyController.onUnhandledDrag( mockPendingIntent, + context.userId, mockDragEvent, mockCallback as Consumer<Boolean>, ) @@ -5387,6 +5467,11 @@ class DesktopTasksControllerTest : ShellTestCase() { val STABLE_BOUNDS = Rect(0, 0, 1000, 1000) const val MAX_TASK_LIMIT = 6 private const val TASKBAR_FRAME_HEIGHT = 200 + + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> = + FlagsParameterization.allCombinationsOf(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt index e85901bbd9d4..554b09f130bd 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt @@ -180,6 +180,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun addPendingMinimizeTransition_taskIsNotMinimized() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val task = setUpFreeformTask() markTaskHidden(task) @@ -190,6 +192,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun onTransitionReady_noPendingTransition_taskIsNotMinimized() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val task = setUpFreeformTask() markTaskHidden(task) @@ -203,6 +207,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun onTransitionReady_differentPendingTransition_taskIsNotMinimized() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val pendingTransition = Binder() val taskTransition = Binder() val task = setUpFreeformTask() @@ -219,6 +225,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun onTransitionReady_pendingTransition_noTaskChange_taskVisible_taskIsNotMinimized() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val transition = Binder() val task = setUpFreeformTask() markTaskVisible(task) @@ -232,6 +240,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun onTransitionReady_pendingTransition_noTaskChange_taskInvisible_taskIsMinimized() { val transition = Binder() + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val task = setUpFreeformTask() markTaskHidden(task) addPendingMinimizeChange(transition, taskId = task.taskId) @@ -243,6 +253,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun onTransitionReady_pendingTransition_changeTaskToBack_taskIsMinimized() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val transition = Binder() val task = setUpFreeformTask() addPendingMinimizeChange(transition, taskId = task.taskId) @@ -257,6 +269,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun onTransitionReady_pendingTransition_changeTaskToBack_boundsSaved() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val bounds = Rect(0, 0, 200, 200) val transition = Binder() val task = setUpFreeformTask() @@ -280,6 +294,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun onTransitionReady_transitionMergedFromPending_taskIsMinimized() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val mergedTransition = Binder() val newTransition = Binder() val task = setUpFreeformTask() @@ -302,6 +318,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) fun removeLeftoverMinimizedTasks_activeNonMinimizedTasksStillAround_doesNothing() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) desktopTaskRepo.addTask(displayId = DEFAULT_DISPLAY, taskId = 1, isVisible = true) desktopTaskRepo.addTask(displayId = DEFAULT_DISPLAY, taskId = 2, isVisible = true) desktopTaskRepo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = 2) @@ -318,6 +336,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) fun removeLeftoverMinimizedTasks_noMinimizedTasks_doesNothing() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val wct = WindowContainerTransaction() desktopTasksLimiter.leftoverMinimizedTasksRemover.removeLeftoverMinimizedTasks( DEFAULT_DISPLAY, @@ -330,6 +350,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) fun removeLeftoverMinimizedTasks_onlyMinimizedTasksLeft_removesAllMinimizedTasks() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) desktopTaskRepo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task1.taskId) @@ -351,6 +373,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) fun removeLeftoverMinimizedTasks_onlyMinimizedTasksLeft_backNavEnabled_doesNothing() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) desktopTaskRepo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task1.taskId) @@ -364,6 +388,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun addAndGetMinimizeTaskChanges_tasksWithinLimit_noTaskMinimized() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) (1..<MAX_TASK_LIMIT).forEach { _ -> setUpFreeformTask() } val wct = WindowContainerTransaction() @@ -380,6 +406,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun addAndGetMinimizeTaskChanges_tasksAboveLimit_backTaskMinimized() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) // The following list will be ordered bottom -> top, as the last task is moved to top last. val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() } @@ -399,6 +427,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun addAndGetMinimizeTaskChanges_nonMinimizedTasksWithinLimit_noTaskMinimized() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() } desktopTaskRepo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = tasks[0].taskId) @@ -416,6 +446,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun getTaskToMinimize_tasksWithinLimit_returnsNull() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() } val minimizedTask = @@ -426,6 +458,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun getTaskToMinimize_tasksAboveLimit_returnsBackTask() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val tasks = (1..MAX_TASK_LIMIT + 1).map { setUpFreeformTask() } val minimizedTask = @@ -437,6 +471,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun getTaskToMinimize_tasksAboveLimit_otherLimit_returnsBackTask() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) desktopTasksLimiter = DesktopTasksLimiter( transitions, @@ -458,6 +494,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun getTaskToMinimize_withNewTask_tasksAboveLimit_returnsBackTask() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() } val minimizedTask = @@ -472,6 +510,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun getTaskToMinimize_tasksAtLimit_newIntentReturnsBackTask() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() } val minimizedTask = desktopTasksLimiter.getTaskIdToMinimize( @@ -486,6 +526,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun minimizeTransitionReadyAndFinished_logsJankInstrumentationBeginAndEnd() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) (1..<MAX_TASK_LIMIT).forEach { _ -> setUpFreeformTask() } val transition = Binder() val task = setUpFreeformTask() @@ -510,6 +552,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun minimizeTransitionReadyAndAborted_logsJankInstrumentationBeginAndCancel() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) (1..<MAX_TASK_LIMIT).forEach { _ -> setUpFreeformTask() } val transition = Binder() val task = setUpFreeformTask() @@ -534,6 +578,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun minimizeTransitionReadyAndMerged_logsJankInstrumentationBeginAndEnd() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) (1..<MAX_TASK_LIMIT).forEach { _ -> setUpFreeformTask() } val mergedTransition = Binder() val newTransition = Binder() @@ -566,6 +612,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun getMinimizingTask_pendingTaskTransition_returnsTask() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val transition = Binder() val task = setUpFreeformTask() addPendingMinimizeChange( @@ -582,6 +630,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun getMinimizingTask_activeTaskTransition_returnsTask() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val transition = Binder() val task = setUpFreeformTask() addPendingMinimizeChange( @@ -613,6 +663,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun getUnminimizingTask_pendingTaskTransition_returnsTask() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val transition = Binder() val task = setUpFreeformTask() addPendingUnminimizeChange( @@ -632,6 +684,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun getUnminimizingTask_activeTaskTransition_returnsTask() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val transition = Binder() val task = setUpFreeformTask() addPendingMinimizeChange( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt index aee8821a63f6..8b6cafb10df4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt @@ -35,6 +35,7 @@ object DesktopTestHelpers { ): RunningTaskInfo = TestRunningTaskInfoBuilder() .setDisplayId(displayId) + .setParentTaskId(displayId) .setToken(MockToken().token()) .setActivityType(ACTIVITY_TYPE_STANDARD) .setWindowingMode(WINDOWING_MODE_FREEFORM) @@ -73,10 +74,14 @@ object DesktopTestHelpers { .setLastActiveTime(100) .build() + /** + * Create a new System Modal task builder, i.e. a builder for a task with only transparent + * activities. + */ + fun createSystemModalTaskBuilder(displayId: Int = DEFAULT_DISPLAY): TestRunningTaskInfoBuilder = + createFullscreenTaskBuilder(displayId).setActivityStackTransparent(true).setNumActivities(1) + /** Create a new System Modal task, i.e. a task with only transparent activities. */ fun createSystemModalTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo = - createFullscreenTaskBuilder(displayId) - .setActivityStackTransparent(true) - .setNumActivities(1) - .build() + createSystemModalTaskBuilder(displayId).build() } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/compatui/SystemModalsTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/compatui/SystemModalsTransitionHandlerTest.kt index 1569f9dc9b10..dfb1b0c8c642 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/compatui/SystemModalsTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/compatui/SystemModalsTransitionHandlerTest.kt @@ -16,6 +16,7 @@ package com.android.wm.shell.desktopmode.compatui +import android.content.Intent import android.os.Binder import android.testing.AndroidTestingRunner import android.view.SurfaceControl @@ -29,7 +30,10 @@ import com.android.wm.shell.desktopmode.DesktopRepository import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFullscreenTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFullscreenTaskBuilder import com.android.wm.shell.desktopmode.DesktopTestHelpers.createSystemModalTask +import com.android.wm.shell.desktopmode.DesktopTestHelpers.createSystemModalTaskBuilder import com.android.wm.shell.desktopmode.DesktopUserRepositories +import com.android.wm.shell.desktopmode.DesktopWallpaperActivity +import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.TransitionInfoBuilder import com.android.wm.shell.transition.Transitions @@ -60,12 +64,14 @@ class SystemModalsTransitionHandlerTest : ShellTestCase() { private val finishT = mock<SurfaceControl.Transaction>() private lateinit var transitionHandler: SystemModalsTransitionHandler + private lateinit var desktopModeCompatPolicy: DesktopModeCompatPolicy @Before fun setUp() { // Simulate having one Desktop task so that we see Desktop Mode as active whenever(desktopUserRepositories.current).thenReturn(desktopRepository) whenever(desktopRepository.getVisibleTaskCount(anyInt())).thenReturn(1) + desktopModeCompatPolicy = DesktopModeCompatPolicy(context) transitionHandler = createTransitionHandler() } @@ -77,6 +83,7 @@ class SystemModalsTransitionHandlerTest : ShellTestCase() { shellInit, transitions, desktopUserRepositories, + desktopModeCompatPolicy, ) @Test @@ -116,6 +123,19 @@ class SystemModalsTransitionHandlerTest : ShellTestCase() { } @Test + fun startAnimation_launchingWallpaperTask_doesNotAnimate() { + val wallpaperTask = + createSystemModalTaskBuilder().setBaseIntent(createWallpaperIntent()).build() + val info = + TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_OPEN, wallpaperTask).build() + + assertThat(transitionHandler.startAnimation(Binder(), info, startT, finishT) {}).isFalse() + } + + private fun createWallpaperIntent() = + Intent().apply { setComponent(DesktopWallpaperActivity.wallpaperActivityComponent) } + + @Test fun startAnimation_launchingFullscreenTask_doesNotAnimate() { val info = TransitionInfoBuilder(TRANSIT_OPEN) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt index 5475032f35a9..493a8c83c48e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt @@ -29,7 +29,6 @@ import com.android.wm.shell.ShellTestCase import com.android.wm.shell.desktopmode.CaptionState import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository import com.android.wm.shell.desktopmode.education.AppHandleEducationController.Companion.APP_HANDLE_EDUCATION_DELAY_MILLIS -import com.android.wm.shell.desktopmode.education.AppHandleEducationController.Companion.APP_HANDLE_EDUCATION_TIMEOUT_MILLIS import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource @@ -47,7 +46,6 @@ import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.junit.Before -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -113,10 +111,10 @@ class AppHandleEducationControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - fun init_appHandleVisible_shouldCallShowEducationTooltip() = + fun init_appHandleVisible_shouldCallShowEducationTooltipAndMarkAsViewed() = testScope.runTest { // App handle is visible. Should show education tooltip. - setShouldShowAppHandleEducation(true) + setShouldShowDesktopModeEducation(true) // Simulate app handle visible. testCaptionStateFlow.value = createAppHandleState() @@ -124,6 +122,38 @@ class AppHandleEducationControllerTest : ShellTestCase() { waitForBufferDelay() verify(mockTooltipController, times(1)).showEducationTooltip(any(), any()) + verify(mockDataStoreRepository, times(1)) + .updateAppHandleHintViewedTimestampMillis(eq(true)) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + fun init_appHandleVisibleAndMenuExpanded_shouldCallShowEducationTooltipAndMarkAsViewed() = + testScope.runTest { + setShouldShowDesktopModeEducation(true) + + // Simulate app handle visible and handle menu is expanded. + testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true) + waitForBufferDelay() + + verify(mockTooltipController, times(1)).showEducationTooltip(any(), any()) + verify(mockDataStoreRepository, times(1)) + .updateEnterDesktopModeHintViewedTimestampMillis(eq(true)) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + fun init_appHeaderVisible_shouldCallShowEducationTooltipAndMarkAsViewed() = + testScope.runTest { + setShouldShowDesktopModeEducation(true) + + // Simulate app header visible. + testCaptionStateFlow.value = createAppHeaderState() + waitForBufferDelay() + + verify(mockTooltipController, times(1)).showEducationTooltip(any(), any()) + verify(mockDataStoreRepository, times(1)) + .updateExitDesktopModeHintViewedTimestampMillis(eq(true)) } @Test @@ -133,7 +163,7 @@ class AppHandleEducationControllerTest : ShellTestCase() { // App handle visible but education aconfig flag disabled, should not show education // tooltip. whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(false) - setShouldShowAppHandleEducation(true) + setShouldShowDesktopModeEducation(true) // Simulate app handle visible. testCaptionStateFlow.value = createAppHandleState() @@ -145,12 +175,11 @@ class AppHandleEducationControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - fun init_shouldShowAppHandleEducationReturnsFalse_shouldNotCallShowEducationTooltip() = + fun init_shouldShowDesktopModeEducationReturnsFalse_shouldNotCallShowEducationTooltip() = testScope.runTest { - // App handle is visible but [shouldShowAppHandleEducation] api returns false, should - // not - // show education tooltip. - setShouldShowAppHandleEducation(false) + // App handle is visible but [shouldShowDesktopModeEducation] api returns false, should + // not show education tooltip. + setShouldShowDesktopModeEducation(false) // Simulate app handle visible. testCaptionStateFlow.value = createAppHandleState() @@ -165,7 +194,7 @@ class AppHandleEducationControllerTest : ShellTestCase() { fun init_appHandleNotVisible_shouldNotCallShowEducationTooltip() = testScope.runTest { // App handle is not visible, should not show education tooltip. - setShouldShowAppHandleEducation(true) + setShouldShowDesktopModeEducation(true) // Simulate app handle is not visible. testCaptionStateFlow.value = CaptionState.NoCaption @@ -184,7 +213,7 @@ class AppHandleEducationControllerTest : ShellTestCase() { // Mark app handle hint viewed. testDataStoreFlow.value = createWindowingEducationProto(appHandleHintViewedTimestampMillis = 123L) - setShouldShowAppHandleEducation(true) + setShouldShowDesktopModeEducation(true) // Simulate app handle visible. testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = false) @@ -196,231 +225,95 @@ class AppHandleEducationControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - fun overridePrerequisite_appHandleHintViewedAlready_shouldCallShowEducationTooltip() = + fun init_enterDesktopModeHintViewedAlready_shouldNotCallShowEducationTooltip() = testScope.runTest { - // App handle is visible but app handle hint has been viewed before. - // But as we are overriding prerequisite conditions, we should show app - // handle tooltip. + // App handle is visible but app handle hint has been viewed before, + // should not show education tooltip. // Mark app handle hint viewed. testDataStoreFlow.value = - createWindowingEducationProto(appHandleHintViewedTimestampMillis = 123L) - val systemPropertiesKey = - "persist.desktop_windowing_app_handle_education_override_conditions" - whenever(SystemProperties.getBoolean(eq(systemPropertiesKey), anyBoolean())) - .thenReturn(true) - setShouldShowAppHandleEducation(true) + createWindowingEducationProto(enterDesktopModeHintViewedTimestampMillis = 123L) + setShouldShowDesktopModeEducation(true) // Simulate app handle visible. - testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = false) - // Wait for first tooltip to showup. - waitForBufferDelay() - - verify(mockTooltipController, times(1)).showEducationTooltip(any(), any()) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - fun init_appHandleExpanded_shouldMarkAppHandleHintUsed() = - testScope.runTest { - setShouldShowAppHandleEducation(false) - - // Simulate app handle visible and expanded. testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true) - // Wait for some time before verifying + // Wait for first tooltip to showup. waitForBufferDelay() - verify(mockDataStoreRepository, times(1)) - .updateAppHandleHintUsedTimestampMillis(eq(true)) + verify(mockTooltipController, never()).showEducationTooltip(any(), any()) } @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - fun init_showFirstTooltip_shouldMarkAppHandleHintViewed() = + fun init_exitDesktopModeHintViewedAlready_shouldNotCallShowEducationTooltip() = testScope.runTest { - // App handle is visible. Should show education tooltip. - setShouldShowAppHandleEducation(true) + // App handle is visible but app handle hint has been viewed before, + // should not show education tooltip. + // Mark app handle hint viewed. + testDataStoreFlow.value = + createWindowingEducationProto(exitDesktopModeHintViewedTimestampMillis = 123L) + setShouldShowDesktopModeEducation(true) // Simulate app handle visible. - testCaptionStateFlow.value = createAppHandleState() + testCaptionStateFlow.value = createAppHeaderState() // Wait for first tooltip to showup. waitForBufferDelay() - verify(mockDataStoreRepository, times(1)) - .updateAppHandleHintViewedTimestampMillis(eq(true)) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - @Ignore("b/371527084: revisit testcase after refactoring original logic") - fun showWindowingImageButtonTooltip_appHandleExpanded_shouldCallShowEducationTooltipTwice() = - testScope.runTest { - // After first tooltip is dismissed, app handle is expanded. Should show second - // education - // tooltip. - showAndDismissFirstTooltip() - - // Simulate app handle expanded. - testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true) - // Wait for next tooltip to showup. - waitForBufferDelay() - - // [showEducationTooltip] should be called twice, once for each tooltip. - verify(mockTooltipController, times(2)).showEducationTooltip(any(), any()) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - @Ignore("b/371527084: revisit testcase after refactoring original logic") - fun showWindowingImageButtonTooltip_appHandleExpandedAfterTimeout_shouldCallShowEducationTooltipOnce() = - testScope.runTest { - // After first tooltip is dismissed, app handle is expanded after timeout. Should not - // show - // second education tooltip. - showAndDismissFirstTooltip() - - // Wait for timeout to occur, after this timeout we should not listen for further - // triggers - // anymore. - advanceTimeBy(APP_HANDLE_EDUCATION_TIMEOUT_BUFFER_MILLIS) - runCurrent() - // Simulate app handle expanded. - testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true) - // Wait for next tooltip to showup. - waitForBufferDelay() - - // [showEducationTooltip] should be called once, just for the first tooltip. - verify(mockTooltipController, times(1)).showEducationTooltip(any(), any()) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - @Ignore("b/371527084: revisit testcase after refactoring original logic") - fun showWindowingImageButtonTooltip_appHandleExpandedTwice_shouldCallShowEducationTooltipTwice() = - testScope.runTest { - // After first tooltip is dismissed, app handle is expanded twice. Should show second - // education tooltip only once. - showAndDismissFirstTooltip() - - // Simulate app handle expanded. - testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true) - // Wait for next tooltip to showup. - waitForBufferDelay() - // Simulate app handle being expanded twice. - testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true) - waitForBufferDelay() - - // [showEducationTooltip] should not be called thrice, even if app handle was expanded - // twice. Should be called twice, once for each tooltip. - verify(mockTooltipController, times(2)).showEducationTooltip(any(), any()) + verify(mockTooltipController, never()).showEducationTooltip(any(), any()) } @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - @Ignore("b/371527084: revisit testcase after refactoring original logic") - fun showWindowingImageButtonTooltip_appHandleNotExpanded_shouldCallShowEducationTooltipOnce() = + fun overridePrerequisite_appHandleHintViewedAlready_shouldCallShowEducationTooltip() = testScope.runTest { - // After first tooltip is dismissed, app handle is not expanded. Should not show second - // education tooltip. - showAndDismissFirstTooltip() + // App handle is visible but app handle hint has been viewed before. + // But as we are overriding prerequisite conditions, we should show app + // handle tooltip. + // Mark app handle hint viewed. + testDataStoreFlow.value = + createWindowingEducationProto(appHandleHintViewedTimestampMillis = 123L) + val systemPropertiesKey = "persist.windowing_force_show_desktop_mode_education" + whenever(SystemProperties.getBoolean(eq(systemPropertiesKey), anyBoolean())) + .thenReturn(true) + setShouldShowDesktopModeEducation(true) - // Simulate app handle visible but not expanded. + // Simulate app handle visible. testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = false) - // Wait for next tooltip to showup. + // Wait for first tooltip to showup. waitForBufferDelay() - // [showEducationTooltip] should be called once, just for the first tooltip. verify(mockTooltipController, times(1)).showEducationTooltip(any(), any()) } @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - @Ignore("b/371527084: revisit testcase after refactoring original logic") - fun showExitWindowingButtonTooltip_appHeaderVisible_shouldCallShowEducationTooltipThrice() = - testScope.runTest { - // After first two tooltips are dismissed, app header is visible. Should show third - // education tooltip. - showAndDismissFirstTooltip() - showAndDismissSecondTooltip() - - // Simulate app header visible. - testCaptionStateFlow.value = createAppHeaderState() - // Wait for next tooltip to showup. - waitForBufferDelay() - - verify(mockTooltipController, times(3)).showEducationTooltip(any(), any()) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - @Ignore("b/371527084: revisit testcase after refactoring original logic") - fun showExitWindowingButtonTooltip_appHeaderVisibleAfterTimeout_shouldCallShowEducationTooltipTwice() = - testScope.runTest { - // After first two tooltips are dismissed, app header is visible after timeout. Should - // not - // show third education tooltip. - showAndDismissFirstTooltip() - showAndDismissSecondTooltip() - - // Wait for timeout to occur, after this timeout we should not listen for further - // triggers - // anymore. - advanceTimeBy(APP_HANDLE_EDUCATION_TIMEOUT_BUFFER_MILLIS) - runCurrent() - // Simulate app header visible. - testCaptionStateFlow.value = createAppHeaderState() - // Wait for next tooltip to showup. - waitForBufferDelay() - - verify(mockTooltipController, times(2)).showEducationTooltip(any(), any()) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - @Ignore("b/371527084: revisit testcase after refactoring original logic") - fun showExitWindowingButtonTooltip_appHeaderVisibleTwice_shouldCallShowEducationTooltipThrice() = + fun clickAppHandleHint_openHandleMenuCallbackInvoked() = testScope.runTest { - // After first two tooltips are dismissed, app header is visible twice. Should show - // third - // education tooltip only once. - showAndDismissFirstTooltip() - showAndDismissSecondTooltip() - - // Simulate app header visible. - testCaptionStateFlow.value = createAppHeaderState() - // Wait for next tooltip to showup. - waitForBufferDelay() - testCaptionStateFlow.value = createAppHeaderState() - // Wait for next tooltip to showup. + // App handle is visible. Should show education tooltip. + setShouldShowDesktopModeEducation(true) + val mockOpenHandleMenuCallback: (Int) -> Unit = mock() + val mockToDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit = mock() + educationController.setAppHandleEducationTooltipCallbacks( + mockOpenHandleMenuCallback, + mockToDesktopModeCallback, + ) + // Simulate app handle visible. + testCaptionStateFlow.value = createAppHandleState() + // Wait for first tooltip to showup. waitForBufferDelay() - verify(mockTooltipController, times(3)).showEducationTooltip(any(), any()) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - @Ignore("b/371527084: revisit testcase after refactoring original logic") - fun showExitWindowingButtonTooltip_appHeaderExpanded_shouldCallShowEducationTooltipTwice() = - testScope.runTest { - // After first two tooltips are dismissed, app header is visible but expanded. Should - // not - // show third education tooltip. - showAndDismissFirstTooltip() - showAndDismissSecondTooltip() - - // Simulate app header visible. - testCaptionStateFlow.value = createAppHeaderState(isHeaderMenuExpanded = true) - // Wait for next tooltip to showup. - waitForBufferDelay() + verify(mockTooltipController, atLeastOnce()) + .showEducationTooltip(educationConfigCaptor.capture(), any()) + educationConfigCaptor.lastValue.onEducationClickAction.invoke() - verify(mockTooltipController, times(2)).showEducationTooltip(any(), any()) + verify(mockOpenHandleMenuCallback, times(1)).invoke(any()) } @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - fun setAppHandleEducationTooltipCallbacks_onAppHandleTooltipClicked_callbackInvoked() = + fun clickEnterDesktopModeHint_toDesktopModeCallbackInvoked() = testScope.runTest { // App handle is visible. Should show education tooltip. - setShouldShowAppHandleEducation(true) + setShouldShowDesktopModeEducation(true) val mockOpenHandleMenuCallback: (Int) -> Unit = mock() val mockToDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit = mock() educationController.setAppHandleEducationTooltipCallbacks( @@ -428,7 +321,7 @@ class AppHandleEducationControllerTest : ShellTestCase() { mockToDesktopModeCallback, ) // Simulate app handle visible. - testCaptionStateFlow.value = createAppHandleState() + testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true) // Wait for first tooltip to showup. waitForBufferDelay() @@ -436,68 +329,41 @@ class AppHandleEducationControllerTest : ShellTestCase() { .showEducationTooltip(educationConfigCaptor.capture(), any()) educationConfigCaptor.lastValue.onEducationClickAction.invoke() - verify(mockOpenHandleMenuCallback, times(1)).invoke(any()) + verify(mockToDesktopModeCallback, times(1)) + .invoke(any(), eq(DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON)) } @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - @Ignore("b/371527084: revisit testcase after refactoring original logic") - fun setAppHandleEducationTooltipCallbacks_onWindowingImageButtonTooltipClicked_callbackInvoked() = + fun clickExitDesktopModeHint_openHandleMenuCallbackInvoked() = testScope.runTest { - // After first tooltip is dismissed, app handle is expanded. Should show second - // education - // tooltip. - showAndDismissFirstTooltip() + // App handle is visible. Should show education tooltip. + setShouldShowDesktopModeEducation(true) val mockOpenHandleMenuCallback: (Int) -> Unit = mock() val mockToDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit = mock() educationController.setAppHandleEducationTooltipCallbacks( mockOpenHandleMenuCallback, mockToDesktopModeCallback, ) - // Simulate app handle expanded. - testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true) - // Wait for next tooltip to showup. + // Simulate app handle visible. + testCaptionStateFlow.value = createAppHeaderState() + // Wait for first tooltip to showup. waitForBufferDelay() verify(mockTooltipController, atLeastOnce()) .showEducationTooltip(educationConfigCaptor.capture(), any()) educationConfigCaptor.lastValue.onEducationClickAction.invoke() - verify(mockToDesktopModeCallback, times(1)).invoke(any(), any()) + verify(mockOpenHandleMenuCallback, times(1)).invoke(any()) } - private suspend fun TestScope.showAndDismissFirstTooltip() { - setShouldShowAppHandleEducation(true) - // Simulate app handle visible. - testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = false) - // Wait for first tooltip to showup. - waitForBufferDelay() - // [shouldShowAppHandleEducation] should return false as education has been viewed - // before. - setShouldShowAppHandleEducation(false) - // Dismiss previous tooltip, after this we should listen for next tooltip's trigger. - captureAndInvokeOnDismissAction() + private suspend fun setShouldShowDesktopModeEducation(shouldShowDesktopModeEducation: Boolean) { + whenever(mockEducationFilter.shouldShowDesktopModeEducation(any<CaptionState.AppHandle>())) + .thenReturn(shouldShowDesktopModeEducation) + whenever(mockEducationFilter.shouldShowDesktopModeEducation(any<CaptionState.AppHeader>())) + .thenReturn(shouldShowDesktopModeEducation) } - private fun TestScope.showAndDismissSecondTooltip() { - // Simulate app handle expanded. - testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true) - // Wait for next tooltip to showup. - waitForBufferDelay() - // Dismiss previous tooltip, after this we should listen for next tooltip's trigger. - captureAndInvokeOnDismissAction() - } - - private fun captureAndInvokeOnDismissAction() { - verify(mockTooltipController, atLeastOnce()) - .showEducationTooltip(educationConfigCaptor.capture(), any()) - educationConfigCaptor.lastValue.onDismissAction.invoke() - } - - private suspend fun setShouldShowAppHandleEducation(shouldShowAppHandleEducation: Boolean) = - whenever(mockEducationFilter.shouldShowAppHandleEducation(any())) - .thenReturn(shouldShowAppHandleEducation) - /** * Class under test waits for some time before showing education, simulate advance time before * verifying or moving forward @@ -510,7 +376,5 @@ class AppHandleEducationControllerTest : ShellTestCase() { private companion object { val APP_HANDLE_EDUCATION_DELAY_BUFFER_MILLIS: Long = APP_HANDLE_EDUCATION_DELAY_MILLIS + 1000L - val APP_HANDLE_EDUCATION_TIMEOUT_BUFFER_MILLIS: Long = - APP_HANDLE_EDUCATION_TIMEOUT_MILLIS + 1000L } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt index 4db883d13551..31dfc78902b2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt @@ -123,6 +123,24 @@ class AppHandleEducationDatastoreRepositoryTest { } @Test + fun updateEnterDesktopModeHintViewedTimestampMillis_updatesDatastoreProto() = + runTest(StandardTestDispatcher()) { + datastoreRepository.updateEnterDesktopModeHintViewedTimestampMillis(true) + + val result = testDatastore.data.first().hasEnterDesktopModeHintViewedTimestampMillis() + assertThat(result).isEqualTo(true) + } + + @Test + fun updateExitDesktopModeHintViewedTimestampMillis_updatesDatastoreProto() = + runTest(StandardTestDispatcher()) { + datastoreRepository.updateExitDesktopModeHintViewedTimestampMillis(true) + + val result = testDatastore.data.first().hasExitDesktopModeHintViewedTimestampMillis() + assertThat(result).isEqualTo(true) + } + + @Test fun updateAppHandleHintUsedTimestampMillis_updatesDatastoreProto() = runTest(StandardTestDispatcher()) { datastoreRepository.updateAppHandleHintUsedTimestampMillis(true) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilterTest.kt index 2fc36efb1a41..218226240c0f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilterTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilterTest.kt @@ -89,9 +89,9 @@ class AppHandleEducationFilterTest : ShellTestCase() { } @Test - fun shouldShowAppHandleEducation_isTriggerValid_returnsTrue() = runTest { - // setup() makes sure that all of the conditions satisfy and #shouldShowAppHandleEducation - // should return true + fun shouldShowDesktopModeEducation_isTriggerValid_returnsTrue() = runTest { + // setup() makes sure that all of the conditions satisfy and + // [shouldShowDesktopModeEducation] should return true val windowingEducationProto = createWindowingEducationProto( appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4), @@ -99,16 +99,15 @@ class AppHandleEducationFilterTest : ShellTestCase() { ) `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) - val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState()) + val result = educationFilter.shouldShowDesktopModeEducation(createAppHandleState()) assertThat(result).isTrue() } @Test - fun shouldShowAppHandleEducation_focusAppNotInAllowlist_returnsFalse() = runTest { + fun shouldShowDesktopModeEducation_focusAppNotInAllowlist_returnsFalse() = runTest { // Pass Youtube as current focus app, it is not in allowlist hence - // #shouldShowAppHandleEducation - // should return false + // [shouldShowDesktopModeEducation] should return false testableResources.addOverride( R.array.desktop_windowing_app_handle_education_allowlist_apps, arrayOf(GMAIL_PACKAGE_NAME), @@ -122,16 +121,15 @@ class AppHandleEducationFilterTest : ShellTestCase() { createAppHandleState(createTaskInfo(runningTaskPackageName = YOUTUBE_PACKAGE_NAME)) `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) - val result = educationFilter.shouldShowAppHandleEducation(captionState) + val result = educationFilter.shouldShowDesktopModeEducation(captionState) assertThat(result).isFalse() } @Test - fun shouldShowAppHandleEducation_timeSinceSetupIsNotSufficient_returnsFalse() = runTest { - // Time required to have passed setup is > 100 years, hence #shouldShowAppHandleEducation - // should - // return false + fun shouldShowDesktopModeEducation_timeSinceSetupIsNotSufficient_returnsFalse() = runTest { + // Time required to have passed setup is > 100 years, hence [shouldShowDesktopModeEducation] + // should return false testableResources.addOverride( R.integer.desktop_windowing_education_required_time_since_setup_seconds, MAX_VALUE, @@ -143,50 +141,15 @@ class AppHandleEducationFilterTest : ShellTestCase() { ) `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) - val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState()) - - assertThat(result).isFalse() - } - - @Test - fun shouldShowAppHandleEducation_appHandleHintViewedBefore_returnsFalse() = runTest { - // App handle hint has been viewed before, hence #shouldShowAppHandleEducation should return - // false - val windowingEducationProto = - createWindowingEducationProto( - appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4), - appHandleHintViewedTimestampMillis = 123L, - appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE, - ) - `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) - - val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState()) - - assertThat(result).isFalse() - } - - @Test - fun shouldShowAppHandleEducation_appHandleHintUsedBefore_returnsFalse() = runTest { - // App handle hint has been used before, hence #shouldShowAppHandleEducation should return - // false - val windowingEducationProto = - createWindowingEducationProto( - appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4), - appHandleHintUsedTimestampMillis = 123L, - appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE, - ) - `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) - - val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState()) + val result = educationFilter.shouldShowDesktopModeEducation(createAppHandleState()) assertThat(result).isFalse() } @Test - fun shouldShowAppHandleEducation_doesNotHaveMinAppUsage_returnsFalse() = runTest { + fun shouldShowDesktopModeEducation_doesNotHaveMinAppUsage_returnsFalse() = runTest { // Simulate that gmail app has been launched twice before, minimum app launch count is 3, - // hence - // #shouldShowAppHandleEducation should return false + // hence [shouldShowDesktopModeEducation] should return false testableResources.addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3) val windowingEducationProto = createWindowingEducationProto( @@ -195,13 +158,13 @@ class AppHandleEducationFilterTest : ShellTestCase() { ) `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) - val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState()) + val result = educationFilter.shouldShowDesktopModeEducation(createAppHandleState()) assertThat(result).isFalse() } @Test - fun shouldShowAppHandleEducation_appUsageStatsStale_queryAppUsageStats() = runTest { + fun shouldShowDesktopModeEducation_appUsageStatsStale_queryAppUsageStats() = runTest { // UsageStats caching interval is set to 0ms, that means caching should happen very // frequently testableResources.addOverride( @@ -209,8 +172,7 @@ class AppHandleEducationFilterTest : ShellTestCase() { 0, ) // The DataStore currently holds a proto object where Gmail's app launch count is recorded - // as 4. - // This value exceeds the minimum required count of 3. + // as 4. This value exceeds the minimum required count of 3. testableResources.addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3) val windowingEducationProto = createWindowingEducationProto( @@ -223,40 +185,20 @@ class AppHandleEducationFilterTest : ShellTestCase() { .thenReturn(mapOf(GMAIL_PACKAGE_NAME to UsageStats().apply { mAppLaunchCount = 2 })) `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) - val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState()) + val result = educationFilter.shouldShowDesktopModeEducation(createAppHandleState()) // Result should be false as queried usage stats should be considered to determine the - // result - // instead of cached stats - assertThat(result).isFalse() - } - - @Test - fun shouldShowAppHandleEducation_appHandleMenuExpanded_returnsFalse() = runTest { - val windowingEducationProto = - createWindowingEducationProto( - appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4), - appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE, - ) - // Simulate app handle menu is expanded - val captionState = createAppHandleState(isHandleMenuExpanded = true) - `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) - - val result = educationFilter.shouldShowAppHandleEducation(captionState) - - // We should not show app handle education if app menu is expanded + // result instead of cached stats assertThat(result).isFalse() } @Test - fun shouldShowAppHandleEducation_overridePrerequisite_returnsTrue() = runTest { + fun shouldShowDesktopModeEducation_overridePrerequisite_returnsTrue() = runTest { // Simulate that gmail app has been launched twice before, minimum app launch count is 3, - // hence - // #shouldShowAppHandleEducation should return false. But as we are overriding prerequisite - // conditions, #shouldShowAppHandleEducation should return true. + // hence [shouldShowDesktopModeEducation] should return false. But as we are overriding + // prerequisite conditions, [shouldShowDesktopModeEducation] should return true. testableResources.addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3) - val systemPropertiesKey = - "persist.desktop_windowing_app_handle_education_override_conditions" + val systemPropertiesKey = "persist.windowing_force_show_desktop_mode_education" whenever(SystemProperties.getBoolean(eq(systemPropertiesKey), anyBoolean())) .thenReturn(true) val windowingEducationProto = @@ -266,7 +208,7 @@ class AppHandleEducationFilterTest : ShellTestCase() { ) `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) - val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState()) + val result = educationFilter.shouldShowDesktopModeEducation(createAppHandleState()) assertThat(result).isTrue() } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt new file mode 100644 index 000000000000..a07203d86b75 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.desktopmode.multidesks + +import android.testing.AndroidTestingRunner +import android.view.Display +import android.view.SurfaceControl +import android.window.TransitionInfo +import android.window.WindowContainerTransaction +import android.window.WindowContainerTransaction.HierarchyOp +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestShellExecutor +import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask +import com.android.wm.shell.sysui.ShellCommandHandler +import com.android.wm.shell.sysui.ShellInit +import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +/** + * Tests for [RootTaskDesksOrganizer]. + * + * Usage: atest WMShellUnitTests:RootTaskDesksOrganizerTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class RootTaskDesksOrganizerTest : ShellTestCase() { + + private val testExecutor = TestShellExecutor() + private val testShellInit = ShellInit(testExecutor) + private val mockShellCommandHandler = mock<ShellCommandHandler>() + private val mockShellTaskOrganizer = mock<ShellTaskOrganizer>() + + private lateinit var organizer: RootTaskDesksOrganizer + + @Before + fun setUp() { + organizer = + RootTaskDesksOrganizer(testShellInit, mockShellCommandHandler, mockShellTaskOrganizer) + } + + @Test + fun testCreateDesk_callsBack() { + val callback = FakeOnCreateCallback() + organizer.createDesk(Display.DEFAULT_DISPLAY, callback) + + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + + assertThat(callback.created).isTrue() + assertEquals(freeformRoot.taskId, callback.deskId) + } + + @Test + fun testOnTaskAppeared_withoutRequest_throws() { + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + + assertThrows(Exception::class.java) { + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + } + } + + @Test + fun testOnTaskAppeared_withRequestOnlyInAnotherDisplay_throws() { + organizer.createDesk(displayId = 2, FakeOnCreateCallback()) + val freeformRoot = createFreeformTask(Display.DEFAULT_DISPLAY).apply { parentTaskId = -1 } + + assertThrows(Exception::class.java) { + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + } + } + + @Test + fun testOnTaskAppeared_duplicateRoot_throws() { + organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + + assertThrows(Exception::class.java) { + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + } + } + + @Test + fun testOnTaskVanished_removesRoot() { + val callback = FakeOnCreateCallback() + organizer.createDesk(Display.DEFAULT_DISPLAY, callback) + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + + organizer.onTaskVanished(freeformRoot) + + assertThat(organizer.roots.contains(freeformRoot.taskId)).isFalse() + } + + @Test + fun testDesktopWindowAppearsInDesk() { + organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + val child = createFreeformTask().apply { parentTaskId = freeformRoot.taskId } + + organizer.onTaskAppeared(child, SurfaceControl()) + + assertThat(organizer.roots[freeformRoot.taskId].children).contains(child.taskId) + } + + @Test + fun testDesktopWindowDisappearsFromDesk() { + organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + val child = createFreeformTask().apply { parentTaskId = freeformRoot.taskId } + + organizer.onTaskAppeared(child, SurfaceControl()) + organizer.onTaskVanished(child) + + assertThat(organizer.roots[freeformRoot.taskId].children).doesNotContain(child.taskId) + } + + @Test + fun testRemoveDesk() { + organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + + val wct = WindowContainerTransaction() + organizer.removeDesk(wct, freeformRoot.taskId) + + assertThat( + wct.hierarchyOps.any { hop -> + hop.type == HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_ROOT_TASK && + hop.container == freeformRoot.token.asBinder() + } + ) + .isTrue() + } + + @Test + fun testRemoveDesk_didNotExist_throws() { + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + + val wct = WindowContainerTransaction() + assertThrows(Exception::class.java) { organizer.removeDesk(wct, freeformRoot.taskId) } + } + + @Test + fun testActivateDesk() { + organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + + val wct = WindowContainerTransaction() + organizer.activateDesk(wct, freeformRoot.taskId) + + assertThat( + wct.hierarchyOps.any { hop -> + hop.type == HierarchyOp.HIERARCHY_OP_TYPE_REORDER && + hop.toTop && + hop.container == freeformRoot.token.asBinder() + } + ) + .isTrue() + assertThat( + wct.hierarchyOps.any { hop -> + hop.type == HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT && + hop.container == freeformRoot.token.asBinder() + } + ) + .isTrue() + } + + @Test + fun testActivateDesk_didNotExist_throws() { + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + + val wct = WindowContainerTransaction() + assertThrows(Exception::class.java) { organizer.activateDesk(wct, freeformRoot.taskId) } + } + + @Test + fun testMoveTaskToDesk() { + organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + + val desktopTask = createFreeformTask().apply { parentTaskId = -1 } + val wct = WindowContainerTransaction() + organizer.moveTaskToDesk(wct, freeformRoot.taskId, desktopTask) + + assertThat( + wct.hierarchyOps.any { hop -> + hop.isReparent && + hop.toTop && + hop.container == desktopTask.token.asBinder() && + hop.newParent == freeformRoot.token.asBinder() + } + ) + .isTrue() + } + + @Test + fun testMoveTaskToDesk_didNotExist_throws() { + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + + val desktopTask = createFreeformTask().apply { parentTaskId = -1 } + val wct = WindowContainerTransaction() + assertThrows(Exception::class.java) { + organizer.moveTaskToDesk(wct, freeformRoot.taskId, desktopTask) + } + } + + @Test + fun testGetDeskAtEnd() { + organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + + val task = createFreeformTask().apply { parentTaskId = freeformRoot.taskId } + val endDesk = + organizer.getDeskAtEnd( + TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task } + ) + + assertThat(endDesk).isEqualTo(freeformRoot.taskId) + } + + private class FakeOnCreateCallback : DesksOrganizer.OnCreateCallback { + var deskId: Int? = null + val created: Boolean + get() = deskId != null + + override fun onCreated(deskId: Int) { + this.deskId = deskId + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt index a3c441698905..9a8f264e98a4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt @@ -17,6 +17,7 @@ package com.android.wm.shell.desktopmode.persistence import android.os.UserManager +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner @@ -24,6 +25,7 @@ import android.view.Display.DEFAULT_DISPLAY import androidx.test.filters.SmallTest import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_HSUM import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE +import com.android.window.flags.Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND import com.android.wm.shell.ShellTestCase import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.desktopmode.DesktopUserRepositories @@ -85,7 +87,9 @@ class DesktopRepositoryInitializerTest : ShellTestCase() { @Test @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE, FLAG_ENABLE_DESKTOP_WINDOWING_HSUM) - fun initWithPersistence_multipleUsers_addedCorrectly() = + /** TODO: b/362720497 - add multi-desk version when implemented. */ + @DisableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun initWithPersistence_multipleUsers_addedCorrectly_multiDesksDisabled() = runTest(StandardTestDispatcher()) { whenever(persistentRepository.getUserDesktopRepositoryMap()) .thenReturn( @@ -145,7 +149,9 @@ class DesktopRepositoryInitializerTest : ShellTestCase() { @Test @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) - fun initWithPersistence_singleUser_addedCorrectly() = + /** TODO: b/362720497 - add multi-desk version when implemented. */ + @DisableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun initWithPersistence_singleUser_addedCorrectly_multiDesksDisabled() = runTest(StandardTestDispatcher()) { whenever(persistentRepository.getUserDesktopRepositoryMap()) .thenReturn(mapOf(USER_ID_1 to desktopRepositoryState1)) @@ -156,24 +162,24 @@ class DesktopRepositoryInitializerTest : ShellTestCase() { repositoryInitializer.initialize(desktopUserRepositories) - // Desktop Repository currently returns all tasks across desktops for a specific user - // since the repository currently doesn't handle desktops. This test logic should be - // updated - // once the repository handles multiple desktops. assertThat( - desktopUserRepositories.getProfile(USER_ID_1).getActiveTasks(DEFAULT_DISPLAY) + desktopUserRepositories + .getProfile(USER_ID_1) + .getActiveTaskIdsInDesk(deskId = DEFAULT_DISPLAY) ) .containsExactly(1, 3, 4, 5) .inOrder() assertThat( desktopUserRepositories .getProfile(USER_ID_1) - .getExpandedTasksOrdered(DEFAULT_DISPLAY) + .getExpandedTasksIdsInDeskOrdered(deskId = DEFAULT_DISPLAY) ) .containsExactly(5, 1) .inOrder() assertThat( - desktopUserRepositories.getProfile(USER_ID_1).getMinimizedTasks(DEFAULT_DISPLAY) + desktopUserRepositories + .getProfile(USER_ID_1) + .getMinimizedTaskIdsInDesk(deskId = DEFAULT_DISPLAY) ) .containsExactly(3, 4) .inOrder() @@ -195,6 +201,7 @@ class DesktopRepositoryInitializerTest : ShellTestCase() { val desktop1: Desktop = Desktop.newBuilder() .setDesktopId(DESKTOP_ID_1) + .setDisplayId(DEFAULT_DISPLAY) .addAllZOrderedTasks(freeformTasksInZOrder1) .putTasksByTaskId( 1, @@ -216,6 +223,7 @@ class DesktopRepositoryInitializerTest : ShellTestCase() { val desktop2: Desktop = Desktop.newBuilder() .setDesktopId(DESKTOP_ID_2) + .setDisplayId(DEFAULT_DISPLAY) .addAllZOrderedTasks(freeformTasksInZOrder2) .putTasksByTaskId( 4, @@ -237,6 +245,7 @@ class DesktopRepositoryInitializerTest : ShellTestCase() { val desktop3: Desktop = Desktop.newBuilder() .setDesktopId(DESKTOP_ID_3) + .setDisplayId(DEFAULT_DISPLAY) .addAllZOrderedTasks(freeformTasksInZOrder3) .putTasksByTaskId( 7, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java index 6c16b3220a07..4174bbd89b76 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java @@ -46,6 +46,7 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; import com.android.wm.shell.common.LaunchAdjacentController; +import com.android.wm.shell.desktopmode.DesktopModeLoggerTransitionObserver; import com.android.wm.shell.desktopmode.DesktopRepository; import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.desktopmode.DesktopUserRepositories; @@ -89,6 +90,8 @@ public final class FreeformTaskListenerTests extends ShellTestCase { @Mock private DesktopTasksController mDesktopTasksController; @Mock + private DesktopModeLoggerTransitionObserver mDesktopModeLoggerTransitionObserver; + @Mock private LaunchAdjacentController mLaunchAdjacentController; @Mock private TaskChangeListener mTaskChangeListener; @@ -114,6 +117,7 @@ public final class FreeformTaskListenerTests extends ShellTestCase { mTaskOrganizer, Optional.of(mDesktopUserRepositories), Optional.of(mDesktopTasksController), + mDesktopModeLoggerTransitionObserver, mLaunchAdjacentController, mWindowDecorViewModel, Optional.of(mTaskChangeListener)); @@ -283,6 +287,19 @@ public final class FreeformTaskListenerTests extends ShellTestCase { } @Test + public void onTaskVanished_withDesktopModeLogger_forwards() { + ActivityManager.RunningTaskInfo task = + new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(); + task.isVisible = true; + mFreeformTaskListener.onTaskAppeared(task, mMockSurfaceControl); + + mFreeformTaskListener.onTaskVanished(task); + + verify(mDesktopModeLoggerTransitionObserver).onTaskVanished(task); + } + + + @Test public void onTaskInfoChanged_withDesktopController_forwards() { ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java index 065fa219e8d0..542289db6cfc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java @@ -211,6 +211,7 @@ public class RecentTasksControllerTest extends ShellTestCase { @Test public void testAddRemoveSplitNotifyChange() { + reset(mRecentTasksController); RecentTaskInfo t1 = makeTaskInfo(1); RecentTaskInfo t2 = makeTaskInfo(2); setRawList(t1, t2); @@ -225,6 +226,7 @@ public class RecentTasksControllerTest extends ShellTestCase { @Test public void testAddSameSplitBoundsInfoSkipNotifyChange() { + reset(mRecentTasksController); RecentTaskInfo t1 = makeTaskInfo(1); RecentTaskInfo t2 = makeTaskInfo(2); setRawList(t1, t2); @@ -535,6 +537,7 @@ public class RecentTasksControllerTest extends ShellTestCase { @Test public void testTaskWindowingModeChangedNotifiesChange() { + reset(mRecentTasksController); RecentTaskInfo t1 = makeTaskInfo(1); setRawList(t1); @@ -551,7 +554,8 @@ public class RecentTasksControllerTest extends ShellTestCase { WINDOWING_MODE_MULTI_WINDOW); mShellTaskOrganizer.onTaskInfoChanged(rt2MultiWIndow); - verify(mRecentTasksController).notifyRecentTasksChanged(); + // One for onTaskAppeared and one for onTaskInfoChanged + verify(mRecentTasksController, times(2)).notifyRecentTasksChanged(); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/AppCompatUtilsTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt index 7157a7f0b38f..8c78debdc19f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/AppCompatUtilsTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt @@ -14,29 +14,38 @@ * limitations under the License. */ -package com.android.wm.shell.compatui +package com.android.wm.shell.shared.desktopmode import android.content.ComponentName import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.internal.R +import com.android.wm.shell.compatui.CompatUIShellTestCase import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith /** - * Tests for [@link AppCompatUtils]. + * Tests for [@link DesktopModeCompatPolicy]. * - * Build/Install/Run: atest WMShellUnitTests:AppCompatUtilsTest + * Build/Install/Run: atest WMShellUnitTests:DesktopModeCompatPolicyTest */ @RunWith(AndroidTestingRunner::class) @SmallTest -class AppCompatUtilsTest : CompatUIShellTestCase() { +class DesktopModeCompatPolicyTest : CompatUIShellTestCase() { + private lateinit var desktopModeCompatPolicy: DesktopModeCompatPolicy + + @Before + fun setUp() { + desktopModeCompatPolicy = DesktopModeCompatPolicy(mContext) + } + @Test fun testIsTopActivityExemptFromDesktopWindowing_onlyTransparentActivitiesInStack() { - assertTrue(isTopActivityExemptFromDesktopWindowing(mContext, + assertTrue(desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing( createFreeformTask(/* displayId */ 0) .apply { isActivityStackTransparent = true @@ -47,7 +56,7 @@ class AppCompatUtilsTest : CompatUIShellTestCase() { @Test fun testIsTopActivityExemptFromDesktopWindowing_noActivitiesInStack() { - assertFalse(isTopActivityExemptFromDesktopWindowing(mContext, + assertFalse(desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing( createFreeformTask(/* displayId */ 0) .apply { isActivityStackTransparent = true @@ -58,7 +67,7 @@ class AppCompatUtilsTest : CompatUIShellTestCase() { @Test fun testIsTopActivityExemptFromDesktopWindowing_nonTransparentActivitiesInStack() { - assertFalse(isTopActivityExemptFromDesktopWindowing(mContext, + assertFalse(desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing( createFreeformTask(/* displayId */ 0) .apply { isActivityStackTransparent = false @@ -69,7 +78,7 @@ class AppCompatUtilsTest : CompatUIShellTestCase() { @Test fun testIsTopActivityExemptFromDesktopWindowing_transparentActivityStack_notDisplayed() { - assertFalse(isTopActivityExemptFromDesktopWindowing(mContext, + assertFalse(desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing( createFreeformTask(/* displayId */ 0) .apply { isActivityStackTransparent = true @@ -82,7 +91,7 @@ class AppCompatUtilsTest : CompatUIShellTestCase() { fun testIsTopActivityExemptFromDesktopWindowing_systemUiTask() { val systemUIPackageName = context.resources.getString(R.string.config_systemUi) val baseComponent = ComponentName(systemUIPackageName, /* class */ "") - assertTrue(isTopActivityExemptFromDesktopWindowing(mContext, + assertTrue(desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing( createFreeformTask(/* displayId */ 0) .apply { baseActivity = baseComponent @@ -94,7 +103,7 @@ class AppCompatUtilsTest : CompatUIShellTestCase() { fun testIsTopActivityExemptFromDesktopWindowing_systemUiTask_notDisplayed() { val systemUIPackageName = context.resources.getString(R.string.config_systemUi) val baseComponent = ComponentName(systemUIPackageName, /* class */ "") - assertFalse(isTopActivityExemptFromDesktopWindowing(mContext, + assertFalse(desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing( createFreeformTask(/* displayId */ 0) .apply { baseActivity = baseComponent diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java index bb9703fce2e3..7f6b06d46abf 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java @@ -213,7 +213,7 @@ public class SplitScreenControllerTests extends ShellTestCase { @Test public void startIntent_multiInstancesSupported_appendsMultipleTaskFag() { - doReturn(true).when(mMultiInstanceHelper).supportsMultiInstanceSplit(any()); + doReturn(true).when(mMultiInstanceHelper).supportsMultiInstanceSplit(any(), anyInt()); Intent startIntent = createStartIntent("startActivity"); PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE); @@ -252,13 +252,13 @@ public class SplitScreenControllerTests extends ShellTestCase { verify(mStageCoordinator).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT), isNull(), isNull(), eq(SPLIT_INDEX_0)); - verify(mMultiInstanceHelper, never()).supportsMultiInstanceSplit(any()); + verify(mMultiInstanceHelper, never()).supportsMultiInstanceSplit(any(), anyInt()); verify(mStageCoordinator, never()).switchSplitPosition(any()); } @Test public void startIntent_multiInstancesSupported_startTaskInBackgroundAfterSplitActivated() { - doReturn(true).when(mMultiInstanceHelper).supportsMultiInstanceSplit(any()); + doReturn(true).when(mMultiInstanceHelper).supportsMultiInstanceSplit(any(), anyInt()); doNothing().when(mSplitScreenController).startTask(anyInt(), anyInt(), any(), any()); Intent startIntent = createStartIntent("startActivity"); PendingIntent pendingIntent = @@ -276,14 +276,14 @@ public class SplitScreenControllerTests extends ShellTestCase { mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null, SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */, SPLIT_INDEX_0); - verify(mMultiInstanceHelper, never()).supportsMultiInstanceSplit(any()); + verify(mMultiInstanceHelper, never()).supportsMultiInstanceSplit(any(), anyInt()); verify(mStageCoordinator).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT), isNull(), isNull(), eq(SPLIT_INDEX_0)); } @Test public void startIntent_multiInstancesNotSupported_switchesPositionAfterSplitActivated() { - doReturn(false).when(mMultiInstanceHelper).supportsMultiInstanceSplit(any()); + doReturn(false).when(mMultiInstanceHelper).supportsMultiInstanceSplit(any(), anyInt()); Intent startIntent = createStartIntent("startActivity"); PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java index 4211e4682810..b9d6a454694d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java @@ -67,6 +67,7 @@ import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.launcher3.icons.IconProvider; +import com.android.wm.shell.Flags; import com.android.wm.shell.MockToken; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; @@ -355,8 +356,13 @@ public class SplitTransitionTests extends ShellTestCase { // Make sure it cleans-up if recents doesn't restore WindowContainerTransaction commitWCT = new WindowContainerTransaction(); - mStageCoordinator.onRecentsInSplitAnimationFinish(commitWCT, - mock(SurfaceControl.Transaction.class)); + if (Flags.enableRecentsBookendTransition()) { + mStageCoordinator.onRecentsInSplitAnimationFinishing(false /* returnToApp */, commitWCT, + mock(SurfaceControl.Transaction.class)); + } else { + mStageCoordinator.onRecentsInSplitAnimationFinish(commitWCT, + mock(SurfaceControl.Transaction.class)); + } assertFalse(mStageCoordinator.isSplitScreenVisible()); } @@ -420,8 +426,13 @@ public class SplitTransitionTests extends ShellTestCase { // simulate the restoreWCT being applied: mMainStage.onTaskAppeared(mMainChild, mock(SurfaceControl.class)); mSideStage.onTaskAppeared(mSideChild, mock(SurfaceControl.class)); - mStageCoordinator.onRecentsInSplitAnimationFinish(restoreWCT, - mock(SurfaceControl.Transaction.class)); + if (Flags.enableRecentsBookendTransition()) { + mStageCoordinator.onRecentsInSplitAnimationFinishing(true /* returnToApp */, restoreWCT, + mock(SurfaceControl.Transaction.class)); + } else { + mStageCoordinator.onRecentsInSplitAnimationFinish(restoreWCT, + mock(SurfaceControl.Transaction.class)); + } assertTrue(mStageCoordinator.isSplitScreenVisible()); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/WindowingEducationTestUtils.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/WindowingEducationTestUtils.kt index b9d91e7895db..546848421302 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/WindowingEducationTestUtils.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/WindowingEducationTestUtils.kt @@ -81,7 +81,9 @@ fun createWindowingEducationProto( appHandleHintViewedTimestampMillis: Long? = null, appHandleHintUsedTimestampMillis: Long? = null, appUsageStats: Map<String, Int>? = null, - appUsageStatsLastUpdateTimestampMillis: Long? = null + appUsageStatsLastUpdateTimestampMillis: Long? = null, + enterDesktopModeHintViewedTimestampMillis: Long? = null, + exitDesktopModeHintViewedTimestampMillis: Long? = null, ): WindowingEducationProto = WindowingEducationProto.newBuilder() .apply { @@ -91,6 +93,12 @@ fun createWindowingEducationProto( if (appHandleHintUsedTimestampMillis != null) { setAppHandleHintUsedTimestampMillis(appHandleHintUsedTimestampMillis) } + if (enterDesktopModeHintViewedTimestampMillis != null) { + setEnterDesktopModeHintViewedTimestampMillis(enterDesktopModeHintViewedTimestampMillis) + } + if (exitDesktopModeHintViewedTimestampMillis != null) { + setExitDesktopModeHintViewedTimestampMillis(exitDesktopModeHintViewedTimestampMillis) + } setAppHandleEducation( createAppHandleEducationProto(appUsageStats, appUsageStatsLastUpdateTimestampMillis)) } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt index 908bc9952e99..c5c827467c75 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt @@ -69,6 +69,7 @@ import com.android.wm.shell.desktopmode.education.AppToWebEducationController import com.android.wm.shell.freeform.FreeformTaskTransitionStarter import com.android.wm.shell.recents.RecentsTransitionHandler import com.android.wm.shell.recents.RecentsTransitionStateListener +import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellController @@ -161,6 +162,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { val display = mock<Display>() protected lateinit var spyContext: TestableContext private lateinit var desktopModeEventLogger: DesktopModeEventLogger + private lateinit var desktopModeCompatPolicy: DesktopModeCompatPolicy private val transactionFactory = Supplier<SurfaceControl.Transaction> { SurfaceControl.Transaction() @@ -188,6 +190,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { whenever(mockDisplayController.getDisplay(any())).thenReturn(display) whenever(mockDesktopUserRepositories.getProfile(anyInt())) .thenReturn(mockDesktopRepository) + desktopModeCompatPolicy = DesktopModeCompatPolicy(context) desktopModeWindowDecorViewModel = DesktopModeWindowDecorViewModel( spyContext, testShellExecutor, @@ -230,6 +233,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { mock<DesktopModeUiEventLogger>(), mock<WindowDecorTaskResourceLoader>(), mockRecentsTransitionHandler, + desktopModeCompatPolicy, ) desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController) whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index 9ea5fd6e1abe..87198d14c839 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -280,7 +280,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { mTestableContext = new TestableContext(mContext); mTestableContext.ensureTestableResources(); mContext.setMockPackageManager(mMockPackageManager); - when(mMockMultiInstanceHelper.supportsMultiInstanceSplit(any())) + when(mMockMultiInstanceHelper.supportsMultiInstanceSplit(any(), anyInt())) .thenReturn(false); when(mMockPackageManager.getApplicationLabel(any())).thenReturn("applicationLabel"); final ActivityInfo activityInfo = createActivityInfo(); @@ -295,7 +295,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), any(), anyInt(), anyInt(), anyInt(), anyInt())) .thenReturn(mMockHandleMenu); - when(mMockMultiInstanceHelper.supportsMultiInstanceSplit(any())).thenReturn(false); + when(mMockMultiInstanceHelper.supportsMultiInstanceSplit(any(), anyInt())) + .thenReturn(false); when(mMockAppHeaderViewHolderFactory.create(any(), any(), any(), any(), any(), any())) .thenReturn(mMockAppHeaderViewHolder); when(mMockDesktopUserRepositories.getCurrent()).thenReturn(mDesktopRepository); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt index 2207c705d7dc..0615c1d677ba 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt @@ -51,6 +51,7 @@ import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFI import java.util.function.Supplier import junit.framework.Assert import org.junit.Before +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock @@ -206,6 +207,7 @@ class MultiDisplayVeiledResizeTaskPositionerTest : ShellTestCase() { } @Test + @Ignore("Causing presubmit failure b/391717499") fun testDragResize_movesTask_doesNotShowResizeVeil() = runOnUiThread { taskPositioner.onDragPositioningStart( CTRL_TYPE_UNDEFINED, @@ -245,6 +247,7 @@ class MultiDisplayVeiledResizeTaskPositionerTest : ShellTestCase() { } @Test + @Ignore("Causing presubmit failure b/391717499") fun testDragResize_movesTaskToNewDisplay() = runOnUiThread { taskPositioner.onDragPositioningStart( CTRL_TYPE_UNDEFINED, @@ -370,6 +373,7 @@ class MultiDisplayVeiledResizeTaskPositionerTest : ShellTestCase() { } @Test + @Ignore("Causing presubmit failure b/391717499") fun testDragResize_drag_setBoundsNotRunIfDragEndsInDisallowedEndArea() = runOnUiThread { taskPositioner.onDragPositioningStart( CTRL_TYPE_UNDEFINED, // drag diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt index 1ec0fe794d0a..431de896f433 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt @@ -33,6 +33,7 @@ import com.android.launcher3.icons.IconProvider import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestRunningTaskInfoBuilder import com.android.wm.shell.TestShellExecutor +import com.android.wm.shell.common.UserProfileContexts import com.android.wm.shell.sysui.ShellController import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.sysui.UserChangeListener @@ -69,6 +70,7 @@ class WindowDecorTaskResourceLoaderTest : ShellTestCase() { private val mockIconProvider = mock<IconProvider>() private val mockHeaderIconFactory = mock<BaseIconFactory>() private val mockVeilIconFactory = mock<BaseIconFactory>() + private val mMockUserProfileContexts = mock<UserProfileContexts>() private lateinit var spyContext: TestableContext private lateinit var loader: WindowDecorTaskResourceLoader @@ -83,12 +85,13 @@ class WindowDecorTaskResourceLoaderTest : ShellTestCase() { spyContext = spy(mContext) spyContext.setMockPackageManager(mockPackageManager) doReturn(spyContext).whenever(spyContext).createContextAsUser(any(), anyInt()) + doReturn(spyContext).whenever(mMockUserProfileContexts)[anyInt()] loader = WindowDecorTaskResourceLoader( - context = spyContext, shellInit = shellInit, shellController = mockShellController, shellCommandHandler = mock(), + userProfilesContexts = mMockUserProfileContexts, iconProvider = mockIconProvider, headerIconFactory = mockHeaderIconFactory, veilIconFactory = mockVeilIconFactory, @@ -170,16 +173,6 @@ class WindowDecorTaskResourceLoaderTest : ShellTestCase() { } @Test - fun testUserChange_updatesContext() { - val newUser = 5000 - val newContext = mock<Context>() - - userChangeListener.onUserChanged(newUser, newContext) - - assertThat(loader.currentUserContext).isEqualTo(newContext) - } - - @Test fun testUserChange_clearsCache() { val newUser = 5000 val newContext = mock<Context>() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt index 997ece6ecadc..2cabb9a33b86 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt @@ -23,6 +23,7 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.desktopmode.DesktopModeEventLogger import com.android.wm.shell.desktopmode.DesktopUserRepositories @@ -30,6 +31,7 @@ import com.android.wm.shell.desktopmode.DesktopTasksController import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler +import com.android.wm.shell.transition.FocusTransitionObserver import com.android.wm.shell.transition.Transitions import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader @@ -67,6 +69,8 @@ class DesktopTilingDecorViewModelTest : ShellTestCase() { private val desktopModeWindowDecorationMock: DesktopModeWindowDecoration = mock() private val desktopTilingDecoration: DesktopTilingWindowDecoration = mock() private val taskResourceLoader: WindowDecorTaskResourceLoader = mock() + private val focusTransitionObserver: FocusTransitionObserver = mock() + private val mainExecutor: ShellExecutor = mock() private lateinit var desktopTilingDecorViewModel: DesktopTilingDecorViewModel @Before @@ -86,6 +90,8 @@ class DesktopTilingDecorViewModelTest : ShellTestCase() { userRepositories, desktopModeEventLogger, taskResourceLoader, + focusTransitionObserver, + mainExecutor ) whenever(contextMock.createContextAsUser(any(), any())).thenReturn(contextMock) } @@ -140,7 +146,7 @@ class DesktopTilingDecorViewModelTest : ShellTestCase() { desktopTilingDecorViewModel.moveTaskToFrontIfTiled(task1) verify(desktopTilingDecoration, times(1)) - .moveTiledPairToFront(any(), isTaskFocused = eq(true)) + .moveTiledPairToFront(any(), isFocusedOnDisplay = eq(true)) } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt index 2f15c2e38855..399a51e1ed08 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt @@ -33,6 +33,7 @@ import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout +import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.desktopmode.DesktopModeEventLogger import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger @@ -42,6 +43,7 @@ import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask import com.android.wm.shell.desktopmode.DesktopUserRepositories import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler +import com.android.wm.shell.transition.FocusTransitionObserver import com.android.wm.shell.transition.Transitions import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration import com.android.wm.shell.windowdecor.DragResizeWindowGeometry @@ -105,6 +107,8 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() { private val mainDispatcher: MainCoroutineDispatcher = mock() private val bgScope: CoroutineScope = mock() private val taskResourceLoader: WindowDecorTaskResourceLoader = mock() + private val focusTransitionObserver: FocusTransitionObserver = mock() + private val mainExecutor: ShellExecutor = mock() private lateinit var tilingDecoration: DesktopTilingWindowDecoration private val split_divider_width = 10 @@ -129,6 +133,8 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() { returnToDragStartAnimator, userRepositories, desktopModeEventLogger, + focusTransitionObserver, + mainExecutor ) whenever(context.createContextAsUser(any(), any())).thenReturn(context) whenever(userRepositories.current).thenReturn(desktopRepository) @@ -242,7 +248,7 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() { BOUNDS, ) - assertThat(tilingDecoration.moveTiledPairToFront(task2)).isFalse() + assertThat(tilingDecoration.moveTiledPairToFront(task2.taskId, false)).isFalse() verify(transitions, never()).startTransition(any(), any(), any()) } @@ -272,7 +278,7 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() { BOUNDS, ) - assertThat(tilingDecoration.moveTiledPairToFront(task3)).isFalse() + assertThat(tilingDecoration.moveTiledPairToFront(task3.taskId, false)).isFalse() verify(transitions, never()).startTransition(any(), any(), any()) } @@ -304,7 +310,7 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() { ) task1.isFocused = true - assertThat(tilingDecoration.moveTiledPairToFront(task1, isTaskFocused = true)).isTrue() + assertThat(tilingDecoration.moveTiledPairToFront(task1.taskId, isFocusedOnDisplay = true)).isTrue() verify(transitions, times(1)).startTransition(eq(TRANSIT_TO_FRONT), any(), eq(null)) } @@ -336,8 +342,8 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() { task1.isFocused = true task3.isFocused = true - assertThat(tilingDecoration.moveTiledPairToFront(task3)).isFalse() - assertThat(tilingDecoration.moveTiledPairToFront(task1)).isTrue() + assertThat(tilingDecoration.moveTiledPairToFront(task3.taskId, true)).isFalse() + assertThat(tilingDecoration.moveTiledPairToFront(task1.taskId, true)).isTrue() verify(transitions, times(1)).startTransition(eq(TRANSIT_TO_FRONT), any(), eq(null)) } @@ -367,8 +373,8 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() { BOUNDS, ) - assertThat(tilingDecoration.moveTiledPairToFront(task3, isTaskFocused = true)).isFalse() - assertThat(tilingDecoration.moveTiledPairToFront(task1, isTaskFocused = true)).isTrue() + assertThat(tilingDecoration.moveTiledPairToFront(task3.taskId, isFocusedOnDisplay = true)).isFalse() + assertThat(tilingDecoration.moveTiledPairToFront(task1.taskId, isFocusedOnDisplay = true)).isTrue() verify(transitions, times(1)).startTransition(eq(TRANSIT_TO_FRONT), any(), eq(null)) } diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index 677fd86aca9c..53d3b77f1ba2 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -206,6 +206,9 @@ java_sdk_library { visibility: [ "//frameworks/base", // Framework ], + impl_library_visibility: [ + "//frameworks/base/ravenwood", + ], srcs: [ ":framework-graphics-srcs", diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 52eae43f7db9..71013f7f4e34 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -21,6 +21,8 @@ import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO; import static android.content.Context.DEVICE_ID_DEFAULT; import static android.media.audio.Flags.autoPublicVolumeApiHardening; import static android.media.audio.Flags.automaticBtDeviceType; +import static android.media.audio.Flags.cacheGetStreamMinMaxVolume; +import static android.media.audio.Flags.cacheGetStreamVolume; import static android.media.audio.Flags.FLAG_DEPRECATE_STREAM_BT_SCO; import static android.media.audio.Flags.FLAG_FOCUS_EXCLUSIVE_WITH_RECORDING; import static android.media.audio.Flags.FLAG_FOCUS_FREEZE_TEST_API; @@ -58,7 +60,6 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.media.AudioAttributes.AttributeSystemUsage; -import android.media.AudioDeviceInfo; import android.media.CallbackUtil.ListenerInfo; import android.media.audiopolicy.AudioPolicy; import android.media.audiopolicy.AudioPolicy.AudioPolicyFocusListener; @@ -75,6 +76,7 @@ import android.os.Binder; import android.os.Build; import android.os.Handler; import android.os.IBinder; +import android.os.IpcDataCache; import android.os.Looper; import android.os.Message; import android.os.RemoteException; @@ -1231,6 +1233,102 @@ public class AudioManager { } /** + * API string for caching the min volume for each stream + * @hide + **/ + public static final String VOLUME_MIN_CACHING_API = "getStreamMinVolume"; + /** + * API string for caching the max volume for each stream + * @hide + **/ + public static final String VOLUME_MAX_CACHING_API = "getStreamMaxVolume"; + /** + * API string for caching the volume for each stream + * @hide + **/ + public static final String VOLUME_CACHING_API = "getStreamVolume"; + private static final int VOLUME_CACHING_SIZE = 16; + + private final IpcDataCache.QueryHandler<VolumeCacheQuery, Integer> mVolQuery = + new IpcDataCache.QueryHandler<>() { + @Override + public Integer apply(VolumeCacheQuery query) { + final IAudioService service = getService(); + try { + return switch (query.queryCommand) { + case QUERY_VOL_MIN -> service.getStreamMinVolume(query.stream); + case QUERY_VOL_MAX -> service.getStreamMaxVolume(query.stream); + case QUERY_VOL -> service.getStreamVolume(query.stream); + default -> { + Log.w(TAG, "Not a valid volume cache query: " + query); + yield null; + } + }; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + }; + + private final IpcDataCache<VolumeCacheQuery, Integer> mVolMinCache = + new IpcDataCache<>(VOLUME_CACHING_SIZE, IpcDataCache.MODULE_SYSTEM, + VOLUME_MIN_CACHING_API, VOLUME_MIN_CACHING_API, mVolQuery); + + private final IpcDataCache<VolumeCacheQuery, Integer> mVolMaxCache = + new IpcDataCache<>(VOLUME_CACHING_SIZE, IpcDataCache.MODULE_SYSTEM, + VOLUME_MAX_CACHING_API, VOLUME_MAX_CACHING_API, mVolQuery); + + private final IpcDataCache<VolumeCacheQuery, Integer> mVolCache = + new IpcDataCache<>(VOLUME_CACHING_SIZE, IpcDataCache.MODULE_SYSTEM, + VOLUME_CACHING_API, VOLUME_CACHING_API, mVolQuery); + + /** + * Used to invalidate the cache for the given API + * @hide + **/ + public static void clearVolumeCache(String api) { + if (cacheGetStreamMinMaxVolume() && (VOLUME_MAX_CACHING_API.equals(api) + || VOLUME_MIN_CACHING_API.equals(api))) { + IpcDataCache.invalidateCache(IpcDataCache.MODULE_SYSTEM, api); + } else if (cacheGetStreamVolume() && VOLUME_CACHING_API.equals(api)) { + IpcDataCache.invalidateCache(IpcDataCache.MODULE_SYSTEM, api); + } else { + Log.w(TAG, "invalid clearVolumeCache for api " + api); + } + } + + private static final int QUERY_VOL_MIN = 1; + private static final int QUERY_VOL_MAX = 2; + private static final int QUERY_VOL = 3; + /** @hide */ + @IntDef(prefix = "QUERY_VOL", value = { + QUERY_VOL_MIN, + QUERY_VOL_MAX, + QUERY_VOL} + ) + @Retention(RetentionPolicy.SOURCE) + private @interface QueryVolCommand {} + + private record VolumeCacheQuery(int stream, @QueryVolCommand int queryCommand) { + private String queryVolCommandToString() { + return switch (queryCommand) { + case QUERY_VOL_MIN -> "getStreamMinVolume"; + case QUERY_VOL_MAX -> "getStreamMaxVolume"; + case QUERY_VOL -> "getStreamVolume"; + default -> "invalid command"; + }; + } + + @NonNull + @Override + public String toString() { + return TextUtils.formatSimple("VolumeCacheQuery(stream=%d, queryCommand=%s)", stream, + queryVolCommandToString()); + } + } + + /** * Returns the maximum volume index for a particular stream. * * @param streamType The stream type whose maximum volume index is returned. @@ -1238,6 +1336,9 @@ public class AudioManager { * @see #getStreamVolume(int) */ public int getStreamMaxVolume(int streamType) { + if (cacheGetStreamMinMaxVolume()) { + return mVolMaxCache.query(new VolumeCacheQuery(streamType, QUERY_VOL_MAX)); + } final IAudioService service = getService(); try { return service.getStreamMaxVolume(streamType); @@ -1271,6 +1372,9 @@ public class AudioManager { */ @TestApi public int getStreamMinVolumeInt(int streamType) { + if (cacheGetStreamMinMaxVolume()) { + return mVolMinCache.query(new VolumeCacheQuery(streamType, QUERY_VOL_MIN)); + } final IAudioService service = getService(); try { return service.getStreamMinVolume(streamType); @@ -1288,6 +1392,9 @@ public class AudioManager { * @see #setStreamVolume(int, int, int) */ public int getStreamVolume(int streamType) { + if (cacheGetStreamVolume()) { + return mVolCache.query(new VolumeCacheQuery(streamType, QUERY_VOL)); + } final IAudioService service = getService(); try { return service.getStreamVolume(streamType); diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java index c9625c405faa..c4886836f451 100644 --- a/media/java/android/media/MediaCodec.java +++ b/media/java/android/media/MediaCodec.java @@ -20,6 +20,8 @@ import static android.media.codec.Flags.FLAG_CODEC_AVAILABILITY; import static android.media.codec.Flags.FLAG_NULL_OUTPUT_SURFACE; import static android.media.codec.Flags.FLAG_REGION_OF_INTEREST; import static android.media.codec.Flags.FLAG_SUBSESSION_METRICS; +import static android.media.tv.flags.Flags.applyPictureProfiles; +import static android.media.tv.flags.Flags.mediaQualityFw; import static com.android.media.codec.flags.Flags.FLAG_LARGE_AUDIO_FRAME; @@ -37,6 +39,8 @@ import android.graphics.Rect; import android.graphics.SurfaceTexture; import android.hardware.HardwareBuffer; import android.media.MediaCodecInfo.CodecCapabilities; +import android.media.quality.PictureProfile; +import android.media.quality.PictureProfileHandle; import android.os.Build; import android.os.Bundle; import android.os.Handler; @@ -5370,6 +5374,9 @@ final public class MediaCodec { * @param params The bundle of parameters to set. * @throws IllegalStateException if in the Released state. */ + + private static final String PARAMETER_KEY_PICTURE_PROFILE_HANDLE = "picture-profile-handle"; + public final void setParameters(@Nullable Bundle params) { if (params == null) { return; @@ -5383,19 +5390,41 @@ final public class MediaCodec { if (key.equals(MediaFormat.KEY_AUDIO_SESSION_ID)) { int sessionId = 0; try { - sessionId = (Integer)params.get(key); + sessionId = (Integer) params.get(key); } catch (Exception e) { throw new IllegalArgumentException("Wrong Session ID Parameter!"); } keys[i] = "audio-hw-sync"; values[i] = AudioSystem.getAudioHwSyncForSession(sessionId); + } else if (applyPictureProfiles() && mediaQualityFw() + && key.equals(MediaFormat.KEY_PICTURE_PROFILE_INSTANCE)) { + PictureProfile pictureProfile = null; + try { + pictureProfile = (PictureProfile) params.get(key); + } catch (ClassCastException e) { + throw new IllegalArgumentException( + "Cannot cast the instance parameter to PictureProfile!"); + } catch (Exception e) { + android.util.Log.getStackTraceString(e); + throw new IllegalArgumentException("Unexpected exception when casting the " + + "instance parameter to PictureProfile!"); + } + if (pictureProfile == null) { + throw new IllegalArgumentException( + "Picture profile instance parameter is null!"); + } + PictureProfileHandle handle = pictureProfile.getHandle(); + if (handle != PictureProfileHandle.NONE) { + keys[i] = PARAMETER_KEY_PICTURE_PROFILE_HANDLE; + values[i] = Long.valueOf(handle.getId()); + } } else { keys[i] = key; Object value = params.get(key); // Bundle's byte array is a byte[], JNI layer only takes ByteBuffer if (value instanceof byte[]) { - values[i] = ByteBuffer.wrap((byte[])value); + values[i] = ByteBuffer.wrap((byte[]) value); } else { values[i] = value; } diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index e57148fe5a6a..3af36a404c30 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -281,7 +281,7 @@ public final class MediaRouter2 { /* executor */ null, /* onInstanceInvalidatedListener */ null); } catch (IllegalArgumentException ex) { - Log.e(TAG, "Package " + clientPackageName + " not found. Ignoring."); + Log.e(TAG, "Failed to create proxy router for package '" + clientPackageName + "'", ex); return null; } } diff --git a/media/java/android/media/flags/projection.aconfig b/media/java/android/media/flags/projection.aconfig index fa1349c61c4c..6d4f0b4f47d5 100644 --- a/media/java/android/media/flags/projection.aconfig +++ b/media/java/android/media/flags/projection.aconfig @@ -29,3 +29,13 @@ flag { is_exported: true } +flag { + namespace: "media_projection" + name: "show_stop_dialog_post_call_end" + description: "Shows a stop dialog for MediaProjection sessions that started during call and remain active after a call ends" + bug: "390343524" + metadata { + purpose: PURPOSE_BUGFIX + } + is_exported: true +} diff --git a/media/java/android/media/projection/IMediaProjectionWatcherCallback.aidl b/media/java/android/media/projection/IMediaProjectionWatcherCallback.aidl index e46d34e81483..3baf4d7efd65 100644 --- a/media/java/android/media/projection/IMediaProjectionWatcherCallback.aidl +++ b/media/java/android/media/projection/IMediaProjectionWatcherCallback.aidl @@ -18,6 +18,7 @@ package android.media.projection; import android.media.projection.MediaProjectionInfo; import android.view.ContentRecordingSession; +import android.media.projection.MediaProjectionEvent; /** {@hide} */ oneway interface IMediaProjectionWatcherCallback { @@ -35,4 +36,19 @@ oneway interface IMediaProjectionWatcherCallback { in MediaProjectionInfo info, in @nullable ContentRecordingSession session ); + + /** + * Called when a specific {@link MediaProjectionEvent} occurs during the media projection session. + * + * @param event contains the event type, which describes the nature/context of the event. + * @param info optional {@link MediaProjectionInfo} containing details about the media + projection host. + * @param session the recording session for the current media projection. Can be + * {@code null} when the recording will stop. + */ + void onMediaProjectionEvent( + in MediaProjectionEvent event, + in @nullable MediaProjectionInfo info, + in @nullable ContentRecordingSession session + ); } diff --git a/media/java/android/media/projection/MediaProjectionEvent.aidl b/media/java/android/media/projection/MediaProjectionEvent.aidl new file mode 100644 index 000000000000..34359900ce81 --- /dev/null +++ b/media/java/android/media/projection/MediaProjectionEvent.aidl @@ -0,0 +1,3 @@ +package android.media.projection; + +parcelable MediaProjectionEvent;
\ No newline at end of file diff --git a/media/java/android/media/projection/MediaProjectionEvent.java b/media/java/android/media/projection/MediaProjectionEvent.java new file mode 100644 index 000000000000..6922560c8abe --- /dev/null +++ b/media/java/android/media/projection/MediaProjectionEvent.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.projection; + +import android.annotation.IntDef; +import android.os.Parcel; +import android.os.Parcelable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; + +/** @hide */ +public final class MediaProjectionEvent implements Parcelable { + + /** + * Represents various media projection events. + */ + @IntDef({PROJECTION_STARTED_DURING_CALL_AND_ACTIVE_POST_CALL}) + @Retention(RetentionPolicy.SOURCE) + public @interface EventType {} + + /** Event type for when a call ends but the session is still active. */ + public static final int PROJECTION_STARTED_DURING_CALL_AND_ACTIVE_POST_CALL = 0; + + private final @EventType int mEventType; + private final long mTimestampMillis; + + public MediaProjectionEvent(@EventType int eventType, long timestampMillis) { + mEventType = eventType; + mTimestampMillis = timestampMillis; + } + + private MediaProjectionEvent(Parcel in) { + mEventType = in.readInt(); + mTimestampMillis = in.readLong(); + } + + public @EventType int getEventType() { + return mEventType; + } + + public long getTimestampMillis() { + return mTimestampMillis; + } + + @Override + public boolean equals(Object o) { + if (o instanceof MediaProjectionEvent other) { + return mEventType == other.mEventType && mTimestampMillis == other.mTimestampMillis; + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(mEventType, mTimestampMillis); + } + + @Override + public String toString() { + return "MediaProjectionEvent{mEventType=" + mEventType + ", mTimestampMillis=" + + mTimestampMillis + "}"; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(mEventType); + out.writeLong(mTimestampMillis); + } + + public static final Parcelable.Creator<MediaProjectionEvent> CREATOR = + new Parcelable.Creator<>() { + @Override + public MediaProjectionEvent createFromParcel(Parcel in) { + return new MediaProjectionEvent(in); + } + + @Override + public MediaProjectionEvent[] newArray(int size) { + return new MediaProjectionEvent[size]; + } + }; +} diff --git a/media/java/android/media/projection/MediaProjectionManager.java b/media/java/android/media/projection/MediaProjectionManager.java index 9cc2cca441a4..9036bf385d96 100644 --- a/media/java/android/media/projection/MediaProjectionManager.java +++ b/media/java/android/media/projection/MediaProjectionManager.java @@ -363,6 +363,19 @@ public final class MediaProjectionManager { @Nullable ContentRecordingSession session ) { } + + /** + * Called when a specific {@link MediaProjectionEvent} occurs during the media projection + * session. + * + * @param event the media projection event details. + * @param info optional details about the media projection host. + * @param session optional associated recording session details. + */ + public void onMediaProjectionEvent( + final MediaProjectionEvent event, + @Nullable MediaProjectionInfo info, + @Nullable final ContentRecordingSession session) {} } /** @hide */ @@ -405,5 +418,13 @@ public final class MediaProjectionManager { ) { mHandler.post(() -> mCallback.onRecordingSessionSet(info, session)); } + + @Override + public void onMediaProjectionEvent( + final MediaProjectionEvent event, + @Nullable MediaProjectionInfo info, + @Nullable final ContentRecordingSession session) { + mHandler.post(() -> mCallback.onMediaProjectionEvent(event, info, session)); + } } } diff --git a/media/java/android/media/quality/MediaQualityContract.java b/media/java/android/media/quality/MediaQualityContract.java index d1f63404dbff..e558209420e0 100644 --- a/media/java/android/media/quality/MediaQualityContract.java +++ b/media/java/android/media/quality/MediaQualityContract.java @@ -385,6 +385,332 @@ public class MediaQualityContract { public static final String PARAMETER_AUTO_SUPER_RESOLUTION_ENABLED = "auto_super_resolution_enabled"; + /** + * @hide + * + */ + public static final String PARAMETER_LEVEL_RANGE = "level_range"; + + /** + * @hide + * + */ + public static final String PARAMETER_GAMUT_MAPPING = "gamut_mapping"; + + /** + * @hide + * + */ + public static final String PARAMETER_PC_MODE = "pc_mode"; + + /** + * @hide + * + */ + public static final String PARAMETER_LOW_LATENCY = "low_latency"; + + /** + * @hide + * + */ + public static final String PARAMETER_VRR = "vrr"; + + /** + * @hide + * + */ + public static final String PARAMETER_CVRR = "cvrr"; + + /** + * @hide + * + */ + public static final String PARAMETER_HDMI_RGB_RANGE = "hdmi_rgb_range"; + + /** + * @hide + * + */ + public static final String PARAMETER_COLOR_SPACE = "color_space"; + + /** + * @hide + * + */ + public static final String PARAMETER_PANEL_INIT_MAX_LUMINCE_NITS = + "panel_init_max_lumince_nits"; + + /** + * @hide + * + */ + public static final String PARAMETER_PANEL_INIT_MAX_LUMINCE_VALID = + "panel_init_max_lumince_valid"; + + /** + * @hide + * + */ + public static final String PARAMETER_GAMMA = "gamma"; + + /** + * @hide + * + */ + public static final String PARAMETER_COLOR_TEMPERATURE_RED_OFFSET = + "color_temperature_red_offset"; + + /** + * @hide + * + */ + public static final String PARAMETER_COLOR_TEMPERATURE_GREEN_OFFSET = + "color_temperature_green_offset"; + + /** + * @hide + * + */ + public static final String PARAMETER_COLOR_TEMPERATURE_BLUE_OFFSET = + "color_temperature_blue_offset"; + + /** + * @hide + * + */ + public static final String PARAMETER_ELEVEN_POINT_RED = "eleven_point_red"; + + /** + * @hide + * + */ + public static final String PARAMETER_ELEVEN_POINT_GREEN = "eleven_point_green"; + + /** + * @hide + * + */ + public static final String PARAMETER_ELEVEN_POINT_BLUE = "eleven_point_blue"; + + /** + * @hide + * + */ + public static final String PARAMETER_LOW_BLUE_LIGHT = "low_blue_light"; + + /** + * @hide + * + */ + public static final String PARAMETER_LD_MODE = "ld_mode"; + + /** + * @hide + * + */ + public static final String PARAMETER_OSD_RED_GAIN = "osd_red_gain"; + + /** + * @hide + * + */ + public static final String PARAMETER_OSD_GREEN_GAIN = "osd_green_gain"; + + /** + * @hide + * + */ + public static final String PARAMETER_OSD_BLUE_GAIN = "osd_blue_gain"; + + /** + * @hide + * + */ + public static final String PARAMETER_OSD_RED_OFFSET = "osd_red_offset"; + + /** + * @hide + * + */ + public static final String PARAMETER_OSD_GREEN_OFFSET = "osd_green_offset"; + + /** + * @hide + * + */ + public static final String PARAMETER_OSD_BLUE_OFFSET = "osd_blue_offset"; + + /** + * @hide + * + */ + public static final String PARAMETER_OSD_HUE = "osd_hue"; + + /** + * @hide + * + */ + public static final String PARAMETER_OSD_SATURATION = "osd_saturation"; + + /** + * @hide + * + */ + public static final String PARAMETER_OSD_CONTRAST = "osd_contrast"; + + /** + * @hide + * + */ + public static final String PARAMETER_COLOR_TUNER_SWITCH = "color_tuner_switch"; + + /** + * @hide + * + */ + public static final String PARAMETER_COLOR_TUNER_HUE_RED = "color_tuner_hue_red"; + + /** + * @hide + * + */ + public static final String PARAMETER_COLOR_TUNER_HUE_GREEN = "color_tuner_hue_green"; + + /** + * @hide + * + */ + public static final String PARAMETER_COLOR_TUNER_HUE_BLUE = "color_tuner_hue_blue"; + + /** + * @hide + * + */ + public static final String PARAMETER_COLOR_TUNER_HUE_CYAN = "color_tuner_hue_cyan"; + + /** + * @hide + * + */ + public static final String PARAMETER_COLOR_TUNER_HUE_MAGENTA = "color_tuner_hue_magenta"; + + /** + * @hide + * + */ + public static final String PARAMETER_COLOR_TUNER_HUE_YELLOW = "color_tuner_hue_yellow"; + + /** + * @hide + * + */ + public static final String PARAMETER_COLOR_TUNER_HUE_FLESH = "color_tuner_hue_flesh"; + + /** + * @hide + * + */ + public static final String PARAMETER_COLOR_TUNER_SATURATION_RED = + "color_tuner_saturation_red"; + + /** + * @hide + * + */ + public static final String PARAMETER_COLOR_TUNER_SATURATION_GREEN = + "color_tuner_saturation_green"; + + /** + * @hide + * + */ + public static final String PARAMETER_COLOR_TUNER_SATURATION_BLUE = + "color_tuner_saturation_blue"; + + /** + * @hide + * + */ + public static final String PARAMETER_COLOR_TUNER_SATURATION_CYAN = + "color_tuner_saturation_cyan"; + + /** + * @hide + * + */ + public static final String PARAMETER_COLOR_TUNER_SATURATION_MAGENTA = + "color_tuner_saturation_magenta"; + + /** + * @hide + * + */ + public static final String PARAMETER_COLOR_TUNER_SATURATION_YELLOW = + "color_tuner_saturation_yellow"; + + /** + * @hide + * + */ + public static final String PARAMETER_COLOR_TUNER_SATURATION_FLESH = + "color_tuner_saturation_flesh"; + + /** + * @hide + * + */ + public static final String PARAMETER_COLOR_TUNER_LUMINANCE_RED = + "color_tuner_luminance_red"; + + /** + * @hide + * + */ + public static final String PARAMETER_COLOR_TUNER_LUMINANCE_GREEN = + "color_tuner_luminance_green"; + + /** + * @hide + * + */ + public static final String PARAMETER_COLOR_TUNER_LUMINANCE_BLUE = + "color_tuner_luminance_blue"; + + /** + * @hide + * + */ + public static final String PARAMETER_COLOR_TUNER_LUMINANCE_CYAN = + "color_tuner_luminance_cyan"; + + /** + * @hide + * + */ + public static final String PARAMETER_COLOR_TUNER_LUMINANCE_MAGENTA = + "color_tuner_luminance_magenta"; + + /** + * @hide + * + */ + public static final String PARAMETER_COLOR_TUNER_LUMINANCE_YELLOW = + "color_tuner_luminance_yellow"; + + /** + * @hide + * + */ + public static final String PARAMETER_COLOR_TUNER_LUMINANCE_FLESH = + "color_tuner_luminance_flesh"; + + /** + * @hide + * + */ + public static final String PARAMETER_PICTURE_QUALITY_EVENT_TYPE = + "picture_quality_event_type"; + private PictureQuality() { } } @@ -641,6 +967,12 @@ public class MediaQualityContract { */ public static final String PARAMETER_DIGITAL_OUTPUT_MODE = "digital_output_mode"; + /** + * @hide + */ + public static final String PARAMETER_SOUND_STYLE = "sound_style"; + + private SoundQuality() { } diff --git a/media/java/android/media/quality/MediaQualityManager.java b/media/java/android/media/quality/MediaQualityManager.java index b7269256a449..0d6d32a22dae 100644 --- a/media/java/android/media/quality/MediaQualityManager.java +++ b/media/java/android/media/quality/MediaQualityManager.java @@ -51,7 +51,6 @@ import java.util.function.Consumer; @FlaggedApi(Flags.FLAG_MEDIA_QUALITY_FW) @SystemService(Context.MEDIA_QUALITY_SERVICE) public final class MediaQualityManager { - // TODO: unhide the APIs for api review private static final String TAG = "MediaQualityManager"; private final IMediaQualityManager mService; @@ -123,7 +122,6 @@ public final class MediaQualityManager { public void onPictureProfileAdded(String profileId, PictureProfile profile) { synchronized (mPpLock) { for (PictureProfileCallbackRecord record : mPpCallbackRecords) { - // TODO: filter callback record record.postPictureProfileAdded(profileId, profile); } } @@ -132,7 +130,6 @@ public final class MediaQualityManager { public void onPictureProfileUpdated(String profileId, PictureProfile profile) { synchronized (mPpLock) { for (PictureProfileCallbackRecord record : mPpCallbackRecords) { - // TODO: filter callback record record.postPictureProfileUpdated(profileId, profile); } } @@ -141,7 +138,6 @@ public final class MediaQualityManager { public void onPictureProfileRemoved(String profileId, PictureProfile profile) { synchronized (mPpLock) { for (PictureProfileCallbackRecord record : mPpCallbackRecords) { - // TODO: filter callback record record.postPictureProfileRemoved(profileId, profile); } } @@ -151,7 +147,6 @@ public final class MediaQualityManager { String profileId, List<ParameterCapability> caps) { synchronized (mPpLock) { for (PictureProfileCallbackRecord record : mPpCallbackRecords) { - // TODO: filter callback record record.postParameterCapabilitiesChanged(profileId, caps); } } @@ -160,7 +155,6 @@ public final class MediaQualityManager { public void onError(String profileId, int err) { synchronized (mPpLock) { for (PictureProfileCallbackRecord record : mPpCallbackRecords) { - // TODO: filter callback record record.postError(profileId, err); } } @@ -171,7 +165,6 @@ public final class MediaQualityManager { public void onSoundProfileAdded(String profileId, SoundProfile profile) { synchronized (mSpLock) { for (SoundProfileCallbackRecord record : mSpCallbackRecords) { - // TODO: filter callback record record.postSoundProfileAdded(profileId, profile); } } @@ -180,7 +173,6 @@ public final class MediaQualityManager { public void onSoundProfileUpdated(String profileId, SoundProfile profile) { synchronized (mSpLock) { for (SoundProfileCallbackRecord record : mSpCallbackRecords) { - // TODO: filter callback record record.postSoundProfileUpdated(profileId, profile); } } @@ -189,7 +181,6 @@ public final class MediaQualityManager { public void onSoundProfileRemoved(String profileId, SoundProfile profile) { synchronized (mSpLock) { for (SoundProfileCallbackRecord record : mSpCallbackRecords) { - // TODO: filter callback record record.postSoundProfileRemoved(profileId, profile); } } @@ -199,7 +190,6 @@ public final class MediaQualityManager { String profileId, List<ParameterCapability> caps) { synchronized (mSpLock) { for (SoundProfileCallbackRecord record : mSpCallbackRecords) { - // TODO: filter callback record record.postParameterCapabilitiesChanged(profileId, caps); } } @@ -208,7 +198,6 @@ public final class MediaQualityManager { public void onError(String profileId, int err) { synchronized (mSpLock) { for (SoundProfileCallbackRecord record : mSpCallbackRecords) { - // TODO: filter callback record record.postError(profileId, err); } } diff --git a/media/java/android/mtp/OWNERS b/media/java/android/mtp/OWNERS index 77ed08b1f9a5..c57265a91eff 100644 --- a/media/java/android/mtp/OWNERS +++ b/media/java/android/mtp/OWNERS @@ -1,9 +1,9 @@ set noparent -anothermark@google.com +vmartensson@google.com +nkapron@google.com febinthattil@google.com -aprasath@google.com +shubhankarm@google.com jsharkey@android.com jameswei@google.com rmojumder@google.com -kumarashishg@google.com diff --git a/media/jni/android_mtp_MtpDatabase.cpp b/media/jni/android_mtp_MtpDatabase.cpp index a77bc9fe0570..a1ce495fe33d 100644 --- a/media/jni/android_mtp_MtpDatabase.cpp +++ b/media/jni/android_mtp_MtpDatabase.cpp @@ -910,7 +910,7 @@ MtpResponseCode MtpDatabase::getObjectInfo(MtpObjectHandle handle, case MTP_FORMAT_TIFF: case MTP_FORMAT_TIFF_EP: case MTP_FORMAT_DEFINED: { - String8 temp(path); + String8 temp {static_cast<std::string_view>(path)}; std::unique_ptr<FileStream> stream(new FileStream(temp)); piex::PreviewImageData image_data; if (!GetExifFromRawImage(stream.get(), temp, image_data)) { @@ -967,7 +967,7 @@ void* MtpDatabase::getThumbnail(MtpObjectHandle handle, size_t& outThumbSize) { case MTP_FORMAT_TIFF: case MTP_FORMAT_TIFF_EP: case MTP_FORMAT_DEFINED: { - String8 temp(path); + String8 temp {static_cast<std::string_view>(path)}; std::unique_ptr<FileStream> stream(new FileStream(temp)); piex::PreviewImageData image_data; if (!GetExifFromRawImage(stream.get(), temp, image_data)) { diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioManagerTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioManagerTest.java index e9a0d3eceba3..209734ca4a53 100644 --- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioManagerTest.java +++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioManagerTest.java @@ -16,6 +16,15 @@ package com.android.audiopolicytest; +import static android.media.AudioManager.STREAM_ACCESSIBILITY; +import static android.media.AudioManager.STREAM_ALARM; +import static android.media.AudioManager.STREAM_DTMF; +import static android.media.AudioManager.STREAM_MUSIC; +import static android.media.AudioManager.STREAM_NOTIFICATION; +import static android.media.AudioManager.STREAM_RING; +import static android.media.AudioManager.STREAM_SYSTEM; +import static android.media.AudioManager.STREAM_VOICE_CALL; + import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static com.android.audiopolicytest.AudioVolumeTestUtil.DEFAULT_ATTRIBUTES; @@ -28,11 +37,15 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import android.content.Context; import android.media.AudioAttributes; import android.media.AudioManager; import android.media.AudioSystem; +import android.media.IAudioService; import android.media.audiopolicy.AudioProductStrategy; import android.media.audiopolicy.AudioVolumeGroup; +import android.os.IBinder; +import android.os.ServiceManager; import android.platform.test.annotations.Presubmit; import android.util.Log; @@ -54,6 +67,17 @@ public class AudioManagerTest { private AudioManager mAudioManager; + private static final int[] PUBLIC_STREAM_TYPES = { + STREAM_VOICE_CALL, + STREAM_SYSTEM, + STREAM_RING, + STREAM_MUSIC, + STREAM_ALARM, + STREAM_NOTIFICATION, + STREAM_DTMF, + STREAM_ACCESSIBILITY, + }; + @Rule public final AudioVolumesTestRule rule = new AudioVolumesTestRule(); @@ -207,6 +231,33 @@ public class AudioManagerTest { } //----------------------------------------------------------------- + // Test getStreamVolume consistency with AudioService + //----------------------------------------------------------------- + @Test + public void getStreamMinMaxVolume_consistentWithAs() throws Exception { + IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE); + IAudioService service = IAudioService.Stub.asInterface(b); + + for (int streamType : PUBLIC_STREAM_TYPES) { + assertEquals(service.getStreamMinVolume(streamType), + mAudioManager.getStreamMinVolume(streamType)); + assertEquals(service.getStreamMaxVolume(streamType), + mAudioManager.getStreamMaxVolume(streamType)); + } + } + + @Test + public void getStreamVolume_consistentWithAs() throws Exception { + IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE); + IAudioService service = IAudioService.Stub.asInterface(b); + + for (int streamType : PUBLIC_STREAM_TYPES) { + assertEquals(service.getStreamVolume(streamType), + mAudioManager.getStreamVolume(streamType)); + } + } + + //----------------------------------------------------------------- // Test Volume per Attributes setter/getters //----------------------------------------------------------------- @Test diff --git a/media/tests/MtpTests/OWNERS b/media/tests/MtpTests/OWNERS index bdb6cdbea332..c57265a91eff 100644 --- a/media/tests/MtpTests/OWNERS +++ b/media/tests/MtpTests/OWNERS @@ -1,9 +1,9 @@ set noparent -anothermark@google.com +vmartensson@google.com +nkapron@google.com febinthattil@google.com -aprasath@google.com +shubhankarm@google.com jsharkey@android.com jameswei@google.com rmojumder@google.com -kumarashishg@google.com
\ No newline at end of file diff --git a/packages/PrintSpooler/src/com/android/printspooler/model/PageContentRepository.java b/packages/PrintSpooler/src/com/android/printspooler/model/PageContentRepository.java index fe27cee7ba2d..acead8e2a0eb 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/model/PageContentRepository.java +++ b/packages/PrintSpooler/src/com/android/printspooler/model/PageContentRepository.java @@ -510,7 +510,10 @@ public final class PageContentRepository { protected Void doInBackground(Void... params) { synchronized (mLock) { try { - if (mRenderer != null) { + // A page count < 0 indicates there was an error + // opening the document, in which case it doesn't + // need to be closed. + if (mRenderer != null && mPageCount >= 0) { mRenderer.closeDocument(); } } catch (RemoteException re) { diff --git a/packages/PrintSpooler/src/com/android/printspooler/model/RemotePrintDocument.java b/packages/PrintSpooler/src/com/android/printspooler/model/RemotePrintDocument.java index b48c55ddfef0..a9d00e9a77eb 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/model/RemotePrintDocument.java +++ b/packages/PrintSpooler/src/com/android/printspooler/model/RemotePrintDocument.java @@ -70,6 +70,7 @@ public final class RemotePrintDocument { private static final int STATE_CANCELING = 6; private static final int STATE_CANCELED = 7; private static final int STATE_DESTROYED = 8; + private static final int STATE_INVALID = 9; private final Context mContext; @@ -287,7 +288,8 @@ public final class RemotePrintDocument { } if (mState != STATE_STARTED && mState != STATE_UPDATED && mState != STATE_FAILED && mState != STATE_CANCELING - && mState != STATE_CANCELED && mState != STATE_DESTROYED) { + && mState != STATE_CANCELED && mState != STATE_DESTROYED + && mState != STATE_INVALID) { throw new IllegalStateException("Cannot finish in state:" + stateToString(mState)); } @@ -300,6 +302,16 @@ public final class RemotePrintDocument { } } + /** + * Mark this document as invalid. + */ + public void invalid() { + if (DEBUG) { + Log.i(LOG_TAG, "[CALLED] invalid()"); + } + mState = STATE_INVALID; + } + public void cancel(boolean force) { if (DEBUG) { Log.i(LOG_TAG, "[CALLED] cancel(" + force + ")"); @@ -491,6 +503,9 @@ public final class RemotePrintDocument { case STATE_DESTROYED: { return "STATE_DESTROYED"; } + case STATE_INVALID: { + return "STATE_INVALID"; + } default: { return "STATE_UNKNOWN"; } diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java index 4a3a6d248254..2e3234e6e622 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java +++ b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java @@ -167,6 +167,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat private static final int STATE_PRINTER_UNAVAILABLE = 6; private static final int STATE_UPDATE_SLOW = 7; private static final int STATE_PRINT_COMPLETED = 8; + private static final int STATE_FILE_INVALID = 9; private static final int UI_STATE_PREVIEW = 0; private static final int UI_STATE_ERROR = 1; @@ -404,6 +405,11 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat public void onPause() { PrintSpoolerService spooler = mSpoolerProvider.getSpooler(); + if (isInvalid()) { + super.onPause(); + return; + } + if (mState == STATE_INITIALIZING) { if (isFinishing()) { if (spooler != null) { @@ -478,7 +484,8 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat } if (mState == STATE_PRINT_CANCELED || mState == STATE_PRINT_CONFIRMED - || mState == STATE_PRINT_COMPLETED) { + || mState == STATE_PRINT_COMPLETED + || mState == STATE_FILE_INVALID) { return true; } @@ -509,23 +516,32 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat @Override public void onMalformedPdfFile() { onPrintDocumentError("Cannot print a malformed PDF file"); + mPrintedDocument.invalid(); + setState(STATE_FILE_INVALID); } @Override public void onSecurePdfFile() { onPrintDocumentError("Cannot print a password protected PDF file"); + mPrintedDocument.invalid(); + setState(STATE_FILE_INVALID); } private void onPrintDocumentError(String message) { setState(mProgressMessageController.cancel()); - ensureErrorUiShown(null, PrintErrorFragment.ACTION_RETRY); + ensureErrorUiShown( + getString(R.string.print_cannot_load_page), PrintErrorFragment.ACTION_NONE); setState(STATE_UPDATE_FAILED); if (DEBUG) { Log.i(LOG_TAG, "PrintJob state[" + PrintJobInfo.STATE_FAILED + "] reason: " + message); } PrintSpoolerService spooler = mSpoolerProvider.getSpooler(); - spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_FAILED, message); + // Use a cancel state for the spooler. This will prevent the notification from getting + // displayed and will remove the job. The notification (which displays the cancel and + // restart options) doesn't make sense for an invalid document since it will just fail + // again. + spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_CANCELED, message); mPrintedDocument.finish(); } @@ -995,6 +1011,9 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat } private void setState(int state) { + if (isInvalid()) { + return; + } if (isFinalState(mState)) { if (isFinalState(state)) { if (DEBUG) { @@ -1015,7 +1034,12 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat private static boolean isFinalState(int state) { return state == STATE_PRINT_CANCELED || state == STATE_PRINT_COMPLETED - || state == STATE_CREATE_FILE_FAILED; + || state == STATE_CREATE_FILE_FAILED + || state == STATE_FILE_INVALID; + } + + private boolean isInvalid() { + return mState == STATE_FILE_INVALID; } private void updateSelectedPagesFromPreview() { @@ -1100,7 +1124,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat } private void ensurePreviewUiShown() { - if (isFinishing() || isDestroyed()) { + if (isFinishing() || isDestroyed() || isInvalid()) { return; } if (mUiState != UI_STATE_PREVIEW) { @@ -1257,6 +1281,9 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat } private boolean updateDocument(boolean clearLastError) { + if (isInvalid()) { + return false; + } if (!clearLastError && mPrintedDocument.hasUpdateError()) { return false; } @@ -1676,7 +1703,8 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat || mState == STATE_UPDATE_FAILED || mState == STATE_CREATE_FILE_FAILED || mState == STATE_PRINTER_UNAVAILABLE - || mState == STATE_UPDATE_SLOW) { + || mState == STATE_UPDATE_SLOW + || mState == STATE_FILE_INVALID) { disableOptionsUi(isFinalState(mState)); return; } @@ -2100,6 +2128,9 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat } private boolean canUpdateDocument() { + if (isInvalid()) { + return false; + } if (mPrintedDocument.isDestroyed()) { return false; } diff --git a/packages/SettingsLib/BannerMessagePreference/Android.bp b/packages/SettingsLib/BannerMessagePreference/Android.bp index 77e2cc735895..182daeb45d7c 100644 --- a/packages/SettingsLib/BannerMessagePreference/Android.bp +++ b/packages/SettingsLib/BannerMessagePreference/Android.bp @@ -28,4 +28,8 @@ android_library { sdk_version: "system_current", min_sdk_version: "28", + apex_available: [ + "//apex_available:platform", + "com.android.healthfitness", + ], } diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled.xml index f55b320269a8..ff22b2e7f86f 100644 --- a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled.xml +++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled.xml @@ -20,7 +20,8 @@ android:layout_height="wrap_content" android:orientation="vertical" android:paddingStart="?android:attr/listPreferredItemPaddingStart" - android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:filterTouchesWhenObscured="true"> <com.google.android.material.button.MaterialButton android:id="@+id/settingslib_button" diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_extra.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_extra.xml index b663b6ccc5bf..d878ba0d310b 100644 --- a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_extra.xml +++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_extra.xml @@ -20,7 +20,8 @@ android:layout_height="wrap_content" android:orientation="vertical" android:paddingStart="?android:attr/listPreferredItemPaddingStart" - android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:filterTouchesWhenObscured="true"> <com.google.android.material.button.MaterialButton android:id="@+id/settingslib_button" diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_large.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_large.xml index 784e6ad6a9f8..8f0a158c55eb 100644 --- a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_large.xml +++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_large.xml @@ -20,7 +20,8 @@ android:layout_height="wrap_content" android:orientation="vertical" android:paddingStart="?android:attr/listPreferredItemPaddingStart" - android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:filterTouchesWhenObscured="true"> <com.google.android.material.button.MaterialButton android:id="@+id/settingslib_button" diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline.xml index 8b44a6539801..0c8996063e9f 100644 --- a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline.xml +++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline.xml @@ -20,7 +20,8 @@ android:layout_height="wrap_content" android:orientation="vertical" android:paddingStart="?android:attr/listPreferredItemPaddingStart" - android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:filterTouchesWhenObscured="true"> <com.google.android.material.button.MaterialButton android:id="@+id/settingslib_button" diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_extra.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_extra.xml index f8a2d8fbd975..41d8490feeb3 100644 --- a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_extra.xml +++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_extra.xml @@ -20,7 +20,8 @@ android:layout_height="wrap_content" android:orientation="vertical" android:paddingStart="?android:attr/listPreferredItemPaddingStart" - android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:filterTouchesWhenObscured="true"> <com.google.android.material.button.MaterialButton android:id="@+id/settingslib_button" diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_large.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_large.xml index 781a5a136164..958552064c98 100644 --- a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_large.xml +++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_large.xml @@ -20,7 +20,8 @@ android:layout_height="wrap_content" android:orientation="vertical" android:paddingStart="?android:attr/listPreferredItemPaddingStart" - android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:filterTouchesWhenObscured="true"> <com.google.android.material.button.MaterialButton android:id="@+id/settingslib_button" diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal.xml index 5b568f870ea4..03ca1f0a1033 100644 --- a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal.xml +++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal.xml @@ -20,7 +20,8 @@ android:layout_height="wrap_content" android:orientation="vertical" android:paddingStart="?android:attr/listPreferredItemPaddingStart" - android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:filterTouchesWhenObscured="true"> <com.google.android.material.button.MaterialButton android:id="@+id/settingslib_button" diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_extra.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_extra.xml index 1e7a08b714f1..030ee66fef3f 100644 --- a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_extra.xml +++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_extra.xml @@ -20,7 +20,8 @@ android:layout_height="wrap_content" android:orientation="vertical" android:paddingStart="?android:attr/listPreferredItemPaddingStart" - android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:filterTouchesWhenObscured="true"> <com.google.android.material.button.MaterialButton android:id="@+id/settingslib_button" diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_large.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_large.xml index 42116be07041..5c16723f7a63 100644 --- a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_large.xml +++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_large.xml @@ -20,7 +20,8 @@ android:layout_height="wrap_content" android:orientation="vertical" android:paddingStart="?android:attr/listPreferredItemPaddingStart" - android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:filterTouchesWhenObscured="true"> <com.google.android.material.button.MaterialButton android:id="@+id/settingslib_button" diff --git a/packages/SettingsLib/ButtonPreference/res/layout/settingslib_button_layout.xml b/packages/SettingsLib/ButtonPreference/res/layout/settingslib_button_layout.xml index 1ff09901ffaf..5405045a013d 100644 --- a/packages/SettingsLib/ButtonPreference/res/layout/settingslib_button_layout.xml +++ b/packages/SettingsLib/ButtonPreference/res/layout/settingslib_button_layout.xml @@ -20,7 +20,8 @@ android:layout_height="wrap_content" android:orientation="vertical" android:paddingStart="?android:attr/listPreferredItemPaddingStart" - android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:filterTouchesWhenObscured="true"> <Button android:id="@+id/settingslib_button" diff --git a/packages/SettingsLib/ButtonPreference/res/layout/settingslib_number_button.xml b/packages/SettingsLib/ButtonPreference/res/layout/settingslib_number_button.xml index fa13b4125065..b23c5a510745 100644 --- a/packages/SettingsLib/ButtonPreference/res/layout/settingslib_number_button.xml +++ b/packages/SettingsLib/ButtonPreference/res/layout/settingslib_number_button.xml @@ -29,7 +29,8 @@ android:layout_height="wrap_content" android:paddingVertical="@dimen/settingslib_expressive_space_small1" android:paddingHorizontal="@dimen/settingslib_expressive_space_small4" - android:background="@drawable/settingslib_number_button_background"> + android:background="@drawable/settingslib_number_button_background" + android:filterTouchesWhenObscured="true"> <TextView android:id="@+id/settingslib_number_count" android:layout_width="wrap_content" diff --git a/packages/SettingsLib/ButtonPreference/res/layout/settingslib_section_button.xml b/packages/SettingsLib/ButtonPreference/res/layout/settingslib_section_button.xml index e7fb572d06dc..66a4c2e25c77 100644 --- a/packages/SettingsLib/ButtonPreference/res/layout/settingslib_section_button.xml +++ b/packages/SettingsLib/ButtonPreference/res/layout/settingslib_section_button.xml @@ -21,7 +21,8 @@ android:orientation="vertical" android:paddingStart="?android:attr/listPreferredItemPaddingStart" android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" - android:paddingVertical="@dimen/settingslib_expressive_space_extrasmall4"> + android:paddingVertical="@dimen/settingslib_expressive_space_extrasmall4" + android:filterTouchesWhenObscured="true"> <com.google.android.material.button.MaterialButton android:id="@+id/settingslib_section_button" diff --git a/packages/SettingsLib/DisplayUtils/Android.bp b/packages/SettingsLib/DisplayUtils/Android.bp index 279bb70d81bf..62630b5a9331 100644 --- a/packages/SettingsLib/DisplayUtils/Android.bp +++ b/packages/SettingsLib/DisplayUtils/Android.bp @@ -15,6 +15,4 @@ android_library { ], srcs: ["src/**/*.java"], - - min_sdk_version: "21", } diff --git a/packages/SettingsLib/DisplayUtils/src/com/android/settingslib/display/DisplayDensityConfiguration.java b/packages/SettingsLib/DisplayUtils/src/com/android/settingslib/display/DisplayDensityConfiguration.java index 284a9025de64..127e628fbd2f 100644 --- a/packages/SettingsLib/DisplayUtils/src/com/android/settingslib/display/DisplayDensityConfiguration.java +++ b/packages/SettingsLib/DisplayUtils/src/com/android/settingslib/display/DisplayDensityConfiguration.java @@ -16,13 +16,20 @@ package com.android.settingslib.display; +import android.annotation.NonNull; +import android.content.Context; +import android.hardware.display.DisplayManager; import android.os.AsyncTask; import android.os.RemoteException; import android.os.UserHandle; import android.util.Log; +import android.view.Display; +import android.view.DisplayInfo; import android.view.IWindowManager; import android.view.WindowManagerGlobal; +import java.util.function.Predicate; + /** Utility methods for controlling the display density. */ public class DisplayDensityConfiguration { private static final String LOG_TAG = "DisplayDensityConfig"; @@ -85,4 +92,42 @@ public class DisplayDensityConfiguration { } }); } + + /** + * Asynchronously applies display density changes to all displays that satisfy the predicate. + * + * <p>The change will be applied to the user specified by the value of + * {@link UserHandle#myUserId()} at the time the method is called. + * + * @param context The context + * @param predicate Determines which displays to set the density to + * @param density The density to force + */ + public static void setForcedDisplayDensity(@NonNull Context context, + @NonNull Predicate<DisplayInfo> predicate, final int density) { + final int userId = UserHandle.myUserId(); + DisplayManager dm = context.getSystemService(DisplayManager.class); + AsyncTask.execute(() -> { + try { + for (Display display : dm.getDisplays( + DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED)) { + int displayId = display.getDisplayId(); + DisplayInfo info = new DisplayInfo(); + if (!display.getDisplayInfo(info)) { + Log.w(LOG_TAG, "Unable to save forced display density setting " + + "for display " + displayId); + continue; + } + if (!predicate.test(info)) { + continue; + } + + final IWindowManager wm = WindowManagerGlobal.getWindowManagerService(); + wm.setForcedDisplayDensityForUser(displayId, density, userId); + } + } catch (RemoteException exc) { + Log.w(LOG_TAG, "Unable to save forced display density setting"); + } + }); + } } diff --git a/packages/SettingsLib/Graph/graph.proto b/packages/SettingsLib/Graph/graph.proto index 33a7df4c6ba8..a834947144a0 100644 --- a/packages/SettingsLib/Graph/graph.proto +++ b/packages/SettingsLib/Graph/graph.proto @@ -26,6 +26,14 @@ message PreferenceScreenProto { optional PreferenceGroupProto root = 2; // If the preference screen provides complete hierarchy by source code. optional bool complete_hierarchy = 3; + // Parameterized screens (not recursive, provided on the top level only) + repeated ParameterizedPreferenceScreenProto parameterized_screens = 4; +} + +// Proto of parameterized preference screen +message ParameterizedPreferenceScreenProto { + optional BundleProto args = 1; + optional PreferenceScreenProto screen = 2; } // Proto of PreferenceGroup. diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt index adffd206d552..27ce1c7246e6 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt @@ -18,12 +18,14 @@ package com.android.settingslib.graph import android.app.Application import android.os.Bundle +import android.os.Parcelable import android.os.SystemClock import com.android.settingslib.graph.proto.PreferenceGraphProto import com.android.settingslib.ipc.ApiHandler import com.android.settingslib.ipc.ApiPermissionChecker import com.android.settingslib.ipc.MessageCodec import com.android.settingslib.metadata.PreferenceRemoteOpMetricsLogger +import com.android.settingslib.metadata.PreferenceScreenCoordinate import com.android.settingslib.metadata.PreferenceScreenRegistry import com.android.settingslib.preference.PreferenceScreenProvider import java.util.Locale @@ -59,10 +61,9 @@ class GetPreferenceGraphApiHandler( var success = false try { val builder = PreferenceGraphBuilder.of(application, callingPid, callingUid, request) - if (request.screenKeys.isEmpty()) { - PreferenceScreenRegistry.preferenceScreenMetadataFactories.forEachKeyAsync { - builder.addPreferenceScreenFromRegistry(it) - } + if (request.screens.isEmpty()) { + val factories = PreferenceScreenRegistry.preferenceScreenMetadataFactories + factories.forEachAsync { _, factory -> builder.addPreferenceScreen(factory) } for (provider in preferenceScreenProviders) { builder.addPreferenceScreenProvider(provider) } @@ -84,15 +85,15 @@ class GetPreferenceGraphApiHandler( /** * Request of [GetPreferenceGraphApiHandler]. * - * @param screenKeys screen keys of the preference graph - * @param visitedScreens keys of the visited preference screen + * @param screens screens of the preference graph + * @param visitedScreens visited preference screens * @param locale locale of the preference graph */ data class GetPreferenceGraphRequest @JvmOverloads constructor( - val screenKeys: Set<String> = setOf(), - val visitedScreens: Set<String> = setOf(), + val screens: Set<PreferenceScreenCoordinate> = setOf(), + val visitedScreens: Set<PreferenceScreenCoordinate> = setOf(), val locale: Locale? = null, val flags: Int = PreferenceGetterFlags.ALL, val includeValueDescriptor: Boolean = true, @@ -101,26 +102,32 @@ constructor( object GetPreferenceGraphRequestCodec : MessageCodec<GetPreferenceGraphRequest> { override fun encode(data: GetPreferenceGraphRequest): Bundle = Bundle(4).apply { - putStringArray(KEY_SCREEN_KEYS, data.screenKeys.toTypedArray()) - putStringArray(KEY_VISITED_KEYS, data.visitedScreens.toTypedArray()) + putParcelableArray(KEY_SCREENS, data.screens.toTypedArray()) + putParcelableArray(KEY_VISITED_SCREENS, data.visitedScreens.toTypedArray()) putString(KEY_LOCALE, data.locale?.toLanguageTag()) putInt(KEY_FLAGS, data.flags) } + @Suppress("DEPRECATION") override fun decode(data: Bundle): GetPreferenceGraphRequest { - val screenKeys = data.getStringArray(KEY_SCREEN_KEYS) ?: arrayOf() - val visitedScreens = data.getStringArray(KEY_VISITED_KEYS) ?: arrayOf() + data.classLoader = PreferenceScreenCoordinate::class.java.classLoader + val screens = data.getParcelableArray(KEY_SCREENS) ?: arrayOf() + val visitedScreens = data.getParcelableArray(KEY_VISITED_SCREENS) ?: arrayOf() fun String?.toLocale() = if (this != null) Locale.forLanguageTag(this) else null + fun Array<Parcelable>.toScreenCoordinates() = + buildSet(size) { + for (element in this@toScreenCoordinates) add(element as PreferenceScreenCoordinate) + } return GetPreferenceGraphRequest( - screenKeys.toSet(), - visitedScreens.toSet(), + screens.toScreenCoordinates(), + visitedScreens.toScreenCoordinates(), data.getString(KEY_LOCALE).toLocale(), data.getInt(KEY_FLAGS), ) } - private const val KEY_SCREEN_KEYS = "k" - private const val KEY_VISITED_KEYS = "v" + private const val KEY_SCREENS = "s" + private const val KEY_VISITED_SCREENS = "v" private const val KEY_LOCALE = "l" private const val KEY_FLAGS = "f" } diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt index a9958b975fc6..1d4e2c9e1bef 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt @@ -26,6 +26,7 @@ import com.android.settingslib.ipc.ApiPermissionChecker import com.android.settingslib.metadata.PreferenceCoordinate import com.android.settingslib.metadata.PreferenceHierarchyNode import com.android.settingslib.metadata.PreferenceRemoteOpMetricsLogger +import com.android.settingslib.metadata.PreferenceScreenCoordinate import com.android.settingslib.metadata.PreferenceScreenRegistry /** @@ -105,8 +106,10 @@ class PreferenceGetterApiHandler( val errors = mutableMapOf<PreferenceCoordinate, Int>() val preferences = mutableMapOf<PreferenceCoordinate, PreferenceProto>() val flags = request.flags - for ((screenKey, coordinates) in request.preferences.groupBy { it.screenKey }) { - val screenMetadata = PreferenceScreenRegistry.create(application, screenKey) + val groups = + request.preferences.groupBy { PreferenceScreenCoordinate(it.screenKey, it.args) } + for ((screen, coordinates) in groups) { + val screenMetadata = PreferenceScreenRegistry.create(application, screen) if (screenMetadata == null) { val latencyMs = SystemClock.elapsedRealtime() - elapsedRealtime for (coordinate in coordinates) { diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt index c0d244989044..4290437b0d02 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt @@ -40,6 +40,7 @@ import com.android.settingslib.graph.proto.PreferenceProto import com.android.settingslib.graph.proto.PreferenceProto.ActionTarget import com.android.settingslib.graph.proto.PreferenceScreenProto import com.android.settingslib.graph.proto.TextProto +import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_ARGS import com.android.settingslib.metadata.IntRangeValuePreference import com.android.settingslib.metadata.PersistentPreference import com.android.settingslib.metadata.PreferenceAvailabilityProvider @@ -47,7 +48,10 @@ import com.android.settingslib.metadata.PreferenceHierarchy import com.android.settingslib.metadata.PreferenceMetadata import com.android.settingslib.metadata.PreferenceRestrictionProvider import com.android.settingslib.metadata.PreferenceScreenBindingKeyProvider +import com.android.settingslib.metadata.PreferenceScreenCoordinate import com.android.settingslib.metadata.PreferenceScreenMetadata +import com.android.settingslib.metadata.PreferenceScreenMetadataFactory +import com.android.settingslib.metadata.PreferenceScreenMetadataParameterizedFactory import com.android.settingslib.metadata.PreferenceScreenRegistry import com.android.settingslib.metadata.PreferenceSummaryProvider import com.android.settingslib.metadata.PreferenceTitleProvider @@ -72,15 +76,19 @@ private constructor( PreferenceScreenFactory(context.ofLocale(request.locale)) } private val builder by lazy { PreferenceGraphProto.newBuilder() } - private val visitedScreens = mutableSetOf<String>().apply { addAll(request.visitedScreens) } + private val visitedScreens = request.visitedScreens.toMutableSet() + private val screens = mutableMapOf<String, PreferenceScreenProto.Builder>() private suspend fun init() { - for (key in request.screenKeys) { - addPreferenceScreenFromRegistry(key) + for (screen in request.screens) { + PreferenceScreenRegistry.create(context, screen)?.let { addPreferenceScreen(it) } } } - fun build(): PreferenceGraphProto = builder.build() + fun build(): PreferenceGraphProto { + for ((key, screenBuilder) in screens) builder.putScreens(key, screenBuilder.build()) + return builder.build() + } /** * Adds an activity to the graph. @@ -138,19 +146,12 @@ private constructor( null } - suspend fun addPreferenceScreenFromRegistry(key: String): Boolean { - val metadata = PreferenceScreenRegistry.create(context, key) ?: return false - return addPreferenceScreenMetadata(metadata) + private suspend fun addPreferenceScreenFromRegistry(key: String): Boolean { + val factory = + PreferenceScreenRegistry.preferenceScreenMetadataFactories[key] ?: return false + return addPreferenceScreen(factory) } - private suspend fun addPreferenceScreenMetadata(metadata: PreferenceScreenMetadata): Boolean = - addPreferenceScreen(metadata.key) { - preferenceScreenProto { - completeHierarchy = metadata.hasCompleteHierarchy() - root = metadata.getPreferenceHierarchy(context).toProto(metadata, true) - } - } - suspend fun addPreferenceScreenProvider(activityClass: Class<*>) { Log.d(TAG, "add $activityClass") createPreferenceScreen { activityClass.newInstance() } @@ -188,26 +189,52 @@ private constructor( Log.e(TAG, "\"$preferenceScreen\" has no key") return } - @Suppress("CheckReturnValue") addPreferenceScreen(key) { preferenceScreen.toProto(intent) } + val args = preferenceScreen.peekExtras()?.getBundle(EXTRA_BINDING_SCREEN_ARGS) + @Suppress("CheckReturnValue") + addPreferenceScreen(key, args) { + this.intent = intent.toProto() + root = preferenceScreen.toProto() + } + } + + suspend fun addPreferenceScreen(factory: PreferenceScreenMetadataFactory): Boolean { + if (factory is PreferenceScreenMetadataParameterizedFactory) { + factory.parameters(context).collect { addPreferenceScreen(factory.create(context, it)) } + return true + } + return addPreferenceScreen(factory.create(context)) } + private suspend fun addPreferenceScreen(metadata: PreferenceScreenMetadata): Boolean = + addPreferenceScreen(metadata.key, metadata.arguments) { + completeHierarchy = metadata.hasCompleteHierarchy() + root = metadata.getPreferenceHierarchy(context).toProto(metadata, true) + } + private suspend fun addPreferenceScreen( key: String, - preferenceScreenProvider: suspend () -> PreferenceScreenProto, - ): Boolean = - if (visitedScreens.add(key)) { - builder.putScreens(key, preferenceScreenProvider()) - true - } else { - Log.w(TAG, "$key visited") - false + args: Bundle?, + init: suspend PreferenceScreenProto.Builder.() -> Unit, + ): Boolean { + if (!visitedScreens.add(PreferenceScreenCoordinate(key, args))) { + Log.w(TAG, "$key $args visited") + return false } - - private suspend fun PreferenceScreen.toProto(intent: Intent?): PreferenceScreenProto = - preferenceScreenProto { - intent?.let { this.intent = it.toProto() } - root = (this@toProto as PreferenceGroup).toProto() + if (args == null) { // normal screen + screens[key] = PreferenceScreenProto.newBuilder().also { init(it) } + } else if (args.isEmpty) { // parameterized screen with backward compatibility + val builder = screens.getOrPut(key) { PreferenceScreenProto.newBuilder() } + init(builder) + } else { // parameterized screen with non-empty arguments + val builder = screens.getOrPut(key) { PreferenceScreenProto.newBuilder() } + val parameterizedScreen = parameterizedPreferenceScreenProto { + setArgs(args.toProto()) + setScreen(PreferenceScreenProto.newBuilder().also { init(it) }) + } + builder.addParameterizedScreens(parameterizedScreen) } + return true + } private suspend fun PreferenceGroup.toProto(): PreferenceGroupProto = preferenceGroupProto { preference = (this@toProto as Preference).toProto() @@ -271,7 +298,7 @@ private constructor( .toProto(context, callingPid, callingUid, screenMetadata, isRoot, request.flags) .also { if (metadata is PreferenceScreenMetadata) { - @Suppress("CheckReturnValue") addPreferenceScreenMetadata(metadata) + @Suppress("CheckReturnValue") addPreferenceScreen(metadata) } metadata.intent(context)?.resolveActivity(context.packageManager)?.let { if (it.packageName == context.packageName) { @@ -322,7 +349,7 @@ private constructor( val screenKey = screen?.key if (!screenKey.isNullOrEmpty()) { @Suppress("CheckReturnValue") - addPreferenceScreen(screenKey) { screen.toProto(null) } + addPreferenceScreen(screenKey, null) { root = screen.toProto() } return actionTargetProto { key = screenKey } } } catch (e: Exception) { diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt index a595f42a573d..60f9c6bb92a3 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt @@ -40,11 +40,12 @@ import com.android.settingslib.metadata.SensitivityLevel.Companion.HIGH_SENSITIV import com.android.settingslib.metadata.SensitivityLevel.Companion.UNKNOWN_SENSITIVITY /** Request to set preference value. */ -data class PreferenceSetterRequest( - val screenKey: String, - val key: String, +class PreferenceSetterRequest( + screenKey: String, + args: Bundle?, + key: String, val value: PreferenceValueProto, -) +) : PreferenceCoordinate(screenKey, args, key) /** Result of preference setter request. */ @IntDef( @@ -121,7 +122,7 @@ class PreferenceSetterApiHandler( metricsLogger?.logSetterApi( application, callingUid, - PreferenceCoordinate(request.screenKey, request.key), + request, null, null, PreferenceSetterResult.UNSUPPORTED, @@ -130,7 +131,7 @@ class PreferenceSetterApiHandler( return PreferenceSetterResult.UNSUPPORTED } val screenMetadata = - PreferenceScreenRegistry.create(application, request.screenKey) ?: return notFound() + PreferenceScreenRegistry.create(application, request) ?: return notFound() val key = request.key val metadata = screenMetadata.getPreferenceHierarchy(application).find(key) ?: return notFound() @@ -199,7 +200,7 @@ class PreferenceSetterApiHandler( metricsLogger?.logSetterApi( application, callingUid, - PreferenceCoordinate(request.screenKey, request.key), + request, screenMetadata, metadata, result, @@ -235,6 +236,7 @@ object PreferenceSetterRequestCodec : MessageCodec<PreferenceSetterRequest> { override fun encode(data: PreferenceSetterRequest) = Bundle(3).apply { putString(SCREEN_KEY, data.screenKey) + putBundle(ARGS, data.args) putString(KEY, data.key) putByteArray(null, data.value.toByteArray()) } @@ -242,10 +244,12 @@ object PreferenceSetterRequestCodec : MessageCodec<PreferenceSetterRequest> { override fun decode(data: Bundle) = PreferenceSetterRequest( data.getString(SCREEN_KEY)!!, + data.getBundle(ARGS), data.getString(KEY)!!, PreferenceValueProto.parseFrom(data.getByteArray(null)!!), ) private const val SCREEN_KEY = "s" private const val KEY = "k" + private const val ARGS = "a" } diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoDsl.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoDsl.kt index adbe77318353..5f2a0d826407 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoDsl.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoDsl.kt @@ -19,6 +19,7 @@ package com.android.settingslib.graph import com.android.settingslib.graph.proto.BundleProto import com.android.settingslib.graph.proto.BundleProto.BundleValue import com.android.settingslib.graph.proto.IntentProto +import com.android.settingslib.graph.proto.ParameterizedPreferenceScreenProto import com.android.settingslib.graph.proto.PreferenceGroupProto import com.android.settingslib.graph.proto.PreferenceOrGroupProto import com.android.settingslib.graph.proto.PreferenceProto @@ -39,6 +40,12 @@ inline fun preferenceScreenProto( init: PreferenceScreenProto.Builder.() -> Unit ): PreferenceScreenProto = PreferenceScreenProto.newBuilder().also(init).build() +/** Kotlin DSL-style builder for [PreferenceScreenProto]. */ +inline fun parameterizedPreferenceScreenProto( + init: ParameterizedPreferenceScreenProto.Builder.() -> Unit +): ParameterizedPreferenceScreenProto = + ParameterizedPreferenceScreenProto.newBuilder().also(init).build() + /** Returns preference or null. */ val PreferenceOrGroupProto.preferenceOrNull get() = if (hasPreference()) preference else null diff --git a/packages/SettingsLib/IntroPreference/res/layout/settingslib_expressive_preference_intro.xml b/packages/SettingsLib/IntroPreference/res/layout/settingslib_expressive_preference_intro.xml index 43cf6aa09109..7adcbf6c6601 100644 --- a/packages/SettingsLib/IntroPreference/res/layout/settingslib_expressive_preference_intro.xml +++ b/packages/SettingsLib/IntroPreference/res/layout/settingslib_expressive_preference_intro.xml @@ -18,6 +18,8 @@ <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/entity_header" + android:layout_width="match_parent" + android:layout_height="wrap_content" style="@style/SettingsLibEntityHeader"> <LinearLayout diff --git a/packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt b/packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt index 38b641336547..69b75adea9d3 100644 --- a/packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt +++ b/packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt @@ -33,6 +33,9 @@ import javax.tools.Diagnostic /** Processor to gather preference screens annotated with `@ProvidePreferenceScreen`. */ class PreferenceScreenAnnotationProcessor : AbstractProcessor() { private val screens = mutableListOf<Screen>() + private val bundleType: TypeMirror by lazy { + processingEnv.elementUtils.getTypeElement("android.os.Bundle").asType() + } private val contextType: TypeMirror by lazy { processingEnv.elementUtils.getTypeElement("android.content.Context").asType() } @@ -83,19 +86,57 @@ class PreferenceScreenAnnotationProcessor : AbstractProcessor() { error("@$ANNOTATION_NAME must be added to $PREFERENCE_SCREEN_METADATA subclass", this) return } - val constructorType = getConstructorType() - if (constructorType == null) { + fun reportConstructorError() = error( - "Class must be an object, or has single public constructor that " + - "accepts no parameter or a Context parameter", + "Must have only one public constructor: constructor(), " + + "constructor(Context), constructor(Bundle) or constructor(Context, Bundle)", this, ) + val constructor = findConstructor() + if (constructor == null || constructor.parameters.size > 2) { + reportConstructorError() return } + val constructorHasContextParameter = constructor.hasParameter(0, contextType) + var index = if (constructorHasContextParameter) 1 else 0 val annotation = annotationMirrors.single { it.isElement(annotationElement) } val key = annotation.fieldValue<String>("value")!! val overlay = annotation.fieldValue<Boolean>("overlay") == true - screens.add(Screen(key, overlay, qualifiedName.toString(), constructorType)) + val parameterized = annotation.fieldValue<Boolean>("parameterized") == true + var parametersHasContextParameter = false + if (parameterized) { + val parameters = findParameters() + if (parameters == null) { + error("require a static 'parameters()' or 'parameters(Context)' method", this) + return + } + parametersHasContextParameter = parameters + if (constructor.hasParameter(index, bundleType)) { + index++ + } else { + error( + "Parameterized screen constructor must be" + + "constructor(Bundle) or constructor(Context, Bundle)", + this, + ) + return + } + } + if (index == constructor.parameters.size) { + screens.add( + Screen( + key, + overlay, + parameterized, + annotation.fieldValue<Boolean>("parameterizedMigration") == true, + qualifiedName.toString(), + constructorHasContextParameter, + parametersHasContextParameter, + ) + ) + } else { + reportConstructorError() + } } private fun codegen() { @@ -116,10 +157,15 @@ class PreferenceScreenAnnotationProcessor : AbstractProcessor() { screens.sort() processingEnv.filer.createSourceFile("$outputPkg.$outputClass").openWriter().use { it.write("package $outputPkg;\n\n") + it.write("import android.content.Context;\n") + it.write("import android.os.Bundle;\n") it.write("import $PACKAGE.FixedArrayMap;\n") it.write("import $PACKAGE.FixedArrayMap.OrderedInitializer;\n") - it.write("import $PACKAGE.$FACTORY;\n\n") - it.write("// Generated by annotation processor for @$ANNOTATION_NAME\n") + it.write("import $PACKAGE.$PREFERENCE_SCREEN_METADATA;\n") + it.write("import $PACKAGE.$FACTORY;\n") + it.write("import $PACKAGE.$PARAMETERIZED_FACTORY;\n") + it.write("import kotlinx.coroutines.flow.Flow;\n") + it.write("\n// Generated by annotation processor for @$ANNOTATION_NAME\n") it.write("public final class $outputClass {\n") it.write(" private $outputClass() {}\n\n") it.write(" public static FixedArrayMap<String, $FACTORY> $outputFun() {\n") @@ -127,10 +173,29 @@ class PreferenceScreenAnnotationProcessor : AbstractProcessor() { it.write(" return new FixedArrayMap<>($size, $outputClass::init);\n") it.write(" }\n\n") fun Screen.write() { - it.write(" screens.put(\"$key\", context -> new $klass(") - when (constructorType) { - ConstructorType.DEFAULT -> it.write("));") - ConstructorType.CONTEXT -> it.write("context));") + it.write(" screens.put(\"$key\", ") + if (parameterized) { + it.write("new $PARAMETERIZED_FACTORY() {\n") + it.write(" @Override public PreferenceScreenMetadata create") + it.write("(Context context, Bundle args) {\n") + it.write(" return new $klass(") + if (constructorHasContextParameter) it.write("context, ") + it.write("args);\n") + it.write(" }\n\n") + it.write(" @Override public Flow<Bundle> parameters(Context context) {\n") + it.write(" return $klass.parameters(") + if (parametersHasContextParameter) it.write("context") + it.write(");\n") + it.write(" }\n") + if (parameterizedMigration) { + it.write("\n @Override public boolean acceptEmptyArguments()") + it.write(" { return true; }\n") + } + it.write(" });") + } else { + it.write("context -> new $klass(") + if (constructorHasContextParameter) it.write("context") + it.write("));") } if (overlay) it.write(" // overlay") it.write("\n") @@ -159,7 +224,7 @@ class PreferenceScreenAnnotationProcessor : AbstractProcessor() { } private fun AnnotationMirror.isElement(element: TypeElement) = - processingEnv.typeUtils.isSameType(annotationType.asElement().asType(), element.asType()) + annotationType.asElement().asType().isSameType(element.asType()) @Suppress("UNCHECKED_CAST") private fun <T> AnnotationMirror.fieldValue(name: String): T? = field(name)?.value as? T @@ -171,7 +236,7 @@ class PreferenceScreenAnnotationProcessor : AbstractProcessor() { return null } - private fun TypeElement.getConstructorType(): ConstructorType? { + private fun TypeElement.findConstructor(): ExecutableElement? { var constructor: ExecutableElement? = null for (element in enclosedElements) { if (element.kind != ElementKind.CONSTRUCTOR) continue @@ -179,16 +244,30 @@ class PreferenceScreenAnnotationProcessor : AbstractProcessor() { if (constructor != null) return null constructor = element as ExecutableElement } - return constructor?.parameters?.run { - when { - isEmpty() -> ConstructorType.DEFAULT - size == 1 && processingEnv.typeUtils.isSameType(this[0].asType(), contextType) -> - ConstructorType.CONTEXT - else -> null - } + return constructor + } + + private fun TypeElement.findParameters(): Boolean? { + for (element in enclosedElements) { + if (element.kind != ElementKind.METHOD) continue + if (!element.modifiers.contains(Modifier.PUBLIC)) continue + if (!element.modifiers.contains(Modifier.STATIC)) continue + if (!element.simpleName.contentEquals("parameters")) return null + val parameters = (element as ExecutableElement).parameters + if (parameters.isEmpty()) return false + if (parameters.size == 1 && parameters[0].asType().isSameType(contextType)) return true + error("parameters method should have no parameter or a Context parameter", element) + return null } + return null } + private fun ExecutableElement.hasParameter(index: Int, typeMirror: TypeMirror) = + index < parameters.size && parameters[index].asType().isSameType(typeMirror) + + private fun TypeMirror.isSameType(typeMirror: TypeMirror) = + processingEnv.typeUtils.isSameType(this, typeMirror) + private fun warn(msg: CharSequence) = processingEnv.messager.printMessage(Diagnostic.Kind.WARNING, msg) @@ -198,8 +277,11 @@ class PreferenceScreenAnnotationProcessor : AbstractProcessor() { private data class Screen( val key: String, val overlay: Boolean, + val parameterized: Boolean, + val parameterizedMigration: Boolean, val klass: String, - val constructorType: ConstructorType, + val constructorHasContextParameter: Boolean, + val parametersHasContextParameter: Boolean, ) : Comparable<Screen> { override fun compareTo(other: Screen): Int { val diff = key.compareTo(other.key) @@ -207,17 +289,13 @@ class PreferenceScreenAnnotationProcessor : AbstractProcessor() { } } - private enum class ConstructorType { - DEFAULT, // default constructor with no parameter - CONTEXT, // constructor with a Context parameter - } - companion object { private const val PACKAGE = "com.android.settingslib.metadata" private const val ANNOTATION_NAME = "ProvidePreferenceScreen" private const val ANNOTATION = "$PACKAGE.$ANNOTATION_NAME" private const val PREFERENCE_SCREEN_METADATA = "PreferenceScreenMetadata" private const val FACTORY = "PreferenceScreenMetadataFactory" + private const val PARAMETERIZED_FACTORY = "PreferenceScreenMetadataParameterizedFactory" private const val OPTIONS_NAME = "ProvidePreferenceScreenOptions" private const val OPTIONS = "$PACKAGE.$OPTIONS_NAME" diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Annotations.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Annotations.kt index 4bed795ea760..449c78ce8965 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Annotations.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Annotations.kt @@ -22,14 +22,27 @@ package com.android.settingslib.metadata * The annotated class must satisfy either condition: * - the primary constructor has no parameter * - the primary constructor has a single [android.content.Context] parameter + * - (parameterized) the primary constructor has a single [android.os.Bundle] parameter to override + * [PreferenceScreenMetadata.arguments] + * - (parameterized) the primary constructor has a [android.content.Context] and a + * [android.os.Bundle] parameter to override [PreferenceScreenMetadata.arguments] * * @param value unique preference screen key * @param overlay if true, current annotated screen will overlay the screen that has identical key + * @param parameterized if true, the screen relies on additional arguments to build its content + * @param parameterizedMigration whether the parameterized screen was a normal screen, in which case + * `Bundle.EMPTY` will be passed as arguments to take care of backward compatibility + * @see PreferenceScreenMetadata */ @Retention(AnnotationRetention.SOURCE) @Target(AnnotationTarget.CLASS) @MustBeDocumented -annotation class ProvidePreferenceScreen(val value: String, val overlay: Boolean = false) +annotation class ProvidePreferenceScreen( + val value: String, + val overlay: Boolean = false, + val parameterized: Boolean = false, + val parameterizedMigration: Boolean = false, // effective only when parameterized is true +) /** * Provides options for [ProvidePreferenceScreen] annotation processor. diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Bundles.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Bundles.kt new file mode 100644 index 000000000000..a63576510aec --- /dev/null +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Bundles.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.metadata + +import android.content.Intent +import android.os.Bundle + +@Suppress("DEPRECATION") +fun Bundle?.contentEquals(other: Bundle?): Boolean { + if (this == null) return other == null + if (other == null) return false + if (keySet() != other.keySet()) return false + fun Any?.valueEquals(other: Any?) = + when (this) { + is Bundle -> other is Bundle && this.contentEquals(other) + is Intent -> other is Intent && this.filterEquals(other) + is BooleanArray -> other is BooleanArray && this contentEquals other + is ByteArray -> other is ByteArray && this contentEquals other + is CharArray -> other is CharArray && this contentEquals other + is DoubleArray -> other is DoubleArray && this contentEquals other + is FloatArray -> other is FloatArray && this contentEquals other + is IntArray -> other is IntArray && this contentEquals other + is LongArray -> other is LongArray && this contentEquals other + is ShortArray -> other is ShortArray && this contentEquals other + is Array<*> -> other is Array<*> && this contentDeepEquals other + else -> this == other + } + for (key in keySet()) { + if (!get(key).valueEquals(other.get(key))) return false + } + return true +} diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt index 63f1050df94e..e456a7f1aa1c 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt @@ -78,13 +78,8 @@ annotation class SensitivityLevel { /** Preference metadata that has a value persisted in datastore. */ interface PersistentPreference<T> : PreferenceMetadata { - /** - * The value type the preference is associated with. - * - * TODO(b/388167302): Remove the default implementation once all subclasses are migrated. - */ - val valueType: Class<T>? - get() = null + /** The value type the preference is associated with. */ + val valueType: Class<T> /** * Returns the key-value storage of the preference. diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceCoordinate.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceCoordinate.kt index 2dd736ae6083..ac08847b6002 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceCoordinate.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceCoordinate.kt @@ -16,26 +16,41 @@ package com.android.settingslib.metadata +import android.os.Bundle import android.os.Parcel import android.os.Parcelable /** * Coordinate to locate a preference. * - * Within an app, the preference screen key (unique among screens) plus preference key (unique on - * the screen) is used to locate a preference. + * Within an app, the preference screen coordinate (unique among screens) plus preference key + * (unique on the screen) is used to locate a preference. */ -data class PreferenceCoordinate(val screenKey: String, val key: String) : Parcelable { +open class PreferenceCoordinate : PreferenceScreenCoordinate { + val key: String - constructor(parcel: Parcel) : this(parcel.readString()!!, parcel.readString()!!) + constructor(screenKey: String, key: String) : this(screenKey, null, key) + + constructor(screenKey: String, args: Bundle?, key: String) : super(screenKey, args) { + this.key = key + } + + constructor(parcel: Parcel) : super(parcel) { + this.key = parcel.readString()!! + } override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(screenKey) + super.writeToParcel(parcel, flags) parcel.writeString(key) } override fun describeContents() = 0 + override fun equals(other: Any?) = + super.equals(other) && key == (other as PreferenceCoordinate).key + + override fun hashCode() = super.hashCode() xor key.hashCode() + companion object CREATOR : Parcelable.Creator<PreferenceCoordinate> { override fun createFromParcel(parcel: Parcel) = PreferenceCoordinate(parcel) @@ -43,3 +58,46 @@ data class PreferenceCoordinate(val screenKey: String, val key: String) : Parcel override fun newArray(size: Int) = arrayOfNulls<PreferenceCoordinate>(size) } } + +/** Coordinate to locate a preference screen. */ +open class PreferenceScreenCoordinate : Parcelable { + /** Unique preference screen key. */ + val screenKey: String + + /** Arguments to create parameterized preference screen. */ + val args: Bundle? + + constructor(screenKey: String, args: Bundle?) { + this.screenKey = screenKey + this.args = args + } + + constructor(parcel: Parcel) { + screenKey = parcel.readString()!! + args = parcel.readBundle(javaClass.classLoader) + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeString(screenKey) + parcel.writeBundle(args) + } + + override fun describeContents() = 0 + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as PreferenceScreenCoordinate + return screenKey == other.screenKey && args.contentEquals(other.args) + } + + // "args" is not included intentionally, otherwise we need to take care of array, etc. + override fun hashCode() = screenKey.hashCode() + + companion object CREATOR : Parcelable.Creator<PreferenceScreenCoordinate> { + + override fun createFromParcel(parcel: Parcel) = PreferenceScreenCoordinate(parcel) + + override fun newArray(size: Int) = arrayOfNulls<PreferenceScreenCoordinate>(size) + } +} diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt index 876f6152cccd..3bd051dee41d 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt @@ -17,6 +17,7 @@ package com.android.settingslib.metadata import android.content.Context +import android.os.Bundle /** A node in preference hierarchy that is associated with [PreferenceMetadata]. */ open class PreferenceHierarchyNode internal constructor(val metadata: PreferenceMetadata) { @@ -54,8 +55,14 @@ internal constructor(private val context: Context, metadata: PreferenceMetadata) * * @throws NullPointerException if screen is not registered to [PreferenceScreenRegistry] */ - operator fun String.unaryPlus() = - +PreferenceHierarchyNode(PreferenceScreenRegistry.create(context, this)!!) + operator fun String.unaryPlus() = addPreferenceScreen(this, null) + + /** + * Adds parameterized preference screen with given key (as a placeholder) to the hierarchy. + * + * @see String.unaryPlus + */ + infix fun String.args(args: Bundle) = createPreferenceScreenHierarchy(this, args) operator fun PreferenceHierarchyNode.unaryPlus() = also { children.add(it) } @@ -122,6 +129,14 @@ internal constructor(private val context: Context, metadata: PreferenceMetadata) } /** + * Adds parameterized preference screen with given key (as a placeholder) to the hierarchy. + * + * @see addPreferenceScreen + */ + fun addParameterizedScreen(screenKey: String, args: Bundle) = + addPreferenceScreen(screenKey, args) + + /** * Adds preference screen with given key (as a placeholder) to the hierarchy. * * This is mainly to support Android Settings overlays. OEMs might want to custom some of the @@ -132,11 +147,13 @@ internal constructor(private val context: Context, metadata: PreferenceMetadata) * * @throws NullPointerException if screen is not registered to [PreferenceScreenRegistry] */ - fun addPreferenceScreen(screenKey: String) { - children.add( - PreferenceHierarchy(context, PreferenceScreenRegistry.create(context, screenKey)!!) - ) - } + fun addPreferenceScreen(screenKey: String) = addPreferenceScreen(screenKey, null) + + private fun addPreferenceScreen(screenKey: String, args: Bundle?): PreferenceHierarchyNode = + createPreferenceScreenHierarchy(screenKey, args).also { children.add(it) } + + private fun createPreferenceScreenHierarchy(screenKey: String, args: Bundle?) = + PreferenceHierarchyNode(PreferenceScreenRegistry.create(context, screenKey, args)!!) /** Extensions to add more preferences to the hierarchy. */ operator fun PreferenceHierarchy.plusAssign(init: PreferenceHierarchy.() -> Unit) = init(this) diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenBindingKeyProvider.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenBindingKeyProvider.kt index 84014f191f68..4fd13ede6803 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenBindingKeyProvider.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenBindingKeyProvider.kt @@ -17,13 +17,20 @@ package com.android.settingslib.metadata import android.content.Context +import android.os.Bundle /** Provides the associated preference screen key for binding. */ interface PreferenceScreenBindingKeyProvider { /** Returns the associated preference screen key. */ fun getPreferenceScreenBindingKey(context: Context): String? + + /** Returns the arguments to build preference screen. */ + fun getPreferenceScreenBindingArgs(context: Context): Bundle? } /** Extra key to provide the preference screen key for binding. */ const val EXTRA_BINDING_SCREEN_KEY = "settingslib:binding_screen_key" + +/** Extra key to provide arguments for preference screen binding. */ +const val EXTRA_BINDING_SCREEN_ARGS = "settingslib:binding_screen_args" diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenMetadata.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenMetadata.kt index 850d4523e96e..7f1ded71e30a 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenMetadata.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenMetadata.kt @@ -18,12 +18,25 @@ package com.android.settingslib.metadata import android.content.Context import android.content.Intent +import android.os.Bundle import androidx.annotation.AnyThread import androidx.fragment.app.Fragment +import kotlinx.coroutines.flow.Flow -/** Metadata of preference screen. */ +/** + * Metadata of preference screen. + * + * For parameterized preference screen that relies on additional information (e.g. package name, + * language code) to build its content, the subclass must: + * - override [arguments] in constructor + * - add a static method `fun parameters(context: Context): List<Bundle>` (context is optional) to + * provide all possible arguments + */ @AnyThread interface PreferenceScreenMetadata : PreferenceMetadata { + /** Arguments to build the screen content. */ + val arguments: Bundle? + get() = null /** * The screen title resource, which precedes [getScreenTitle] if provided. @@ -65,7 +78,12 @@ interface PreferenceScreenMetadata : PreferenceMetadata { fun getLaunchIntent(context: Context, metadata: PreferenceMetadata?): Intent? = null } -/** Factory of [PreferenceScreenMetadata]. */ +/** + * Factory of [PreferenceScreenMetadata]. + * + * Annotation processor generates implementation of this interface based on + * [ProvidePreferenceScreen] when [ProvidePreferenceScreen.parameterized] is `false`. + */ fun interface PreferenceScreenMetadataFactory { /** @@ -75,3 +93,44 @@ fun interface PreferenceScreenMetadataFactory { */ fun create(context: Context): PreferenceScreenMetadata } + +/** + * Parameterized factory of [PreferenceScreenMetadata]. + * + * Annotation processor generates implementation of this interface based on + * [ProvidePreferenceScreen] when [ProvidePreferenceScreen.parameterized] is `true`. + */ +interface PreferenceScreenMetadataParameterizedFactory : PreferenceScreenMetadataFactory { + override fun create(context: Context) = create(context, Bundle.EMPTY) + + /** + * Creates a new [PreferenceScreenMetadata] with given arguments. + * + * @param context application context to create the PreferenceScreenMetadata + * @param args arguments to create the screen metadata, [Bundle.EMPTY] is reserved for the + * default case when screen is migrated from normal to parameterized + */ + fun create(context: Context, args: Bundle): PreferenceScreenMetadata + + /** + * Returns all possible arguments to create [PreferenceScreenMetadata]. + * + * Note that [Bundle.EMPTY] is a special arguments reserved for backward compatibility when a + * preference screen was a normal screen but migrated to parameterized screen later: + * 1. Set [ProvidePreferenceScreen.parameterizedMigration] to `true`, so that the generated + * [acceptEmptyArguments] will be `true`. + * 1. In the original [parameters] implementation, produce a [Bundle.EMPTY] for the default + * case. + * + * Do not use [Bundle.EMPTY] for other purpose. + */ + fun parameters(context: Context): Flow<Bundle> + + /** + * Returns true when the parameterized screen was a normal screen. + * + * The [PreferenceScreenMetadata] is expected to accept an empty arguments ([Bundle.EMPTY]) and + * take care of backward compatibility. + */ + fun acceptEmptyArguments(): Boolean = false +} diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt index c74b3151abb2..246310984db9 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt @@ -17,10 +17,13 @@ package com.android.settingslib.metadata import android.content.Context +import android.os.Bundle +import android.util.Log import com.android.settingslib.datastore.KeyValueStore /** Registry of all available preference screens in the app. */ object PreferenceScreenRegistry : ReadWritePermitProvider { + private const val TAG = "ScreenRegistry" /** Provider of key-value store. */ private lateinit var keyValueStoreProvider: KeyValueStoreProvider @@ -52,9 +55,28 @@ object PreferenceScreenRegistry : ReadWritePermitProvider { fun getKeyValueStore(context: Context, preference: PreferenceMetadata): KeyValueStore? = keyValueStoreProvider.getKeyValueStore(context, preference) - /** Creates [PreferenceScreenMetadata] of particular screen key. */ - fun create(context: Context, screenKey: String?): PreferenceScreenMetadata? = - screenKey?.let { preferenceScreenMetadataFactories[it]?.create(context.applicationContext) } + /** Creates [PreferenceScreenMetadata] of particular screen. */ + fun create(context: Context, screenCoordinate: PreferenceScreenCoordinate) = + create(context, screenCoordinate.screenKey, screenCoordinate.args) + + /** Creates [PreferenceScreenMetadata] of particular screen key with given arguments. */ + fun create(context: Context, screenKey: String?, args: Bundle?): PreferenceScreenMetadata? { + if (screenKey == null) return null + val factory = preferenceScreenMetadataFactories[screenKey] ?: return null + val appContext = context.applicationContext + if (factory is PreferenceScreenMetadataParameterizedFactory) { + if (args != null) return factory.create(appContext, args) + // In case the parameterized screen was a normal scree, it is expected to accept + // Bundle.EMPTY arguments and take care of backward compatibility. + if (factory.acceptEmptyArguments()) return factory.create(appContext) + Log.e(TAG, "screen $screenKey is parameterized but args is not provided") + return null + } else { + if (args == null) return factory.create(appContext) + Log.e(TAG, "screen $screenKey is not parameterized but args is provided") + return null + } + } /** * Sets the provider to check read write permit. Read and write requests are denied by default. diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt index 65fbe2b66e77..dbac17d4e8b8 100644 --- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt @@ -22,6 +22,7 @@ import androidx.preference.PreferenceCategory import androidx.preference.PreferenceScreen import androidx.preference.SwitchPreferenceCompat import androidx.preference.TwoStatePreference +import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_ARGS import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_KEY import com.android.settingslib.metadata.PreferenceMetadata import com.android.settingslib.metadata.PreferenceScreenMetadata @@ -35,9 +36,11 @@ interface PreferenceScreenBinding : PreferenceBinding { super.bind(preference, metadata) val context = preference.context val screenMetadata = metadata as PreferenceScreenMetadata + val extras = preference.extras // Pass the preference key to fragment, so that the fragment could find associated // preference screen registered in PreferenceScreenRegistry - preference.extras.putString(EXTRA_BINDING_SCREEN_KEY, preference.key) + extras.putString(EXTRA_BINDING_SCREEN_KEY, preference.key) + screenMetadata.arguments?.let { extras.putBundle(EXTRA_BINDING_SCREEN_ARGS, it) } if (preference is PreferenceScreen) { val screenTitle = screenMetadata.screenTitle preference.title = diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt index ffe181d0c350..02f91c1bb50b 100644 --- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt @@ -23,6 +23,7 @@ import android.util.Log import androidx.annotation.XmlRes import androidx.lifecycle.Lifecycle import androidx.preference.PreferenceScreen +import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_ARGS import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_KEY import com.android.settingslib.metadata.PreferenceScreenBindingKeyProvider import com.android.settingslib.metadata.PreferenceScreenRegistry @@ -89,13 +90,19 @@ open class PreferenceFragment : @XmlRes protected open fun getPreferenceScreenResId(context: Context): Int = 0 protected fun getPreferenceScreenCreator(context: Context): PreferenceScreenCreator? = - (PreferenceScreenRegistry.create(context, getPreferenceScreenBindingKey(context)) - as? PreferenceScreenCreator) + (PreferenceScreenRegistry.create( + context, + getPreferenceScreenBindingKey(context), + getPreferenceScreenBindingArgs(context), + ) as? PreferenceScreenCreator) ?.run { if (isFlagEnabled(context)) this else null } override fun getPreferenceScreenBindingKey(context: Context): String? = arguments?.getString(EXTRA_BINDING_SCREEN_KEY) + override fun getPreferenceScreenBindingArgs(context: Context): Bundle? = + arguments?.getBundle(EXTRA_BINDING_SCREEN_ARGS) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) preferenceScreenBindingHelper?.onCreate() diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt index 4a6a589cd3c9..1cb8005ddae0 100644 --- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt @@ -31,6 +31,7 @@ import com.android.settingslib.datastore.KeyValueStore import com.android.settingslib.datastore.KeyedDataObservable import com.android.settingslib.datastore.KeyedObservable import com.android.settingslib.datastore.KeyedObserver +import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_ARGS import com.android.settingslib.metadata.PersistentPreference import com.android.settingslib.metadata.PreferenceChangeReason import com.android.settingslib.metadata.PreferenceHierarchy @@ -227,14 +228,16 @@ class PreferenceScreenBindingHelper( /** Updates preference screen that has incomplete hierarchy. */ @JvmStatic fun bind(preferenceScreen: PreferenceScreen) { - PreferenceScreenRegistry.create(preferenceScreen.context, preferenceScreen.key)?.run { + val context = preferenceScreen.context + val args = preferenceScreen.peekExtras()?.getBundle(EXTRA_BINDING_SCREEN_ARGS) + PreferenceScreenRegistry.create(context, preferenceScreen.key, args)?.run { if (!hasCompleteHierarchy()) { val preferenceBindingFactory = (this as? PreferenceScreenCreator)?.preferenceBindingFactory ?: return bindRecursively( preferenceScreen, preferenceBindingFactory, - getPreferenceHierarchy(preferenceScreen.context), + getPreferenceHierarchy(context), ) } } diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt index 211b3bdaea70..88c4fe6bf188 100644 --- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt @@ -17,10 +17,12 @@ package com.android.settingslib.preference import android.content.Context +import android.os.Bundle import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager import androidx.preference.PreferenceScreen +import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_ARGS import com.android.settingslib.metadata.PreferenceScreenRegistry /** Factory to create preference screen. */ @@ -81,8 +83,12 @@ class PreferenceScreenFactory { * * The screen must be registered in [PreferenceScreenFactory] and provide a complete hierarchy. */ - fun createBindingScreen(context: Context, screenKey: String?): PreferenceScreen? { - val metadata = PreferenceScreenRegistry.create(context, screenKey) ?: return null + fun createBindingScreen( + context: Context, + screenKey: String?, + args: Bundle?, + ): PreferenceScreen? { + val metadata = PreferenceScreenRegistry.create(context, screenKey, args) ?: return null if (metadata is PreferenceScreenCreator && metadata.hasCompleteHierarchy()) { return metadata.createPreferenceScreen(this) } @@ -94,8 +100,9 @@ class PreferenceScreenFactory { @JvmStatic fun createBindingScreen(preference: Preference): PreferenceScreen? { val context = preference.context + val args = preference.peekExtras()?.getBundle(EXTRA_BINDING_SCREEN_ARGS) val preferenceScreenCreator = - (PreferenceScreenRegistry.create(context, preference.key) + (PreferenceScreenRegistry.create(context, preference.key, args) as? PreferenceScreenCreator) ?: return null if (!preferenceScreenCreator.hasCompleteHierarchy()) return null val factory = PreferenceScreenFactory(context) diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictionsProvider.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictionsProvider.kt index 3309faaa8db2..3a6327962dc2 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictionsProvider.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictionsProvider.kt @@ -32,6 +32,7 @@ import kotlinx.coroutines.flow.flowOn data class EnhancedConfirmation( val key: String, val packageName: String, + val isRestrictedSettingAllowed: Boolean? ) data class Restrictions( val userId: Int = UserHandle.myUserId(), @@ -92,6 +93,9 @@ internal class RestrictionsProviderImpl( } restrictions.enhancedConfirmation?.let { ec -> + if (ec.isRestrictedSettingAllowed == true) { + return NoRestricted + } RestrictedLockUtilsInternal .checkIfRequiresEnhancedConfirmation(context, ec.key, ec.packageName) diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt index 7466f95e3fb8..5580d2e3211b 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt @@ -155,7 +155,7 @@ internal fun <T : AppRecord> TogglePermissionAppListModel<T>.TogglePermissionApp } RestrictedSwitchPreference( model = switchModel, - restrictions = getRestrictions(userId, packageName), + restrictions = getRestrictions(userId, packageName, isAllowed()), ifBlockedByAdminOverrideCheckedValueTo = switchifBlockedByAdminOverrideCheckedValueTo, restrictionsProviderFactory = restrictionsProviderFactory, ) diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt index d2867af1eda6..771eb85ee21a 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt @@ -116,12 +116,15 @@ fun <T : AppRecord> TogglePermissionAppListModel<T>.isChangeableWithSystemUidChe fun <T : AppRecord> TogglePermissionAppListModel<T>.getRestrictions( userId: Int, packageName: String, + isRestrictedSettingAllowed: Boolean? ) = Restrictions( userId = userId, keys = switchRestrictionKeys, enhancedConfirmation = - enhancedConfirmationKey?.let { key -> EnhancedConfirmation(key, packageName) }, + enhancedConfirmationKey?.let { + key -> EnhancedConfirmation(key, packageName, isRestrictedSettingAllowed) + }, ) interface TogglePermissionAppListProvider { diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt index ec44d2af4ffa..bef2bdaaefaf 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt @@ -149,13 +149,14 @@ internal class TogglePermissionInternalAppListModel<T : AppRecord>( @Composable fun getSummary(record: T): () -> String { + val allowed = listModel.isAllowed(record) val restrictions = listModel.getRestrictions( userId = record.app.userId, packageName = record.app.packageName, + allowed() ) val restrictedMode by restrictionsProviderFactory.rememberRestrictedMode(restrictions) - val allowed = listModel.isAllowed(record) return RestrictedSwitchPreferenceModel.getSummary( context = context, summaryIfNoRestricted = { getSummaryIfNoRestricted(allowed()) }, diff --git a/packages/SettingsLib/TopIntroPreference/res/layout/settingslib_expressive_top_intro.xml b/packages/SettingsLib/TopIntroPreference/res/layout/settingslib_expressive_top_intro.xml index 834814ccbc6b..1c6b1ebd1535 100644 --- a/packages/SettingsLib/TopIntroPreference/res/layout/settingslib_expressive_top_intro.xml +++ b/packages/SettingsLib/TopIntroPreference/res/layout/settingslib_expressive_top_intro.xml @@ -18,7 +18,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" - android:paddingStart="?android:attr/listPreferredItemPaddingStart"> + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:filterTouchesWhenObscured="true"> <com.android.settingslib.widget.CollapsableTextView android:id="@+id/collapsable_text_view" diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedDropDownPreference.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedDropDownPreference.java index c36ade979d47..d3219c287f4b 100644 --- a/packages/SettingsLib/src/com/android/settingslib/RestrictedDropDownPreference.java +++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedDropDownPreference.java @@ -41,10 +41,23 @@ public class RestrictedDropDownPreference extends DropDownPreference implements * package. Marks the preference as disabled if so. * @param settingIdentifier The key identifying the setting * @param packageName the package to check the settingIdentifier for + * @param settingEnabled Whether the setting in question is enabled + */ + public void checkEcmRestrictionAndSetDisabled(@NonNull String settingIdentifier, + @NonNull String packageName, boolean settingEnabled) { + mHelper.checkEcmRestrictionAndSetDisabled(settingIdentifier, packageName, settingEnabled); + } + + /** + * Checks if the given setting is subject to Enhanced Confirmation Mode restrictions for this + * package. Marks the preference as disabled if so. + * TODO b/390196024: remove this and update all callers to use the "settingEnabled" version + * @param settingIdentifier The key identifying the setting + * @param packageName the package to check the settingIdentifier for */ public void checkEcmRestrictionAndSetDisabled(@NonNull String settingIdentifier, @NonNull String packageName) { - mHelper.checkEcmRestrictionAndSetDisabled(settingIdentifier, packageName); + mHelper.checkEcmRestrictionAndSetDisabled(settingIdentifier, packageName, false); } @Override diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedPreference.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedPreference.java index 332042a5c4f9..ec1b2b3e2589 100644 --- a/packages/SettingsLib/src/com/android/settingslib/RestrictedPreference.java +++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedPreference.java @@ -107,12 +107,25 @@ public class RestrictedPreference extends TwoTargetPreference implements /** * Checks if the given setting is subject to Enhanced Confirmation Mode restrictions for this * package. Marks the preference as disabled if so. + * TODO b/390196024: remove this and update all callers to use the "settingEnabled" version * @param settingIdentifier The key identifying the setting * @param packageName the package to check the settingIdentifier for */ public void checkEcmRestrictionAndSetDisabled(@NonNull String settingIdentifier, @NonNull String packageName) { - mHelper.checkEcmRestrictionAndSetDisabled(settingIdentifier, packageName); + mHelper.checkEcmRestrictionAndSetDisabled(settingIdentifier, packageName, false); + } + + /** + * Checks if the given setting is subject to Enhanced Confirmation Mode restrictions for this + * package. Marks the preference as disabled if so. + * @param settingIdentifier The key identifying the setting + * @param packageName the package to check the settingIdentifier for + * @param settingEnabled Whether the setting in question is enabled + */ + public void checkEcmRestrictionAndSetDisabled(@NonNull String settingIdentifier, + @NonNull String packageName, boolean settingEnabled) { + mHelper.checkEcmRestrictionAndSetDisabled(settingIdentifier, packageName, settingEnabled); } @Override diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java index 25628fba1b66..212e43aa4044 100644 --- a/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java +++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java @@ -204,16 +204,33 @@ public class RestrictedPreferenceHelper { * package. Marks the preference as disabled if so. * @param settingIdentifier The key identifying the setting * @param packageName the package to check the settingIdentifier for + * @param settingEnabled Whether the setting in question is enabled */ public void checkEcmRestrictionAndSetDisabled(@NonNull String settingIdentifier, - @NonNull String packageName) { + @NonNull String packageName, boolean settingEnabled) { updatePackageDetails(packageName, android.os.Process.INVALID_UID); + if (settingEnabled) { + setDisabledByEcm(null); + return; + } Intent intent = RestrictedLockUtilsInternal.checkIfRequiresEnhancedConfirmation( mContext, settingIdentifier, packageName); setDisabledByEcm(intent); } /** + * Checks if the given setting is subject to Enhanced Confirmation Mode restrictions for this + * package. Marks the preference as disabled if so. + * TODO b/390196024: remove this and update all callers to use the "settingEnabled" version + * @param settingIdentifier The key identifying the setting + * @param packageName the package to check the settingIdentifier for + */ + public void checkEcmRestrictionAndSetDisabled(@NonNull String settingIdentifier, + @NonNull String packageName) { + checkEcmRestrictionAndSetDisabled(settingIdentifier, packageName, false); + } + + /** * @return EnforcedAdmin if we have been passed the restriction in the xml. */ public EnforcedAdmin checkRestrictionEnforced() { diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedSelectorWithWidgetPreference.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedSelectorWithWidgetPreference.java index 573869db5073..f8f16a9dd63b 100644 --- a/packages/SettingsLib/src/com/android/settingslib/RestrictedSelectorWithWidgetPreference.java +++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedSelectorWithWidgetPreference.java @@ -134,10 +134,11 @@ public class RestrictedSelectorWithWidgetPreference extends SelectorWithWidgetPr * * @param settingIdentifier The key identifying the setting * @param packageName the package to check the settingIdentifier for + * @param settingEnabled Whether the setting in question is enabled */ - public void checkEcmRestrictionAndSetDisabled( - @NonNull String settingIdentifier, @NonNull String packageName) { - mHelper.checkEcmRestrictionAndSetDisabled(settingIdentifier, packageName); + public void checkEcmRestrictionAndSetDisabled(@NonNull String settingIdentifier, + @NonNull String packageName, boolean settingEnabled) { + mHelper.checkEcmRestrictionAndSetDisabled(settingIdentifier, packageName, settingEnabled); } @Override diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java index 0aac9a1104e9..a5fa6a854e97 100644 --- a/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java +++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java @@ -220,10 +220,23 @@ public class RestrictedSwitchPreference extends SwitchPreferenceCompat implement * package. Marks the preference as disabled if so. * @param settingIdentifier The key identifying the setting * @param packageName the package to check the settingIdentifier for + * @param settingEnabled Whether the setting in question is enabled */ public void checkEcmRestrictionAndSetDisabled(@NonNull String settingIdentifier, - @NonNull String packageName) { - mHelper.checkEcmRestrictionAndSetDisabled(settingIdentifier, packageName); + @NonNull String packageName, boolean settingEnabled) { + mHelper.checkEcmRestrictionAndSetDisabled(settingIdentifier, packageName, settingEnabled); + } + + /** + * Checks if the given setting is subject to Enhanced Confirmation Mode restrictions for this + * package. Marks the preference as disabled if so. + * TODO b/390196024: remove this and update all callers to use the "settingEnabled" version + * @param settingIdentifier The key identifying the setting + * @param packageName the package to check the settingIdentifier for + */ + public void checkEcmRestrictionAndSetDisabled(@NonNull String settingIdentifier, + @NonNull String packageName) { + mHelper.checkEcmRestrictionAndSetDisabled(settingIdentifier, packageName, false); } @Override diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java index 68e9fe703090..a00484ac28ab 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java @@ -1169,4 +1169,38 @@ public class BluetoothUtils { String metadataValue = getFastPairCustomizedField(bluetoothDevice, TEMP_BOND_TYPE); return Objects.equals(metadataValue, TEMP_BOND_DEVICE_METADATA_VALUE); } + + /** + * Set temp bond metadata to device + * + * @param device the BluetoothDevice to be marked as temp bond + * + * Note: It is a workaround since Bluetooth API is not ready. + * Avoid using this method if possible + */ + public static void setTemporaryBondMetadata(@Nullable BluetoothDevice device) { + if (device == null) return; + if (!Flags.enableTemporaryBondDevicesUi()) { + Log.d(TAG, "Skip setTemporaryBondMetadata, flag is disabled"); + return; + } + String fastPairCustomizedMeta = getStringMetaData(device, + METADATA_FAST_PAIR_CUSTOMIZED_FIELDS); + String fullContentWithTag = generateExpressionWithTag(TEMP_BOND_TYPE, + TEMP_BOND_DEVICE_METADATA_VALUE); + if (TextUtils.isEmpty(fastPairCustomizedMeta)) { + fastPairCustomizedMeta = fullContentWithTag; + } else { + String oldValue = extraTagValue(TEMP_BOND_TYPE, fastPairCustomizedMeta); + if (TextUtils.isEmpty(oldValue)) { + fastPairCustomizedMeta += fullContentWithTag; + } else { + fastPairCustomizedMeta = + fastPairCustomizedMeta.replace( + generateExpressionWithTag(TEMP_BOND_TYPE, oldValue), + fullContentWithTag); + } + } + device.setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, fastPairCustomizedMeta.getBytes()); + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java index 7d5eece6c30e..84156429809b 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java @@ -1087,8 +1087,9 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { private String generateRandomPassword() { String randomUUID = UUID.randomUUID().toString(); - // first 12 chars from xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx - return randomUUID.substring(0, 8) + randomUUID.substring(9, 13); + // first 16 chars from xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx + return randomUUID.substring(0, 8) + randomUUID.substring(9, 13) + randomUUID.substring(14, + 18); } private void registerContentObserver() { diff --git a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java index e3d7902f34b2..00973811dbf0 100644 --- a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java +++ b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java @@ -84,7 +84,11 @@ public final class ActionDisabledByAdminControllerFactory { return false; } DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class); - return ParentalControlsUtilsInternal.parentConsentRequired(context, dpm, + final SupervisionManager sm = + android.app.supervision.flags.Flags.deprecateDpmSupervisionApis() + ? context.getSystemService(SupervisionManager.class) + : null; + return ParentalControlsUtilsInternal.parentConsentRequired(context, dpm, sm, BiometricAuthenticator.TYPE_ANY_BIOMETRIC, new UserHandle(UserHandle.myUserId())); } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java index cafe19ff9a9b..7c46db96595f 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java @@ -22,9 +22,11 @@ import static com.android.settingslib.flags.Flags.FLAG_ENABLE_DETERMINING_ADVANC import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -44,6 +46,8 @@ import android.media.AudioDeviceAttributes; import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.net.Uri; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.util.Pair; @@ -109,6 +113,7 @@ public class BluetoothUtilsTest { + "</HEARABLE_CONTROL_SLICE_WITH_WIDTH>"; private static final String TEMP_BOND_METADATA = "<TEMP_BOND_TYPE>" + LE_AUDIO_SHARING_METADATA + "</TEMP_BOND_TYPE>"; + private static final String FAKE_TEMP_BOND_METADATA = "<TEMP_BOND_TYPE>fake</TEMP_BOND_TYPE>"; private static final String TEST_EXCLUSIVE_MANAGER_PACKAGE = "com.test.manager"; private static final String TEST_EXCLUSIVE_MANAGER_COMPONENT = "com.test.manager/.component"; private static final int TEST_BROADCAST_ID = 25; @@ -1348,4 +1353,34 @@ public class BluetoothUtilsTest { assertThat(BluetoothUtils.isTemporaryBondDevice(mBluetoothDevice)).isTrue(); } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI) + public void setTemporaryBondDevice_flagOff_doNothing() { + when(mBluetoothDevice.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) + .thenReturn(new byte[]{}); + BluetoothUtils.setTemporaryBondMetadata(mBluetoothDevice); + verify(mBluetoothDevice, never()).setMetadata(eq(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS), + any()); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI) + public void setTemporaryBondDevice_flagOn_setCorrectValue() { + when(mBluetoothDevice.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) + .thenReturn(new byte[]{}); + BluetoothUtils.setTemporaryBondMetadata(mBluetoothDevice); + verify(mBluetoothDevice).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, + TEMP_BOND_METADATA.getBytes()); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI) + public void setTemporaryBondDevice_flagOff_replaceAndSetCorrectValue() { + when(mBluetoothDevice.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) + .thenReturn(FAKE_TEMP_BOND_METADATA.getBytes()); + BluetoothUtils.setTemporaryBondMetadata(mBluetoothDevice); + verify(mBluetoothDevice).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, + TEMP_BOND_METADATA.getBytes()); + } } diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index d936a5c699c7..27a3cf1198b2 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -291,5 +291,6 @@ public class SecureSettings { Settings.Secure.FACE_KEYGUARD_ENABLED, Settings.Secure.FINGERPRINT_APP_ENABLED, Settings.Secure.FINGERPRINT_KEYGUARD_ENABLED, + Settings.Secure.DUAL_SHADE, }; } diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index 919c3c4721f2..8dca39fdc107 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -460,5 +460,6 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.FACE_KEYGUARD_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.FINGERPRINT_APP_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.FINGERPRINT_KEYGUARD_ENABLED, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.DUAL_SHADE, BOOLEAN_VALIDATOR); } } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java index 14b2dfe414a4..fc402d45c3ec 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java @@ -89,6 +89,7 @@ import java.io.OutputStream; import java.time.DateTimeException; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Objects; @@ -97,7 +98,6 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; -import java.util.HashMap; import java.util.zip.CRC32; /** @@ -1753,8 +1753,8 @@ public class SettingsBackupAgent extends BackupAgentHelper { if (previousDensity == null || previousDensity != newDensity) { // From nothing to something is a change. - DisplayDensityConfiguration.setForcedDisplayDensity( - Display.DEFAULT_DISPLAY, newDensity); + DisplayDensityConfiguration.setForcedDisplayDensity(getBaseContext(), + info -> info.type == Display.TYPE_INTERNAL, newDensity); } } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index c47bf628002d..7c588b3834a5 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -4072,7 +4072,7 @@ public class SettingsProvider extends ContentProvider { @VisibleForTesting final class UpgradeController { - private static final int SETTINGS_VERSION = 226; + private static final int SETTINGS_VERSION = 227; private final int mUserId; @@ -6266,6 +6266,51 @@ public class SettingsProvider extends ContentProvider { currentVersion = 226; } + // Version 226: Introduces dreaming while postured setting and migrates user from + // docked dream trigger to postured dream trigger. + if (currentVersion == 226) { + final SettingsState secureSettings = getSecureSettingsLocked(userId); + final Setting dreamOnDock = secureSettings.getSettingLocked( + Secure.SCREENSAVER_ACTIVATE_ON_DOCK); + final Setting dreamsEnabled = secureSettings.getSettingLocked( + Secure.SCREENSAVER_ENABLED); + final boolean dreamOnPosturedDefault = getContext().getResources().getBoolean( + com.android.internal.R.bool.config_dreamsActivatedOnPosturedByDefault); + final boolean dreamsEnabledByDefault = getContext().getResources().getBoolean( + com.android.internal.R.bool.config_dreamsEnabledByDefault); + + if (dreamOnPosturedDefault && !dreamOnDock.isNull() + && dreamOnDock.getValue().equals("1")) { + // Disable dock activation and enable postured. + secureSettings.insertSettingOverrideableByRestoreLocked( + Secure.SCREENSAVER_ACTIVATE_ON_DOCK, + "0", + null, + true, + SettingsState.SYSTEM_PACKAGE_NAME); + secureSettings.insertSettingOverrideableByRestoreLocked( + Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, + "1", + null, + true, + SettingsState.SYSTEM_PACKAGE_NAME); + + // Disable dreams overall, so user doesn't start to unexpectedly see dreams + // enabled when postured. + if (!dreamsEnabledByDefault && !dreamsEnabled.isNull() + && dreamsEnabled.getValue().equals("1")) { + secureSettings.insertSettingOverrideableByRestoreLocked( + Secure.SCREENSAVER_ENABLED, + "0", + null, + true, + SettingsState.SYSTEM_PACKAGE_NAME); + } + } + + currentVersion = 227; + } + // vXXX: Add new settings above this point. if (currentVersion != newVersion) { diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index 9aad5d5f8367..246aa7158cab 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -178,6 +178,7 @@ public class SettingsBackupTest { Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, + Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_EXPERIENCE_FEATURES, Settings.Global.DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES, Settings.Global.DEVELOPMENT_FORCE_RTL, Settings.Global.DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW, diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index b53198d8ae98..72ae76a45cac 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -392,6 +392,9 @@ <!-- To be able to decipher default applications for certain roles in shortcut helper --> <uses-permission android:name="android.permission.MANAGE_DEFAULT_APPLICATIONS" /> + <!-- To be able to set unrestricted system gesture exclusion rects --> + <uses-permission android:name="android.permission.SET_UNRESTRICTED_GESTURE_EXCLUSION"/> + <protected-broadcast android:name="com.android.settingslib.action.REGISTER_SLICE_RECEIVER" /> <protected-broadcast android:name="com.android.settingslib.action.UNREGISTER_SLICE_RECEIVER" /> <protected-broadcast android:name="com.android.settings.flashlight.action.FLASHLIGHT_CHANGED" /> @@ -548,10 +551,13 @@ android:exported="true" /> <service android:name=".wallpapers.GradientColorWallpaper" - android:featureFlag="android.app.enable_connected_displays_wallpaper" android:singleUser="true" android:permission="android.permission.BIND_WALLPAPER" - android:exported="true" /> + android:exported="true"> + <meta-data android:name="android.service.wallpaper" + android:resource="@xml/gradient_color_wallpaper"> + </meta-data> + </service> <activity android:name=".tuner.TunerActivity" android:enabled="false" @@ -1165,5 +1171,16 @@ android:exported="false" /> + <service + android:name="com.google.android.systemui.lowlightclock.LowLightClockDreamService" + android:enabled="false" + android:exported="false" + android:directBootAware="true" + android:permission="android.permission.BIND_DREAM_SERVICE"> + <intent-filter> + <action android:name="android.service.dreams.DreamService" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + </service> </application> </manifest> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/aconfig/Android.bp b/packages/SystemUI/accessibility/accessibilitymenu/aconfig/Android.bp index 0ff856e0b91e..1d74774c7c11 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/aconfig/Android.bp +++ b/packages/SystemUI/accessibility/accessibilitymenu/aconfig/Android.bp @@ -5,7 +5,7 @@ package { aconfig_declarations { name: "com_android_a11y_menu_flags", package: "com.android.systemui.accessibility.accessibilitymenu", - container: "system", + container: "system_ext", srcs: [ "accessibility.aconfig", ], diff --git a/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig b/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig index 6d790114803a..bdf6d4242e68 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig +++ b/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig @@ -1,5 +1,5 @@ package: "com.android.systemui.accessibility.accessibilitymenu" -container: "system" +container: "system_ext" # NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors. diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 903c05526122..f0c855753292 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -186,6 +186,16 @@ flag { } flag { + name: "notifications_pinned_hun_in_shade" + namespace: "systemui" + description: "Fixes displaying pinned HUNs in the Shade, making sure that their y and z positions are correct." + bug: "385041854" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "pss_app_selector_recents_split_screen" namespace: "systemui" description: "Allows recent apps selected for partial screenshare to be launched in split screen mode" @@ -227,6 +237,13 @@ flag { } flag { + name: "notification_bundle_ui" + namespace: "systemui" + description: "Three-level group UI for notification bundles" + bug: "367996732" +} + +flag { name: "notification_background_tint_optimization" namespace: "systemui" description: "Re-enable the codepath that removed tinting of notifications when the" @@ -524,6 +541,26 @@ flag { } flag { + name: "face_scanning_animation_npe_fix" + namespace: "systemui" + description: "Fix for the face scanning animation NPE" + bug: "392032258" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "indication_text_a11y_fix" + namespace: "systemui" + description: "add double shadow to the indication text at the bottom of the lock screen" + bug: "349297241" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "rest_to_unlock" namespace: "systemui" description: "Require prolonged touch for fingerprint authentication" @@ -596,13 +633,6 @@ flag { } flag { - name: "quick_settings_visual_haptics_longpress" - namespace: "systemui" - description: "Enable special visual and haptic effects for quick settings tiles with long-press actions" - bug: "229856884" -} - -flag { name: "switch_user_on_bg" namespace: "systemui" description: "Does user switching on a background thread" @@ -1832,6 +1862,13 @@ flag { } flag { + name: "double_tap_to_sleep" + namespace: "systemui" + description: "Enable Double Tap to Sleep" + bug: "385194612" +} + +flag{ name: "gsf_bouncer" namespace: "systemui" description: "Applies GSF font styles to Bouncer surfaces." @@ -1915,16 +1952,6 @@ flag { } flag { - name: "stabilize_heads_up_group" - namespace: "systemui" - description: "Stabilize heads up groups in VisualStabilityCoordinator" - bug: "381864715" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "magnetic_notification_horizontal_swipe" namespace: "systemui" description: "Add support for magnetic behavior on horizontal notification swipes." @@ -1942,6 +1969,13 @@ flag { } flag { + name: "permission_helper_ui_rich_ongoing" + namespace: "systemui" + description: "[RONs] Guards inline permission helper for demoting RONs" + bug: "379186372" +} + +flag { name: "aod_ui_rich_ongoing" namespace: "systemui" description: "Show a rich ongoing notification on the always-on display (depends on ui_rich_ongoing)" @@ -1956,4 +1990,14 @@ flag { metadata { purpose: PURPOSE_BUGFIX } -}
\ No newline at end of file +} + +flag { + name: "shade_launch_accessibility" + namespace: "systemui" + description: "Intercept accessibility focus events for the Shade during launch animations to avoid stray TalkBack events." + bug: "379222226" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt index 78ae4af258fc..9545bda80b2d 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt @@ -1,9 +1,12 @@ package com.android.systemui.animation -private const val TAG_WGHT = "wght" -private const val TAG_WDTH = "wdth" -private const val TAG_OPSZ = "opsz" -private const val TAG_ROND = "ROND" +object GSFAxes { + const val WEIGHT = "wght" + const val WIDTH = "wdth" + const val SLANT = "slnt" + const val ROUND = "ROND" + const val OPTICAL_SIZE = "opsz" +} class FontVariationUtils { private var mWeight = -1 @@ -21,7 +24,7 @@ class FontVariationUtils { weight: Int = -1, width: Int = -1, opticalSize: Int = -1, - roundness: Int = -1 + roundness: Int = -1, ): String { isUpdated = false if (weight >= 0 && mWeight != weight) { @@ -43,16 +46,20 @@ class FontVariationUtils { } var resultString = "" if (mWeight >= 0) { - resultString += "'$TAG_WGHT' $mWeight" + resultString += "'${GSFAxes.WEIGHT}' $mWeight" } if (mWidth >= 0) { - resultString += (if (resultString.isBlank()) "" else ", ") + "'$TAG_WDTH' $mWidth" + resultString += + (if (resultString.isBlank()) "" else ", ") + "'${GSFAxes.WIDTH}' $mWidth" } if (mOpticalSize >= 0) { - resultString += (if (resultString.isBlank()) "" else ", ") + "'$TAG_OPSZ' $mOpticalSize" + resultString += + (if (resultString.isBlank()) "" else ", ") + + "'${GSFAxes.OPTICAL_SIZE}' $mOpticalSize" } if (mRoundness >= 0) { - resultString += (if (resultString.isBlank()) "" else ", ") + "'$TAG_ROND' $mRoundness" + resultString += + (if (resultString.isBlank()) "" else ", ") + "'${GSFAxes.ROUND}' $mRoundness" } return if (isUpdated) resultString else "" } diff --git a/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt b/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt index e74b185851c4..1bb8ae5019fb 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt @@ -40,7 +40,7 @@ import kotlinx.coroutines.CoroutineScope @Composable @OptIn(ExperimentalMaterial3ExpressiveApi::class) fun rememberOffsetOverscrollEffect( - animationSpec: AnimationSpec<Float> = MaterialTheme.motionScheme.defaultSpatialSpec() + animationSpec: AnimationSpec<Float> = MaterialTheme.motionScheme.slowSpatialSpec() ): OffsetOverscrollEffect { val animationScope = rememberCoroutineScope() return remember(animationScope, animationSpec) { @@ -51,7 +51,7 @@ fun rememberOffsetOverscrollEffect( @Composable @OptIn(ExperimentalMaterial3ExpressiveApi::class) fun rememberOffsetOverscrollEffectFactory( - animationSpec: AnimationSpec<Float> = MaterialTheme.motionScheme.defaultSpatialSpec() + animationSpec: AnimationSpec<Float> = MaterialTheme.motionScheme.slowSpatialSpec() ): OverscrollFactory { val animationScope = rememberCoroutineScope() return remember(animationScope, animationSpec) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt index c7930549abe8..44c375d6ac5e 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt @@ -272,9 +272,8 @@ private fun calculateNumCellsWidth(width: Dp) = } private fun calculateNumCellsHeight(height: Dp) = - // See https://developer.android.com/develop/ui/views/layout/use-window-size-classes when { - height >= 900.dp -> 3 + height >= 1000.dp -> 3 height >= 480.dp -> 2 else -> 1 } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/MediaCarouselSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/MediaCarouselSection.kt index 0ff567bf90ad..d8b3f742b447 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/MediaCarouselSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/MediaCarouselSection.kt @@ -19,12 +19,11 @@ package com.android.systemui.keyguard.ui.composable.section import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.dimensionResource -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.ContentScope import com.android.systemui.keyguard.ui.viewmodel.KeyguardMediaViewModel +import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.media.controls.ui.composable.MediaCarousel import com.android.systemui.media.controls.ui.controller.MediaCarouselController import com.android.systemui.media.controls.ui.view.MediaHost @@ -38,7 +37,7 @@ class MediaCarouselSection constructor( private val mediaCarouselController: MediaCarouselController, @param:Named(MediaModule.KEYGUARD) private val mediaHost: MediaHost, - private val keyguardMediaViewModel: KeyguardMediaViewModel, + private val keyguardMediaViewModelFactory: KeyguardMediaViewModel.Factory, ) { @Composable @@ -46,7 +45,10 @@ constructor( isShadeLayoutWide: Boolean, modifier: Modifier = Modifier, ) { - val isMediaVisible by keyguardMediaViewModel.isMediaVisible.collectAsStateWithLifecycle() + val viewModel = + rememberViewModel(traceName = "KeyguardMediaCarousel") { + keyguardMediaViewModelFactory.create() + } val horizontalPadding = if (isShadeLayoutWide) { dimensionResource(id = R.dimen.notification_side_paddings) @@ -55,7 +57,7 @@ constructor( dimensionResource(id = R.dimen.notification_panel_margin_horizontal) } MediaCarousel( - isVisible = isMediaVisible, + isVisible = viewModel.isMediaVisible, mediaHost = mediaHost, modifier = modifier.fillMaxWidth().padding(horizontal = horizontalPadding), carouselController = mediaCarouselController, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt index d7545cb07849..22556777c40f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt @@ -26,7 +26,6 @@ import com.android.systemui.scene.ui.composable.transitions.lockscreenToGoneTran import com.android.systemui.scene.ui.composable.transitions.lockscreenToQuickSettingsTransition import com.android.systemui.scene.ui.composable.transitions.lockscreenToShadeTransition import com.android.systemui.scene.ui.composable.transitions.lockscreenToSplitShadeTransition -import com.android.systemui.scene.ui.composable.transitions.notificationsShadeToQuickSettingsShadeTransition import com.android.systemui.scene.ui.composable.transitions.shadeToQuickSettingsTransition import com.android.systemui.scene.ui.composable.transitions.toNotificationsShadeTransition import com.android.systemui.scene.ui.composable.transitions.toQuickSettingsShadeTransition @@ -172,13 +171,6 @@ val SceneContainerTransitions = transitions { toQuickSettingsShadeTransition() } from( - Overlays.NotificationsShade, - to = Overlays.QuickSettingsShade, - cuj = Cuj.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE, - ) { - notificationsShadeToQuickSettingsShadeTransition() - } - from( Scenes.Gone, to = Overlays.NotificationsShade, key = SlightlyFasterShadeCollapse, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt index bdd0da9ce4a4..4e10ff689b19 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt @@ -67,11 +67,10 @@ import com.android.systemui.Flags import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.ui.compose.Icon import com.android.systemui.compose.modifiers.sysuiResTag -import com.android.systemui.haptics.slider.SeekableSliderTrackerConfig -import com.android.systemui.haptics.slider.SliderHapticFeedbackConfig import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.res.R +import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderState import kotlin.math.round import kotlinx.coroutines.flow.distinctUntilChanged @@ -103,6 +102,10 @@ fun VolumeSlider( } val value by valueState(state) + val interactionSource = remember { MutableInteractionSource() } + val hapticsViewModel: SliderHapticsViewModel? = + setUpHapticsViewModel(value, state.valueRange, interactionSource, hapticsViewModelFactory) + Column(modifier = modifier.animateContentSize(), verticalArrangement = Arrangement.Top) { Row( horizontalArrangement = Arrangement.spacedBy(12.dp), @@ -127,8 +130,14 @@ fun VolumeSlider( Slider( value = value, valueRange = state.valueRange, - onValueChange = onValueChange, - onValueChangeFinished = onValueChangeFinished, + onValueChange = { newValue -> + hapticsViewModel?.addVelocityDataPoint(newValue) + onValueChange(newValue) + }, + onValueChangeFinished = { + hapticsViewModel?.onValueChangeEnded() + onValueChangeFinished?.invoke() + }, enabled = state.isEnabled, modifier = Modifier.height(40.dp) @@ -210,41 +219,8 @@ private fun LegacyVolumeSlider( ) { val value by valueState(state) val interactionSource = remember { MutableInteractionSource() } - val sliderStepSize = 1f / (state.valueRange.endInclusive - state.valueRange.start) val hapticsViewModel: SliderHapticsViewModel? = - hapticsViewModelFactory?.let { - rememberViewModel(traceName = "SliderHapticsViewModel") { - it.create( - interactionSource, - state.valueRange, - Orientation.Horizontal, - SliderHapticFeedbackConfig( - lowerBookendScale = 0.2f, - progressBasedDragMinScale = 0.2f, - progressBasedDragMaxScale = 0.5f, - deltaProgressForDragThreshold = 0f, - additionalVelocityMaxBump = 0.2f, - maxVelocityToScale = 0.1f, /* slider progress(from 0 to 1) per sec */ - sliderStepSize = sliderStepSize, - ), - SeekableSliderTrackerConfig( - lowerBookendThreshold = 0f, - upperBookendThreshold = 1f, - ), - ) - } - } - var lastDiscreteStep by remember { mutableFloatStateOf(round(value)) } - LaunchedEffect(value) { - snapshotFlow { value } - .map { round(it) } - .filter { it != lastDiscreteStep } - .distinctUntilChanged() - .collect { discreteStep -> - lastDiscreteStep = discreteStep - hapticsViewModel?.onValueChange(discreteStep) - } - } + setUpHapticsViewModel(value, state.valueRange, interactionSource, hapticsViewModelFactory) PlatformSlider( modifier = @@ -357,3 +333,36 @@ private fun SliderIcon( content = { Icon(modifier = Modifier.size(24.dp), icon = icon) }, ) } + +@Composable +fun setUpHapticsViewModel( + value: Float, + valueRange: ClosedFloatingPointRange<Float>, + interactionSource: MutableInteractionSource, + hapticsViewModelFactory: SliderHapticsViewModel.Factory?, +): SliderHapticsViewModel? { + return hapticsViewModelFactory?.let { + rememberViewModel(traceName = "SliderHapticsViewModel") { + it.create( + interactionSource, + valueRange, + Orientation.Horizontal, + VolumeHapticsConfigsProvider.sliderHapticFeedbackConfig(valueRange), + VolumeHapticsConfigsProvider.seekableSliderTrackerConfig, + ) + } + .also { hapticsViewModel -> + var lastDiscreteStep by remember { mutableFloatStateOf(round(value)) } + LaunchedEffect(value) { + snapshotFlow { value } + .map { round(it) } + .filter { it != lastDiscreteStep } + .distinctUntilChanged() + .collect { discreteStep -> + lastDiscreteStep = discreteStep + hapticsViewModel.onValueChange(discreteStep) + } + } + } + } +} 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 ab3f6396e5c0..70ff47baa7a9 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 @@ -127,7 +127,14 @@ internal class DraggableHandler( directionChangeSlop = layoutImpl.directionChangeSlop, ) - return createSwipeAnimation(layoutImpl, result, isUpOrLeft, orientation, gestureContext) + return createSwipeAnimation( + layoutImpl, + result, + isUpOrLeft, + orientation, + gestureContext, + layoutImpl.decayAnimationSpec, + ) } private fun resolveSwipeSource(startedPosition: Offset): SwipeSource.Resolved? { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt index 96d68ff03acd..41279d338896 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt @@ -18,19 +18,26 @@ package com.android.compose.animation.scene import androidx.activity.BackEventCompat import androidx.activity.compose.PredictiveBackHandler +import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.snap import androidx.compose.foundation.gestures.Orientation import androidx.compose.runtime.Composable +import androidx.compose.ui.util.fastCoerceIn import com.android.compose.animation.scene.UserActionResult.ChangeScene import com.android.compose.animation.scene.UserActionResult.HideOverlay import com.android.compose.animation.scene.UserActionResult.ReplaceByOverlay import com.android.compose.animation.scene.UserActionResult.ShowOverlay -import com.android.compose.animation.scene.transition.animateProgress import com.android.mechanics.ProvidedGestureContext import com.android.mechanics.spec.InputDirection +import kotlin.coroutines.cancellation.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch @Composable internal fun PredictiveBackHandler( @@ -63,6 +70,7 @@ internal fun PredictiveBackHandler( distance = 1f, gestureContext = ProvidedGestureContext(dragOffset = 0f, direction = InputDirection.Max), + decayAnimationSpec = layoutImpl.decayAnimationSpec, ) animateProgress( @@ -93,3 +101,63 @@ private fun UserActionResult.copy( is ReplaceByOverlay -> copy(transitionKey = transitionKey) } } + +private suspend fun <T : ContentKey> animateProgress( + state: MutableSceneTransitionLayoutStateImpl, + animation: SwipeAnimation<T>, + progress: Flow<Float>, + commitSpec: AnimationSpec<Float>?, + cancelSpec: AnimationSpec<Float>?, + animationScope: CoroutineScope? = null, +) { + suspend fun animateOffset(targetContent: T, spec: AnimationSpec<Float>?) { + if (state.transitionState != animation.contentTransition || animation.isAnimatingOffset()) { + return + } + + animation.animateOffset( + initialVelocity = 0f, + targetContent = targetContent, + + // Important: we have to specify a spec that correctly animates *progress* (low + // visibility threshold) and not *offset* (higher visibility threshold). + spec = spec ?: animation.contentTransition.transformationSpec.progressSpec, + ) + } + + coroutineScope { + val collectionJob = launch { + try { + progress.collectLatest { progress -> + // Progress based animation should never overscroll given that the + // absoluteDistance exposed to overscroll builders is always 1f and will not + // lead to any noticeable transformation. + animation.dragOffset = progress.fastCoerceIn(0f, 1f) + } + + // Transition committed. + animateOffset(animation.toContent, commitSpec) + } catch (e: CancellationException) { + // Transition cancelled. + animateOffset(animation.fromContent, cancelSpec) + } + } + + // Start the transition. + animationScope?.launch { startTransition(state, animation, collectionJob) } + ?: startTransition(state, animation, collectionJob) + } +} + +private suspend fun <T : ContentKey> startTransition( + state: MutableSceneTransitionLayoutStateImpl, + animation: SwipeAnimation<T>, + progressCollectionJob: Job, +) { + state.startTransition(animation.contentTransition) + // The transition is done. Cancel the collection in case the transition was finished + // because it was interrupted by another transition. + if (progressCollectionJob.isActive) { + progressCollectionJob.cancel() + } +} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt index f6d40eef53a3..72bb82bd41bb 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt @@ -17,6 +17,7 @@ package com.android.compose.animation.scene import androidx.annotation.FloatRange +import androidx.compose.animation.rememberSplineBasedDecay import androidx.compose.foundation.LocalOverscrollFactory import androidx.compose.foundation.OverscrollEffect import androidx.compose.foundation.OverscrollFactory @@ -747,6 +748,7 @@ internal fun SceneTransitionLayoutForTesting( val layoutDirection = LocalLayoutDirection.current val defaultEffectFactory = checkNotNull(LocalOverscrollFactory.current) val animationScope = rememberCoroutineScope() + val decayAnimationSpec = rememberSplineBasedDecay<Float>() val layoutImpl = remember { SceneTransitionLayoutImpl( state = state as MutableSceneTransitionLayoutStateImpl, @@ -762,6 +764,7 @@ internal fun SceneTransitionLayoutForTesting( lookaheadScope = lookaheadScope, directionChangeSlop = directionChangeSlop, defaultEffectFactory = defaultEffectFactory, + decayAnimationSpec = decayAnimationSpec, ) .also { onLayoutImpl?.invoke(it) } } @@ -801,6 +804,7 @@ internal fun SceneTransitionLayoutForTesting( layoutImpl.swipeSourceDetector = swipeSourceDetector layoutImpl.swipeDetector = swipeDetector layoutImpl.transitionInterceptionThreshold = transitionInterceptionThreshold + layoutImpl.decayAnimationSpec = decayAnimationSpec } layoutImpl.Content(modifier) 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 585da0633131..53996d25afdb 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 @@ -17,6 +17,7 @@ package com.android.compose.animation.scene import androidx.annotation.VisibleForTesting +import androidx.compose.animation.core.DecayAnimationSpec import androidx.compose.foundation.OverscrollFactory import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.Orientation @@ -82,6 +83,7 @@ internal class SceneTransitionLayoutImpl( internal var swipeSourceDetector: SwipeSourceDetector, internal var swipeDetector: SwipeDetector, internal var transitionInterceptionThreshold: Float, + internal var decayAnimationSpec: DecayAnimationSpec<Float>, builder: SceneTransitionLayoutScope<InternalContentScope>.() -> Unit, /** diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt index 3bd37ad018b0..cb0d33cf5205 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt @@ -21,6 +21,8 @@ package com.android.compose.animation.scene import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.animation.core.DecayAnimationSpec +import androidx.compose.animation.core.calculateTargetValue import androidx.compose.foundation.gestures.Orientation import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.runtime.getValue @@ -42,6 +44,7 @@ internal fun createSwipeAnimation( orientation: Orientation, distance: Float, gestureContext: MutableDragOffsetGestureContext, + decayAnimationSpec: DecayAnimationSpec<Float>, ): SwipeAnimation<*> { return createSwipeAnimation( layoutState, @@ -53,6 +56,7 @@ internal fun createSwipeAnimation( error("Computing contentForUserActions requires a SceneTransitionLayoutImpl") }, gestureContext = gestureContext, + decayAnimationSpec = decayAnimationSpec, ) } @@ -62,6 +66,7 @@ internal fun createSwipeAnimation( isUpOrLeft: Boolean, orientation: Orientation, gestureContext: MutableDragOffsetGestureContext, + decayAnimationSpec: DecayAnimationSpec<Float>, distance: Float = DistanceUnspecified, ): SwipeAnimation<*> { var lastDistance = distance @@ -106,6 +111,7 @@ internal fun createSwipeAnimation( distance = ::distance, contentForUserActions = { layoutImpl.contentForUserActions().key }, gestureContext = gestureContext, + decayAnimationSpec = decayAnimationSpec, ) } @@ -117,6 +123,7 @@ private fun createSwipeAnimation( distance: (SwipeAnimation<*>) -> Float, contentForUserActions: () -> ContentKey, gestureContext: MutableDragOffsetGestureContext, + decayAnimationSpec: DecayAnimationSpec<Float>, ): SwipeAnimation<*> { fun <T : ContentKey> swipeAnimation(fromContent: T, toContent: T): SwipeAnimation<T> { return SwipeAnimation( @@ -128,6 +135,7 @@ private fun createSwipeAnimation( requiresFullDistanceSwipe = result.requiresFullDistanceSwipe, distance = distance, gestureContext = gestureContext, + decayAnimationSpec = decayAnimationSpec, ) } @@ -201,6 +209,7 @@ internal class SwipeAnimation<T : ContentKey>( private val distance: (SwipeAnimation<T>) -> Float, currentContent: T = fromContent, private val gestureContext: MutableDragOffsetGestureContext, + private val decayAnimationSpec: DecayAnimationSpec<Float>, ) : MutableDragOffsetGestureContext by gestureContext { /** The [TransitionState.Transition] whose implementation delegates to this [SwipeAnimation]. */ lateinit var contentTransition: TransitionState.Transition @@ -367,20 +376,10 @@ internal class SwipeAnimation<T : ContentKey>( check(isAnimatingOffset()) - val motionSpatialSpec = spec ?: layoutState.motionScheme.defaultSpatialSpec() - val velocityConsumed = CompletableDeferred<Float>() offsetAnimationRunnable.complete { - val result = - animatable.animateTo( - targetValue = targetOffset, - animationSpec = motionSpatialSpec, - initialVelocity = initialVelocity, - ) - - // We are no longer able to consume the velocity, the rest can be consumed by another - // component in the hierarchy. - velocityConsumed.complete(initialVelocity - result.endState.velocity) + val consumed = animateOffset(animatable, targetOffset, initialVelocity, spec) + velocityConsumed.complete(consumed) // Wait for overscroll to finish so that the transition is removed from the STLState // only after the overscroll is done, to avoid dropping frame right when the user lifts @@ -391,6 +390,53 @@ internal class SwipeAnimation<T : ContentKey>( return velocityConsumed.await() } + private suspend fun animateOffset( + animatable: Animatable<Float, AnimationVector1D>, + targetOffset: Float, + initialVelocity: Float, + spec: AnimationSpec<Float>?, + ): Float { + val initialOffset = animatable.value + val decayOffset = + decayAnimationSpec.calculateTargetValue( + initialVelocity = initialVelocity, + initialValue = initialOffset, + ) + + val willDecayReachBounds = + when { + targetOffset > initialOffset -> decayOffset >= targetOffset + targetOffset < initialOffset -> decayOffset <= targetOffset + else -> true + } + + if (willDecayReachBounds) { + val result = animatable.animateDecay(initialVelocity, decayAnimationSpec) + check(animatable.value == targetOffset) { + buildString { + appendLine( + "animatable.value = ${animatable.value} != $targetOffset = targetOffset" + ) + appendLine(" initialOffset=$initialOffset") + appendLine(" targetOffset=$targetOffset") + appendLine(" initialVelocity=$initialVelocity") + appendLine(" decayOffset=$decayOffset") + } + } + return initialVelocity - result.endState.velocity + } + + val motionSpatialSpec = spec ?: layoutState.motionScheme.defaultSpatialSpec() + animatable.animateTo( + targetValue = targetOffset, + animationSpec = motionSpatialSpec, + initialVelocity = initialVelocity, + ) + + // We consumed the whole velocity. + return initialVelocity + } + private fun canChangeContent(targetContent: ContentKey): Boolean { return when (val transition = contentTransition) { is TransitionState.Transition.ChangeScene -> diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/Seek.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/Seek.kt deleted file mode 100644 index 819cec712808..000000000000 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/Seek.kt +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.compose.animation.scene.transition - -import androidx.annotation.FloatRange -import androidx.compose.animation.core.AnimationSpec -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.ui.util.fastCoerceIn -import com.android.compose.animation.scene.ContentKey -import com.android.compose.animation.scene.MutableSceneTransitionLayoutState -import com.android.compose.animation.scene.MutableSceneTransitionLayoutStateImpl -import com.android.compose.animation.scene.OverlayKey -import com.android.compose.animation.scene.SceneKey -import com.android.compose.animation.scene.SwipeAnimation -import com.android.compose.animation.scene.TransitionKey -import com.android.compose.animation.scene.UserActionResult -import com.android.compose.animation.scene.createSwipeAnimation -import com.android.mechanics.ProvidedGestureContext -import com.android.mechanics.spec.InputDirection -import kotlin.coroutines.cancellation.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch - -/** - * Seek to the given [scene] using [progress]. - * - * This will start a transition from the - * [current scene][MutableSceneTransitionLayoutState.currentScene] to [scene], driven by the - * progress in [progress]. Once [progress] stops emitting, we will animate progress to 1f (using - * [animationSpec]) if it stopped normally or to 0f if it stopped with a - * [kotlin.coroutines.cancellation.CancellationException]. - */ -suspend fun MutableSceneTransitionLayoutState.seekToScene( - scene: SceneKey, - @FloatRange(0.0, 1.0) progress: Flow<Float>, - transitionKey: TransitionKey? = null, - animationSpec: AnimationSpec<Float>? = null, -) { - require(scene != currentScene) { - "seekToScene($scene) has to be called with a different scene than the current scene" - } - - seek(UserActionResult.ChangeScene(scene, transitionKey), progress, animationSpec) -} - -/** - * Seek to show the given [overlay] using [progress]. - * - * This will start a transition to show [overlay] from the - * [current scene][MutableSceneTransitionLayoutState.currentScene], driven by the progress in - * [progress]. Once [progress] stops emitting, we will animate progress to 1f (using - * [animationSpec]) if it stopped normally or to 0f if it stopped with a - * [kotlin.coroutines.cancellation.CancellationException]. - */ -suspend fun MutableSceneTransitionLayoutState.seekToShowOverlay( - overlay: OverlayKey, - @FloatRange(0.0, 1.0) progress: Flow<Float>, - transitionKey: TransitionKey? = null, - animationSpec: AnimationSpec<Float>? = null, -) { - require(overlay in currentOverlays) { - "seekToShowOverlay($overlay) can be called only when the overlay is in currentOverlays" - } - - seek(UserActionResult.ShowOverlay(overlay, transitionKey), progress, animationSpec) -} - -/** - * Seek to hide the given [overlay] using [progress]. - * - * This will start a transition to hide [overlay] to the - * [current scene][MutableSceneTransitionLayoutState.currentScene], driven by the progress in - * [progress]. Once [progress] stops emitting, we will animate progress to 1f (using - * [animationSpec]) if it stopped normally or to 0f if it stopped with a - * [kotlin.coroutines.cancellation.CancellationException]. - */ -suspend fun MutableSceneTransitionLayoutState.seekToHideOverlay( - overlay: OverlayKey, - @FloatRange(0.0, 1.0) progress: Flow<Float>, - transitionKey: TransitionKey? = null, - animationSpec: AnimationSpec<Float>? = null, -) { - require(overlay !in currentOverlays) { - "seekToHideOverlay($overlay) can be called only when the overlay is *not* in " + - "currentOverlays" - } - - seek(UserActionResult.HideOverlay(overlay, transitionKey), progress, animationSpec) -} - -private suspend fun MutableSceneTransitionLayoutState.seek( - result: UserActionResult, - progress: Flow<Float>, - animationSpec: AnimationSpec<Float>?, -) { - val layoutState = - when (this) { - is MutableSceneTransitionLayoutStateImpl -> this - } - - val swipeAnimation = - createSwipeAnimation( - layoutState = layoutState, - result = result, - - // We are animating progress, so distance is always 1f. - distance = 1f, - - // The orientation and isUpOrLeft don't matter here given that they are only used during - // overscroll, which is disabled for progress-based transitions. - orientation = Orientation.Horizontal, - isUpOrLeft = false, - // There is no gesture information available here - animateProgress - // will set the progress as the dragOffset. - gestureContext = ProvidedGestureContext(0f, InputDirection.Max), - ) - - animateProgress( - state = layoutState, - animation = swipeAnimation, - progress = progress, - commitSpec = animationSpec, - cancelSpec = animationSpec, - ) -} - -internal suspend fun <T : ContentKey> animateProgress( - state: MutableSceneTransitionLayoutStateImpl, - animation: SwipeAnimation<T>, - progress: Flow<Float>, - commitSpec: AnimationSpec<Float>?, - cancelSpec: AnimationSpec<Float>?, - animationScope: CoroutineScope? = null, -) { - suspend fun animateOffset(targetContent: T, spec: AnimationSpec<Float>?) { - if (state.transitionState != animation.contentTransition || animation.isAnimatingOffset()) { - return - } - - animation.animateOffset( - initialVelocity = 0f, - targetContent = targetContent, - - // Important: we have to specify a spec that correctly animates *progress* (low - // visibility threshold) and not *offset* (higher visibility threshold). - spec = spec ?: animation.contentTransition.transformationSpec.progressSpec, - ) - } - - coroutineScope { - val collectionJob = launch { - try { - progress.collectLatest { progress -> - // Progress based animation should never overscroll given that the - // absoluteDistance exposed to overscroll builders is always 1f and will not - // lead to any noticeable transformation. - animation.dragOffset = progress.fastCoerceIn(0f, 1f) - } - - // Transition committed. - animateOffset(animation.toContent, commitSpec) - } catch (e: CancellationException) { - // Transition cancelled. - animateOffset(animation.fromContent, cancelSpec) - } - } - - // Start the transition. - animationScope?.launch { startTransition(state, animation, collectionJob) } - ?: startTransition(state, animation, collectionJob) - } -} - -private suspend fun <T : ContentKey> startTransition( - state: MutableSceneTransitionLayoutStateImpl, - animation: SwipeAnimation<T>, - progressCollectionJob: Job, -) { - state.startTransition(animation.contentTransition) - // The transition is done. Cancel the collection in case the transition was finished - // because it was interrupted by another transition. - if (progressCollectionJob.isActive) { - progressCollectionJob.cancel() - } -} diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt index 6b439980cc68..969003cb92f3 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt @@ -18,7 +18,9 @@ package com.android.compose.animation.scene +import androidx.compose.animation.SplineBasedFloatDecayAnimationSpec import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.generateDecayAnimationSpec import androidx.compose.animation.core.spring import androidx.compose.foundation.overscroll import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -119,10 +121,11 @@ class DraggableHandlerTest { val transitionInterceptionThreshold = 0.05f val directionChangeSlop = 10f + private val density = Density(1f) private val layoutImpl = SceneTransitionLayoutImpl( state = layoutState, - density = Density(1f), + density = density, layoutDirection = LayoutDirection.Ltr, swipeSourceDetector = DefaultEdgeDetector, swipeDetector = DefaultSwipeDetector, @@ -134,6 +137,8 @@ class DraggableHandlerTest { animationScope = testScope, directionChangeSlop = directionChangeSlop, defaultEffectFactory = defaultEffectFactory, + decayAnimationSpec = + SplineBasedFloatDecayAnimationSpec(density).generateDecayAnimationSpec(), ) .apply { setContentsAndLayoutTargetSizeForTest(LAYOUT_SIZE) } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt index c5e4061e834a..26f3c259dca9 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt @@ -24,18 +24,14 @@ import com.android.compose.animation.scene.TestScenes.SceneB import com.android.compose.animation.scene.TestScenes.SceneC import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.animation.scene.subjects.assertThat -import com.android.compose.animation.scene.transition.seekToScene import com.android.compose.test.TestSceneTransition import com.android.compose.test.runMonotonicClockTest import com.android.compose.test.transition import com.google.common.truth.Truth.assertThat -import kotlin.coroutines.cancellation.CancellationException import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.cancelAndJoin -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runCurrent @@ -282,77 +278,6 @@ class SceneTransitionLayoutStateTest { } @Test - fun seekToScene() = runMonotonicClockTest { - val state = MutableSceneTransitionLayoutStateForTests(SceneA) - val progress = Channel<Float>() - - val job = - launch(start = CoroutineStart.UNDISPATCHED) { - state.seekToScene(SceneB, progress.consumeAsFlow()) - } - - val transition = assertThat(state.transitionState).isSceneTransition() - assertThat(transition).hasFromScene(SceneA) - assertThat(transition).hasToScene(SceneB) - assertThat(transition).hasProgress(0f) - - // Change progress. - progress.send(0.4f) - assertThat(transition).hasProgress(0.4f) - - // Close the channel normally to confirm the transition. - progress.close() - job.join() - assertThat(state.transitionState).isIdle() - assertThat(state.transitionState).hasCurrentScene(SceneB) - } - - @Test - fun seekToScene_cancelled() = runMonotonicClockTest { - val state = MutableSceneTransitionLayoutStateForTests(SceneA) - val progress = Channel<Float>() - - val job = - launch(start = CoroutineStart.UNDISPATCHED) { - state.seekToScene(SceneB, progress.consumeAsFlow()) - } - - val transition = assertThat(state.transitionState).isSceneTransition() - assertThat(transition).hasFromScene(SceneA) - assertThat(transition).hasToScene(SceneB) - assertThat(transition).hasProgress(0f) - - // Change progress. - progress.send(0.4f) - assertThat(transition).hasProgress(0.4f) - - // Close the channel with a CancellationException to cancel the transition. - progress.close(CancellationException()) - job.join() - assertThat(state.transitionState).isIdle() - assertThat(state.transitionState).hasCurrentScene(SceneA) - } - - @Test - fun seekToScene_interrupted() = runMonotonicClockTest { - val state = MutableSceneTransitionLayoutStateForTests(SceneA) - val progress = Channel<Float>() - - val job = - launch(start = CoroutineStart.UNDISPATCHED) { - state.seekToScene(SceneB, progress.consumeAsFlow()) - } - - assertThat(state.transitionState).isSceneTransition() - - // Start a new transition, interrupting the seek transition. - state.setTargetScene(SceneB, animationScope = this) - - // The previous job is cancelled and does not infinitely collect the progress. - job.join() - } - - @Test fun replacedTransitionIsRemovedFromFinishedTransitions() = runTest { val state = MutableSceneTransitionLayoutStateForTests(SceneA) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt index f6ff3268fca4..36029177d4f6 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt @@ -17,6 +17,7 @@ import android.content.Context import android.content.res.Resources import android.graphics.Typeface import android.view.LayoutInflater +import com.android.systemui.animation.GSFAxes import com.android.systemui.customization.R import com.android.systemui.log.core.MessageBuffer import com.android.systemui.plugins.clocks.ClockController @@ -107,18 +108,18 @@ class DefaultClockProvider( // TODO(b/364681643): Variations for retargetted DIGITAL_CLOCK_FLEX val LEGACY_FLEX_LS_VARIATION = listOf( - ClockFontAxisSetting("wght", 600f), - ClockFontAxisSetting("wdth", 100f), - ClockFontAxisSetting("ROND", 100f), - ClockFontAxisSetting("slnt", 0f), + ClockFontAxisSetting(GSFAxes.WEIGHT, 600f), + ClockFontAxisSetting(GSFAxes.WIDTH, 100f), + ClockFontAxisSetting(GSFAxes.ROUND, 100f), + ClockFontAxisSetting(GSFAxes.SLANT, 0f), ) val LEGACY_FLEX_AOD_VARIATION = listOf( - ClockFontAxisSetting("wght", 74f), - ClockFontAxisSetting("wdth", 43f), - ClockFontAxisSetting("ROND", 100f), - ClockFontAxisSetting("slnt", 0f), + ClockFontAxisSetting(GSFAxes.WEIGHT, 74f), + ClockFontAxisSetting(GSFAxes.WIDTH, 43f), + ClockFontAxisSetting(GSFAxes.ROUND, 100f), + ClockFontAxisSetting(GSFAxes.SLANT, 0f), ) val FLEX_TYPEFACE by lazy { diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt index e69fa994931d..cc3769e0a568 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt @@ -16,6 +16,7 @@ package com.android.systemui.shared.clocks +import com.android.systemui.animation.GSFAxes import com.android.systemui.customization.R import com.android.systemui.plugins.clocks.AlarmData import com.android.systemui.plugins.clocks.AxisType @@ -122,16 +123,16 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController val FONT_AXES = listOf( ClockFontAxis( - key = "wght", + key = GSFAxes.WEIGHT, type = AxisType.Float, - minValue = 1f, + minValue = 25f, currentValue = 400f, maxValue = 1000f, name = "Weight", description = "Glyph Weight", ), ClockFontAxis( - key = "wdth", + key = GSFAxes.WIDTH, type = AxisType.Float, minValue = 25f, currentValue = 100f, @@ -140,7 +141,7 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController description = "Glyph Width", ), ClockFontAxis( - key = "ROND", + key = GSFAxes.ROUND, type = AxisType.Boolean, minValue = 0f, currentValue = 0f, @@ -149,7 +150,7 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController description = "Glyph Roundness", ), ClockFontAxis( - key = "slnt", + key = GSFAxes.SLANT, type = AxisType.Boolean, minValue = 0f, currentValue = 0f, diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt index 827bd6898310..e2bbe0fef3c0 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt @@ -21,6 +21,7 @@ import android.view.Gravity import android.view.View import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.widget.FrameLayout +import com.android.systemui.animation.GSFAxes import com.android.systemui.customization.R import com.android.systemui.plugins.clocks.AlarmData import com.android.systemui.plugins.clocks.ClockAnimations @@ -125,7 +126,19 @@ class FlexClockFaceController(clockCtx: ClockContext, private val isLargeClock: layerController.faceEvents.onThemeChanged(theme) } - override fun onFontAxesChanged(axes: List<ClockFontAxisSetting>) { + override fun onFontAxesChanged(settings: List<ClockFontAxisSetting>) { + var axes = settings + if (!isLargeClock) { + axes = + axes.map { axis -> + if (axis.key == GSFAxes.WIDTH && axis.value > SMALL_CLOCK_MAX_WDTH) { + axis.copy(value = SMALL_CLOCK_MAX_WDTH) + } else { + axis + } + } + } + layerController.events.onFontAxesChanged(axes) } @@ -236,6 +249,7 @@ class FlexClockFaceController(clockCtx: ClockContext, private val isLargeClock: } companion object { + val SMALL_CLOCK_MAX_WDTH = 120f val SMALL_LAYER_CONFIG = LayerConfig( timespec = DigitalTimespec.TIME_FULL_FORMAT, diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt index c40bb9a5ebea..3eb519186a3e 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt @@ -139,13 +139,49 @@ class FlexClockView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) { digitalClockTextViewMap.forEach { (_, textView) -> textView.refreshText() } } + override fun setVisibility(visibility: Int) { + if (visibility != this.visibility) { + logger.d({ "setVisibility(${str1 ?: int1})" }) { + int1 = visibility + str1 = + when (visibility) { + VISIBLE -> "VISIBLE" + INVISIBLE -> "INVISIBLE" + GONE -> "GONE" + else -> null + } + } + } + + super.setVisibility(visibility) + } + + private var loggedAlpha = 1000f + + override fun setAlpha(alpha: Float) { + val delta = if (alpha <= 0f || alpha >= 1f) 0.001f else 0.5f + if (abs(loggedAlpha - alpha) >= delta) { + loggedAlpha = alpha + logger.d({ "setAlpha($double1)" }) { double1 = alpha.toDouble() } + } + super.setAlpha(alpha) + } + + private val isDrawn: Boolean + get() = (mPrivateFlags and 0x20 /* PFLAG_DRAWN */) > 0 + override fun invalidate() { - logger.d("invalidate()") + if (isDrawn && visibility == VISIBLE) { + logger.d("invalidate()") + } + super.invalidate() } override fun requestLayout() { - logger.d("requestLayout()") + if (!isLayoutRequested()) { + logger.d("requestLayout()") + } super.requestLayout() } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt index 2b0825f39243..fbd5887c5b54 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt @@ -34,6 +34,7 @@ import android.view.View.MeasureSpec.EXACTLY import android.view.animation.Interpolator import android.widget.TextView import com.android.internal.annotations.VisibleForTesting +import com.android.systemui.animation.GSFAxes import com.android.systemui.animation.TextAnimator import com.android.systemui.customization.R import com.android.systemui.log.core.Logger @@ -44,6 +45,7 @@ import com.android.systemui.shared.clocks.DimensionParser import com.android.systemui.shared.clocks.FontTextStyle import com.android.systemui.shared.clocks.LogUtil import java.lang.Thread +import kotlin.math.abs import kotlin.math.max import kotlin.math.min @@ -206,7 +208,10 @@ open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSe } override fun onDraw(canvas: Canvas) { - logger.d({ "onDraw(); ls: $str1" }) { str1 = textAnimator.textInterpolator.shapedText } + logger.d({ "onDraw(${str1?.replace("\n", "\\n")})" }) { + str1 = textAnimator.textInterpolator.shapedText + } + val translation = getLocalTranslation() canvas.translate(translation.x.toFloat(), translation.y.toFloat()) digitTranslateAnimator?.let { @@ -221,8 +226,42 @@ open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSe canvas.translate(-translation.x.toFloat(), -translation.y.toFloat()) } + override fun setVisibility(visibility: Int) { + if (visibility != this.visibility) { + logger.d({ "setVisibility(${str1 ?: int1})" }) { + int1 = visibility + str1 = + when (visibility) { + VISIBLE -> "VISIBLE" + INVISIBLE -> "INVISIBLE" + GONE -> "GONE" + else -> null + } + } + } + + super.setVisibility(visibility) + } + + private var loggedAlpha = 1000f + + override fun setAlpha(alpha: Float) { + val delta = if (alpha <= 0f || alpha >= 1f) 0.001f else 0.5f + if (abs(loggedAlpha - alpha) >= delta) { + loggedAlpha = alpha + logger.d({ "setAlpha($double1)" }) { double1 = alpha.toDouble() } + } + super.setAlpha(alpha) + } + + private val isDrawn: Boolean + get() = (mPrivateFlags and 0x20 /* PFLAG_DRAWN */) > 0 + override fun invalidate() { - logger.d("invalidate()") + if (isDrawn && visibility == VISIBLE) { + logger.d("invalidate()") + } + super.invalidate() (parent as? FlexClockView)?.invalidate() } @@ -490,22 +529,22 @@ open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSe Paint().also { it.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT) } val AOD_COLOR = Color.WHITE - val OPTICAL_SIZE_AXIS = ClockFontAxisSetting("opsz", 144f) + val OPTICAL_SIZE_AXIS = ClockFontAxisSetting(GSFAxes.OPTICAL_SIZE, 144f) val DEFAULT_LS_VARIATION = listOf( OPTICAL_SIZE_AXIS, - ClockFontAxisSetting("wght", 400f), - ClockFontAxisSetting("wdth", 100f), - ClockFontAxisSetting("ROND", 0f), - ClockFontAxisSetting("slnt", 0f), + ClockFontAxisSetting(GSFAxes.WEIGHT, 400f), + ClockFontAxisSetting(GSFAxes.WIDTH, 100f), + ClockFontAxisSetting(GSFAxes.ROUND, 0f), + ClockFontAxisSetting(GSFAxes.SLANT, 0f), ) val DEFAULT_AOD_VARIATION = listOf( OPTICAL_SIZE_AXIS, - ClockFontAxisSetting("wght", 200f), - ClockFontAxisSetting("wdth", 100f), - ClockFontAxisSetting("ROND", 0f), - ClockFontAxisSetting("slnt", 0f), + ClockFontAxisSetting(GSFAxes.WEIGHT, 200f), + ClockFontAxisSetting(GSFAxes.WIDTH, 100f), + ClockFontAxisSetting(GSFAxes.ROUND, 0f), + ClockFontAxisSetting(GSFAxes.SLANT, 0f), ) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java index b087ecf1a488..6d75c4ca3a38 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java @@ -46,7 +46,7 @@ import androidx.test.filters.SmallTest; import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.systemui.SysuiTestCase; import com.android.systemui.model.SysUiState; -import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.recents.LauncherProxyService; import com.android.systemui.settings.FakeDisplayTracker; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.util.settings.SecureSettings; @@ -86,7 +86,7 @@ public class IMagnificationConnectionTest extends SysuiTestCase { @Mock private IRemoteMagnificationAnimationCallback mAnimationCallback; @Mock - private OverviewProxyService mOverviewProxyService; + private LauncherProxyService mLauncherProxyService; @Mock private SecureSettings mSecureSettings; @Mock @@ -114,7 +114,7 @@ public class IMagnificationConnectionTest extends SysuiTestCase { assertNotNull(mTestableLooper); mMagnification = new MagnificationImpl(getContext(), mTestableLooper.getLooper(), mContext.getMainExecutor(), mCommandQueue, - mModeSwitchesController, mSysUiState, mOverviewProxyService, mSecureSettings, + mModeSwitchesController, mSysUiState, mLauncherProxyService, mSecureSettings, mDisplayTracker, getContext().getSystemService(DisplayManager.class), mA11yLogger, mIWindowManager, mAccessibilityManager, mViewCaptureAwareWindowManager); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt index b0f81c012cca..f44769d522eb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt @@ -7,11 +7,6 @@ import junit.framework.Assert import org.junit.Test import org.junit.runner.RunWith -private const val TAG_WGHT = "wght" -private const val TAG_WDTH = "wdth" -private const val TAG_OPSZ = "opsz" -private const val TAG_ROND = "ROND" - @RunWith(AndroidJUnit4::class) @SmallTest class FontVariationUtilsTest : SysuiTestCase() { @@ -23,19 +18,22 @@ class FontVariationUtilsTest : SysuiTestCase() { weight = 100, width = 100, opticalSize = -1, - roundness = 100 + roundness = 100, ) - Assert.assertEquals("'$TAG_WGHT' 100, '$TAG_WDTH' 100, '$TAG_ROND' 100", initFvar) + Assert.assertEquals( + "'${GSFAxes.WEIGHT}' 100, '${GSFAxes.WIDTH}' 100, '${GSFAxes.ROUND}' 100", + initFvar, + ) val updatedFvar = fontVariationUtils.updateFontVariation( weight = 200, width = 100, opticalSize = 0, - roundness = 100 + roundness = 100, ) Assert.assertEquals( - "'$TAG_WGHT' 200, '$TAG_WDTH' 100, '$TAG_OPSZ' 0, '$TAG_ROND' 100", - updatedFvar + "'${GSFAxes.WEIGHT}' 200, '${GSFAxes.WIDTH}' 100, '${GSFAxes.OPTICAL_SIZE}' 0, '${GSFAxes.ROUND}' 100", + updatedFvar, ) } @@ -46,14 +44,14 @@ class FontVariationUtilsTest : SysuiTestCase() { weight = 100, width = 100, opticalSize = 0, - roundness = 100 + roundness = 100, ) val updatedFvar1 = fontVariationUtils.updateFontVariation( weight = 100, width = 100, opticalSize = 0, - roundness = 100 + roundness = 100, ) Assert.assertEquals("", updatedFvar1) val updatedFvar2 = fontVariationUtils.updateFontVariation() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt index 5e023a203267..6899d23bcfd7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt @@ -34,7 +34,7 @@ import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType import com.android.systemui.inputdevice.tutorial.tutorialSchedulerRepository import com.android.systemui.keyboard.data.repository.keyboardRepository import com.android.systemui.kosmos.testScope -import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener +import com.android.systemui.recents.LauncherProxyService.LauncherProxyListener import com.android.systemui.testKosmos import com.android.systemui.touchpad.data.repository.touchpadRepository import com.android.systemui.user.data.repository.fakeUserRepository @@ -68,7 +68,7 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge private val keyboardRepository = kosmos.keyboardRepository private val tutorialSchedulerRepository = kosmos.tutorialSchedulerRepository private val userRepository = kosmos.fakeUserRepository - private val overviewProxyService = kosmos.mockOverviewProxyService + private val launcherProxyService = kosmos.mockLauncherProxyService private val underTest: KeyboardTouchpadEduInteractor = kosmos.keyboardTouchpadEduInteractor private val eduClock = kosmos.fakeEduClock @@ -519,8 +519,8 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge } private fun updateContextualEduStats(isTrackpadGesture: Boolean, gestureType: GestureType) { - val listenerCaptor = argumentCaptor<OverviewProxyListener>() - verify(overviewProxyService).addCallback(listenerCaptor.capture()) + val listenerCaptor = argumentCaptor<LauncherProxyListener>() + verify(launcherProxyService).addCallback(listenerCaptor.capture()) listenerCaptor.firstValue.updateContextualEduStats(isTrackpadGesture, gestureType) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt index 692b9c67f322..dc393c01dce1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt @@ -32,7 +32,7 @@ import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType import com.android.systemui.inputdevice.tutorial.tutorialSchedulerRepository import com.android.systemui.keyboard.data.repository.keyboardRepository import com.android.systemui.kosmos.testScope -import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener +import com.android.systemui.recents.LauncherProxyService.LauncherProxyListener import com.android.systemui.testKosmos import com.android.systemui.touchpad.data.repository.touchpadRepository import com.google.common.truth.Truth.assertThat @@ -58,7 +58,7 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { private val touchpadRepository = kosmos.touchpadRepository private val keyboardRepository = kosmos.keyboardRepository private val tutorialSchedulerRepository = kosmos.tutorialSchedulerRepository - private val overviewProxyService = kosmos.mockOverviewProxyService + private val launcherProxyService = kosmos.mockLauncherProxyService private val underTest: KeyboardTouchpadEduInteractor = kosmos.keyboardTouchpadEduInteractor private val eduClock = kosmos.fakeEduClock @@ -167,16 +167,16 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { keyboardRepository.setIsAnyKeyboardConnected(true) } - private fun getOverviewProxyListener(): OverviewProxyListener { - val listenerCaptor = argumentCaptor<OverviewProxyListener>() - verify(overviewProxyService).addCallback(listenerCaptor.capture()) + private fun getLauncherProxyListener(): LauncherProxyListener { + val listenerCaptor = argumentCaptor<LauncherProxyListener>() + verify(launcherProxyService).addCallback(listenerCaptor.capture()) return listenerCaptor.firstValue } private fun TestScope.triggerEducation(gestureType: GestureType) { // Increment max number of signal to try triggering education for (i in 1..KeyboardTouchpadEduInteractor.MAX_SIGNAL_COUNT) { - val listener = getOverviewProxyListener() + val listener = getLauncherProxyListener() listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) } runCurrent() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepositoryTest.kt index 698fac107a1d..4d81cb0ce726 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepositoryTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyboard.shortcut.data.repository import android.content.Context import android.content.Context.INPUT_SERVICE import android.content.Intent +import android.hardware.input.FakeInputManager import android.hardware.input.InputGestureData import android.hardware.input.InputManager import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS @@ -57,13 +58,14 @@ class CustomInputGesturesRepositoryTest : SysuiTestCase() { private val secondaryUserContext: Context = mock() private var activeUserContext: Context = primaryUserContext - private val kosmos = testKosmos().also { - it.userTracker = FakeUserTracker(onCreateCurrentUserContext = { activeUserContext }) - } + private val kosmos = + testKosmos().also { + it.userTracker = FakeUserTracker(onCreateCurrentUserContext = { activeUserContext }) + } private val inputManager = kosmos.fakeInputManager.inputManager private val broadcastDispatcher = kosmos.broadcastDispatcher - private val inputManagerForSecondaryUser: InputManager = mock() + private val inputManagerForSecondaryUser: InputManager = FakeInputManager().inputManager private val testScope = kosmos.testScope private val testHelper = kosmos.shortcutHelperTestHelper private val customInputGesturesRepository = kosmos.customInputGesturesRepository @@ -94,9 +96,10 @@ class CustomInputGesturesRepositoryTest : SysuiTestCase() { fun customInputGestures_initialValueReturnsDataFromAPI() { testScope.runTest { val customInputGestures = listOf(allAppsInputGestureData) - whenever( - inputManager.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY) - ).then { return@then customInputGestures } + whenever(inputManager.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY)) + .then { + return@then customInputGestures + } val inputGestures by collectLastValue(customInputGesturesRepository.customInputGestures) @@ -108,9 +111,10 @@ class CustomInputGesturesRepositoryTest : SysuiTestCase() { fun customInputGestures_isUpdatedToMostRecentDataAfterNewGestureIsAdded() { testScope.runTest { var customInputGestures = listOf<InputGestureData>() - whenever( - inputManager.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY) - ).then { return@then customInputGestures } + whenever(inputManager.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY)) + .then { + return@then customInputGestures + } whenever(inputManager.addCustomInputGesture(any())).then { invocation -> val inputGesture = invocation.getArgument<InputGestureData>(0) customInputGestures = customInputGestures + inputGesture @@ -129,10 +133,10 @@ class CustomInputGesturesRepositoryTest : SysuiTestCase() { fun retrieveCustomInputGestures_retrievesMostRecentData() { testScope.runTest { var customInputGestures = listOf<InputGestureData>() - whenever( - inputManager.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY) - ).then { return@then customInputGestures } - + whenever(inputManager.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY)) + .then { + return@then customInputGestures + } assertThat(customInputGesturesRepository.retrieveCustomInputGestures()).isEmpty() @@ -143,24 +147,38 @@ class CustomInputGesturesRepositoryTest : SysuiTestCase() { } } + @Test + fun getInputGestureByTrigger_returnsInputGestureFromInputManager() = + testScope.runTest { + inputManager.addCustomInputGesture(allAppsInputGestureData) + + val inputGestureData = + customInputGesturesRepository.getInputGestureByTrigger( + allAppsInputGestureData.trigger + ) + + assertThat(inputGestureData).isEqualTo(allAppsInputGestureData) + } + private fun setCustomInputGesturesForPrimaryUser(vararg inputGesture: InputGestureData) { - whenever( - inputManager.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY) - ).thenReturn(inputGesture.toList()) + whenever(inputManager.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY)) + .thenReturn(inputGesture.toList()) } private fun setCustomInputGesturesForSecondaryUser(vararg inputGesture: InputGestureData) { whenever( - inputManagerForSecondaryUser.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY) - ).thenReturn(inputGesture.toList()) + inputManagerForSecondaryUser.getCustomInputGestures( + /* filter= */ InputGestureData.Filter.KEY + ) + ) + .thenReturn(inputGesture.toList()) } private fun switchToSecondaryUser() { activeUserContext = secondaryUserContext broadcastDispatcher.sendIntentToMatchingReceiversOnly( context, - Intent(Intent.ACTION_USER_SWITCHED) + Intent(Intent.ACTION_USER_SWITCHED), ) } - -}
\ No newline at end of file +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt index 4cfb26e6555b..522572dcffb7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt @@ -24,6 +24,7 @@ import android.hardware.input.InputGestureData import android.hardware.input.InputGestureData.createKeyTrigger import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_DOES_NOT_EXIST import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS +import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_HOME import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION import android.hardware.input.fakeInputManager import android.platform.test.annotations.DisableFlags @@ -336,28 +337,6 @@ class CustomShortcutCategoriesRepositoryTest : SysuiTestCase() { } } - private suspend fun customizeShortcut( - customizationRequest: ShortcutCustomizationRequestInfo, - keyCombination: KeyCombination? = null, - ): ShortcutCustomizationRequestResult { - repo.onCustomizationRequested(customizationRequest) - repo.updateUserKeyCombination(keyCombination) - - return when (customizationRequest) { - is SingleShortcutCustomization.Add -> { - repo.confirmAndSetShortcutCurrentlyBeingCustomized() - } - - is SingleShortcutCustomization.Delete -> { - repo.deleteShortcutCurrentlyBeingCustomized() - } - - else -> { - ShortcutCustomizationRequestResult.ERROR_OTHER - } - } - } - @Test @EnableFlags(FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES, FLAG_USE_KEY_GESTURE_EVENT_HANDLER) fun categories_isUpdatedAfterCustomShortcutsAreReset() { @@ -387,10 +366,66 @@ class CustomShortcutCategoriesRepositoryTest : SysuiTestCase() { } } + @Test + fun selectedKeyCombinationIsAvailable_whenTriggerIsNotRegisteredInInputManager() = + testScope.runTest { + helper.toggle(deviceId = 123) + repo.onCustomizationRequested(allAppsShortcutAddRequest) + repo.updateUserKeyCombination(standardKeyCombination) + + assertThat(repo.isSelectedKeyCombinationAvailable()).isTrue() + } + + @Test + fun selectedKeyCombinationIsNotAvailable_whenTriggerIsRegisteredInInputManager() = + testScope.runTest { + inputManager.addCustomInputGesture(buildInputGestureWithStandardKeyCombination()) + + helper.toggle(deviceId = 123) + repo.onCustomizationRequested(allAppsShortcutAddRequest) + repo.updateUserKeyCombination(standardKeyCombination) + + assertThat(repo.isSelectedKeyCombinationAvailable()).isFalse() + } + private fun setApiAppLaunchBookmarks(appLaunchBookmarks: List<InputGestureData>) { whenever(inputManager.appLaunchBookmarks).thenReturn(appLaunchBookmarks) } + private suspend fun customizeShortcut( + customizationRequest: ShortcutCustomizationRequestInfo, + keyCombination: KeyCombination? = null, + ): ShortcutCustomizationRequestResult { + repo.onCustomizationRequested(customizationRequest) + repo.updateUserKeyCombination(keyCombination) + + return when (customizationRequest) { + is SingleShortcutCustomization.Add -> { + repo.confirmAndSetShortcutCurrentlyBeingCustomized() + } + + is SingleShortcutCustomization.Delete -> { + repo.deleteShortcutCurrentlyBeingCustomized() + } + + else -> { + ShortcutCustomizationRequestResult.ERROR_OTHER + } + } + } + + private fun buildInputGestureWithStandardKeyCombination() = + InputGestureData.Builder() + .setKeyGestureType(KEY_GESTURE_TYPE_HOME) + .setTrigger( + createKeyTrigger( + /* keycode= */ standardKeyCombination.keyCode!!, + /* modifierState= */ standardKeyCombination.modifiers and + ALL_SUPPORTED_MODIFIERS, + ) + ) + .build() + private fun simpleInputGestureDataForAppLaunchShortcut( keyCode: Int = KEYCODE_A, modifiers: Int = META_CTRL_ON or META_ALT_ON, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/DefaultShortcutCategoriesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/DefaultShortcutCategoriesRepositoryTest.kt index 3bf59f34db76..cd05980385e0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/DefaultShortcutCategoriesRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/DefaultShortcutCategoriesRepositoryTest.kt @@ -50,6 +50,7 @@ import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCommand import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory +import com.android.systemui.keyboard.shortcut.shortcutHelperAccessibilityShortcutsSource import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource import com.android.systemui.keyboard.shortcut.shortcutHelperInputShortcutsSource @@ -88,6 +89,7 @@ class DefaultShortcutCategoriesRepositoryTest : SysuiTestCase() { it.shortcutHelperAppCategoriesShortcutsSource = fakeAppCategoriesSource it.shortcutHelperInputShortcutsSource = FakeKeyboardShortcutGroupsSource() it.shortcutHelperCurrentAppShortcutsSource = FakeKeyboardShortcutGroupsSource() + it.shortcutHelperAccessibilityShortcutsSource = FakeKeyboardShortcutGroupsSource() } private val repo = kosmos.defaultShortcutCategoriesRepository @@ -284,14 +286,20 @@ class DefaultShortcutCategoriesRepositoryTest : SysuiTestCase() { val categories by collectLastValue(repo.categories) val cycleForwardThroughRecentAppsShortcut = - categories?.first { it.type == ShortcutCategoryType.MultiTasking } - ?.subCategories?.first { it.label == recentAppsGroup.label } - ?.shortcuts?.first { it.label == CYCLE_FORWARD_THROUGH_RECENT_APPS_SHORTCUT_LABEL } + categories + ?.first { it.type == ShortcutCategoryType.MultiTasking } + ?.subCategories + ?.first { it.label == recentAppsGroup.label } + ?.shortcuts + ?.first { it.label == CYCLE_FORWARD_THROUGH_RECENT_APPS_SHORTCUT_LABEL } val cycleBackThroughRecentAppsShortcut = - categories?.first { it.type == ShortcutCategoryType.MultiTasking } - ?.subCategories?.first { it.label == recentAppsGroup.label } - ?.shortcuts?.first { it.label == CYCLE_BACK_THROUGH_RECENT_APPS_SHORTCUT_LABEL } + categories + ?.first { it.type == ShortcutCategoryType.MultiTasking } + ?.subCategories + ?.first { it.label == recentAppsGroup.label } + ?.shortcuts + ?.first { it.label == CYCLE_BACK_THROUGH_RECENT_APPS_SHORTCUT_LABEL } assertThat(cycleForwardThroughRecentAppsShortcut?.isCustomizable).isFalse() assertThat(cycleBackThroughRecentAppsShortcut?.isCustomizable).isFalse() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt index 8f0bc640f0eb..61490986f4a9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt @@ -38,6 +38,7 @@ import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.InputMethodEditor import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System +import com.android.systemui.keyboard.shortcut.shortcutHelperAccessibilityShortcutsSource import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource import com.android.systemui.keyboard.shortcut.shortcutHelperCategoriesInteractor import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource @@ -76,6 +77,7 @@ class ShortcutHelperCategoriesInteractorTest : SysuiTestCase() { it.shortcutHelperMultiTaskingShortcutsSource = multitaskingShortcutsSource it.shortcutHelperAppCategoriesShortcutsSource = FakeKeyboardShortcutGroupsSource() it.shortcutHelperCurrentAppShortcutsSource = FakeKeyboardShortcutGroupsSource() + it.shortcutHelperAccessibilityShortcutsSource = FakeKeyboardShortcutGroupsSource() it.userTracker = FakeUserTracker(onCreateCurrentUserContext = { mockUserContext }) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarterTest.kt index 000024f9b814..7a343351ef64 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarterTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarterTest.kt @@ -26,6 +26,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.keyboard.shortcut.data.source.FakeKeyboardShortcutGroupsSource import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts import com.android.systemui.keyboard.shortcut.shortcutCustomizationDialogStarterFactory +import com.android.systemui.keyboard.shortcut.shortcutHelperAccessibilityShortcutsSource import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource import com.android.systemui.keyboard.shortcut.shortcutHelperInputShortcutsSource @@ -66,6 +67,7 @@ class ShortcutHelperDialogStarterTest : SysuiTestCase() { it.testDispatcher = UnconfinedTestDispatcher() it.shortcutHelperSystemShortcutsSource = fakeSystemSource it.shortcutHelperMultiTaskingShortcutsSource = fakeMultiTaskingSource + it.shortcutHelperAccessibilityShortcutsSource = FakeKeyboardShortcutGroupsSource() it.shortcutHelperAppCategoriesShortcutsSource = FakeKeyboardShortcutGroupsSource() it.shortcutHelperInputShortcutsSource = FakeKeyboardShortcutGroupsSource() it.shortcutHelperCurrentAppShortcutsSource = FakeKeyboardShortcutGroupsSource() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt index d9d34f5ace7b..6eef5eb09812 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt @@ -18,11 +18,15 @@ package com.android.systemui.keyboard.shortcut.ui.viewmodel import android.content.Context import android.content.Context.INPUT_SERVICE -import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_ALREADY_EXISTS +import android.hardware.input.InputGestureData +import android.hardware.input.InputGestureData.createKeyTrigger import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_OTHER import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_RESERVED_GESTURE -import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS +import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_HOME import android.hardware.input.fakeInputManager +import android.view.KeyEvent.KEYCODE_A +import android.view.KeyEvent.META_CTRL_ON +import android.view.KeyEvent.META_META_ON import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -30,7 +34,6 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.allAppsInputGestureData import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.allAppsShortcutAddRequest import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.allAppsShortcutDeleteRequest -import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.goHomeInputGestureData import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.keyDownEventWithActionKeyPressed import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.keyDownEventWithoutActionKeyPressed import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.keyUpEventWithActionKeyPressed @@ -44,16 +47,17 @@ import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiSt import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState.DeleteShortcutDialog import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState.ResetShortcutDialog import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.res.R import com.android.systemui.settings.FakeUserTracker import com.android.systemui.settings.userTracker import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -63,7 +67,7 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { private val mockUserContext: Context = mock() private val kosmos = - testKosmos().also { + testKosmos().useUnconfinedTestDispatcher().also { it.userTracker = FakeUserTracker(onCreateCurrentUserContext = { mockUserContext }) } private val testScope = kosmos.testScope @@ -75,6 +79,7 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { fun setup() { helper.showFromActivity() whenever(mockUserContext.getSystemService(INPUT_SERVICE)).thenReturn(inputManager) + testScope.backgroundScope.launch { viewModel.activate() } } @Test @@ -146,8 +151,6 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { fun uiState_becomeInactiveAfterSuccessfullySettingShortcut() { testScope.runTest { val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) - whenever(inputManager.addCustomInputGesture(any())) - .thenReturn(CUSTOM_INPUT_GESTURE_RESULT_SUCCESS) openAddShortcutDialogAndSetShortcut() @@ -166,11 +169,38 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { } @Test - fun uiState_errorMessage_isKeyCombinationInUse_whenKeyCombinationAlreadyExists() { + fun uiState_errorMessage_onKeyPressed_isKeyCombinationInUse_whenKeyCombinationAlreadyExists() { testScope.runTest { + inputManager.addCustomInputGesture(buildSimpleInputGestureWithMetaCtrlATrigger()) + val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) + + openAddShortcutDialogAndPressKeyCombination() + + assertThat((uiState as AddShortcutDialog).errorMessage) + .isEqualTo( + context.getString( + R.string.shortcut_customizer_key_combination_in_use_error_message + ) + ) + } + } + + @Test + fun uiState_errorMessage_onKeyPressed_isEmpty_whenKeyCombinationIsAvailable() { + testScope.runTest { + val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) + + openAddShortcutDialogAndPressKeyCombination() + + assertThat((uiState as AddShortcutDialog).errorMessage).isEmpty() + } + } + + @Test + fun uiState_errorMessage_onSetShortcut_isKeyCombinationInUse_whenKeyCombinationAlreadyExists() { + testScope.runTest { + inputManager.addCustomInputGesture(buildSimpleInputGestureWithMetaCtrlATrigger()) val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) - whenever(inputManager.addCustomInputGesture(any())) - .thenReturn(CUSTOM_INPUT_GESTURE_RESULT_ERROR_ALREADY_EXISTS) openAddShortcutDialogAndSetShortcut() @@ -184,11 +214,12 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { } @Test - fun uiState_errorMessage_isKeyCombinationInUse_whenKeyCombinationIsReserved() { + fun uiState_errorMessage_onSetShortcut_isKeyCombinationInUse_whenKeyCombinationIsReserved() { testScope.runTest { + inputManager.addCustomInputGesture(buildSimpleInputGestureWithMetaCtrlATrigger()) + kosmos.fakeInputManager.addCustomInputGestureErrorCode = + CUSTOM_INPUT_GESTURE_RESULT_ERROR_RESERVED_GESTURE val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) - whenever(inputManager.addCustomInputGesture(any())) - .thenReturn(CUSTOM_INPUT_GESTURE_RESULT_ERROR_RESERVED_GESTURE) openAddShortcutDialogAndSetShortcut() @@ -202,11 +233,12 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { } @Test - fun uiState_errorMessage_isGenericError_whenErrorIsUnknown() { + fun uiState_errorMessage_onSetShortcut_isGenericError_whenErrorIsUnknown() { testScope.runTest { + inputManager.addCustomInputGesture(buildSimpleInputGestureWithMetaCtrlATrigger()) + kosmos.fakeInputManager.addCustomInputGestureErrorCode = + CUSTOM_INPUT_GESTURE_RESULT_ERROR_OTHER val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) - whenever(inputManager.addCustomInputGesture(any())) - .thenReturn(CUSTOM_INPUT_GESTURE_RESULT_ERROR_OTHER) openAddShortcutDialogAndSetShortcut() @@ -219,10 +251,7 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { fun uiState_becomesInactiveAfterSuccessfullyDeletingShortcut() { testScope.runTest { val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) - whenever(inputManager.getCustomInputGestures(any())) - .thenReturn(listOf(goHomeInputGestureData, allAppsInputGestureData)) - whenever(inputManager.removeCustomInputGesture(any())) - .thenReturn(CUSTOM_INPUT_GESTURE_RESULT_SUCCESS) + inputManager.addCustomInputGesture(allAppsInputGestureData) openDeleteShortcutDialogAndDeleteShortcut() @@ -234,7 +263,6 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { fun uiState_becomesInactiveAfterSuccessfullyResettingShortcuts() { testScope.runTest { val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) - whenever(inputManager.getCustomInputGestures(any())).thenReturn(emptyList()) openResetShortcutDialogAndResetAllCustomShortcuts() @@ -297,24 +325,42 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { } } + @Test + fun uiState_pressedKeys_resetsToEmpty_onClearSelectedShortcutKeyCombination() { + testScope.runTest { + val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) + viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest) + viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed) + viewModel.onShortcutKeyCombinationSelected(keyUpEventWithActionKeyPressed) + viewModel.clearSelectedKeyCombination() + assertThat((uiState as AddShortcutDialog).pressedKeys).isEmpty() + } + } + private suspend fun openAddShortcutDialogAndSetShortcut() { - viewModel.onShortcutCustomizationRequested(allAppsShortcutAddRequest) + openAddShortcutDialogAndPressKeyCombination() + viewModel.onSetShortcut() + } + private fun openAddShortcutDialogAndPressKeyCombination() { + viewModel.onShortcutCustomizationRequested(allAppsShortcutAddRequest) viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed) viewModel.onShortcutKeyCombinationSelected(keyUpEventWithActionKeyPressed) - - viewModel.onSetShortcut() } private suspend fun openDeleteShortcutDialogAndDeleteShortcut() { viewModel.onShortcutCustomizationRequested(allAppsShortcutDeleteRequest) - viewModel.deleteShortcutCurrentlyBeingCustomized() } private suspend fun openResetShortcutDialogAndResetAllCustomShortcuts() { viewModel.onShortcutCustomizationRequested(ShortcutCustomizationRequestInfo.Reset) - viewModel.resetAllCustomShortcuts() } + + private fun buildSimpleInputGestureWithMetaCtrlATrigger() = + InputGestureData.Builder() + .setKeyGestureType(KEY_GESTURE_TYPE_HOME) + .setTrigger(createKeyTrigger(KEYCODE_A, META_CTRL_ON or META_META_ON)) + .build() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt index 3fc46b973959..cf38072912e9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt @@ -45,6 +45,7 @@ import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType. import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory import com.android.systemui.keyboard.shortcut.shared.model.shortcut +import com.android.systemui.keyboard.shortcut.shortcutHelperAccessibilityShortcutsSource import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource import com.android.systemui.keyboard.shortcut.shortcutHelperInputShortcutsSource @@ -95,6 +96,7 @@ class ShortcutHelperViewModelTest : SysuiTestCase() { it.shortcutHelperMultiTaskingShortcutsSource = fakeMultiTaskingSource it.shortcutHelperAppCategoriesShortcutsSource = FakeKeyboardShortcutGroupsSource() it.shortcutHelperInputShortcutsSource = FakeKeyboardShortcutGroupsSource() + it.shortcutHelperAccessibilityShortcutsSource = FakeKeyboardShortcutGroupsSource() it.shortcutHelperCurrentAppShortcutsSource = fakeCurrentAppsSource it.userTracker = FakeUserTracker(onCreateCurrentUserContext = { mockUserContext }) } @@ -112,9 +114,12 @@ class ShortcutHelperViewModelTest : SysuiTestCase() { fakeSystemSource.setGroups(TestShortcuts.systemGroups) fakeMultiTaskingSource.setGroups(TestShortcuts.multitaskingGroups) fakeCurrentAppsSource.setGroups(TestShortcuts.currentAppGroups) - whenever(mockPackageManager.getApplicationInfo(anyString(), eq(0))).thenReturn(mockApplicationInfo) - whenever(mockPackageManager.getApplicationLabel(mockApplicationInfo)).thenReturn("Current App") - whenever(mockPackageManager.getApplicationIcon(anyString())).thenThrow(NameNotFoundException()) + whenever(mockPackageManager.getApplicationInfo(anyString(), eq(0))) + .thenReturn(mockApplicationInfo) + whenever(mockPackageManager.getApplicationLabel(mockApplicationInfo)) + .thenReturn("Current App") + whenever(mockPackageManager.getApplicationIcon(anyString())) + .thenThrow(NameNotFoundException()) whenever(mockUserContext.packageManager).thenReturn(mockPackageManager) whenever(mockUserContext.getSystemService(INPUT_SERVICE)).thenReturn(inputManager) } @@ -278,11 +283,11 @@ class ShortcutHelperViewModelTest : SysuiTestCase() { fun shortcutsUiState_currentAppIsLauncher_defaultSelectedCategoryIsSystem() = testScope.runTest { whenever( - mockRoleManager.getRoleHoldersAsUser( - RoleManager.ROLE_HOME, - fakeUserTracker.userHandle, + mockRoleManager.getRoleHoldersAsUser( + RoleManager.ROLE_HOME, + fakeUserTracker.userHandle, + ) ) - ) .thenReturn(listOf(TestShortcuts.currentAppPackageName)) val uiState by collectLastValue(viewModel.shortcutsUiState) @@ -318,23 +323,23 @@ class ShortcutHelperViewModelTest : SysuiTestCase() { label = "System", iconSource = IconSource(imageVector = Icons.Default.Tv), shortcutCategory = - ShortcutCategory( - System, - subCategoryWithShortcutLabels("first Foo shortcut1"), - subCategoryWithShortcutLabels( - "second foO shortcut2", - subCategoryLabel = SECOND_SIMPLE_GROUP_LABEL, + ShortcutCategory( + System, + subCategoryWithShortcutLabels("first Foo shortcut1"), + subCategoryWithShortcutLabels( + "second foO shortcut2", + subCategoryLabel = SECOND_SIMPLE_GROUP_LABEL, + ), ), - ), ), ShortcutCategoryUi( label = "Multitasking", iconSource = IconSource(imageVector = Icons.Default.VerticalSplit), shortcutCategory = - ShortcutCategory( - MultiTasking, - subCategoryWithShortcutLabels("third FoO shortcut1"), - ), + ShortcutCategory( + MultiTasking, + subCategoryWithShortcutLabels("third FoO shortcut1"), + ), ), ) } @@ -420,9 +425,8 @@ class ShortcutHelperViewModelTest : SysuiTestCase() { @Test fun shortcutsUiState_shouldShowResetButton_isTrueWhenThereAreCustomShortcuts() = testScope.runTest { - whenever( - inputManager.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY) - ).thenReturn(listOf(allAppsInputGestureData)) + whenever(inputManager.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY)) + .thenReturn(listOf(allAppsInputGestureData)) val uiState by collectLastValue(viewModel.shortcutsUiState) testHelper.showFromActivity() @@ -433,7 +437,7 @@ class ShortcutHelperViewModelTest : SysuiTestCase() { @Test fun shortcutsUiState_searchQuery_isResetAfterHelperIsClosedAndReOpened() = - testScope.runTest{ + testScope.runTest { val uiState by collectLastValue(viewModel.shortcutsUiState) openHelperAndSearchForFooString() @@ -443,7 +447,7 @@ class ShortcutHelperViewModelTest : SysuiTestCase() { assertThat((uiState as? ShortcutsUiState.Active)?.searchQuery).isEqualTo("") } - private fun openHelperAndSearchForFooString(){ + private fun openHelperAndSearchForFooString() { testHelper.showFromActivity() viewModel.onSearchQueryChanged("foo") } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt index c8a16483a00c..abcbdb153e80 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt @@ -27,6 +27,8 @@ import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN import com.android.systemui.Flags import com.android.systemui.Flags.FLAG_COMMUNAL_HUB import com.android.systemui.Flags.FLAG_COMMUNAL_SCENE_KTF_REFACTOR +import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 +import com.android.systemui.Flags.glanceableHubV2 import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository import com.android.systemui.communal.domain.interactor.CommunalSceneTransitionInteractor @@ -34,6 +36,8 @@ import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.domain.interactor.communalSceneInteractor import com.android.systemui.communal.domain.interactor.communalSceneTransitionInteractor import com.android.systemui.communal.domain.interactor.setCommunalAvailable +import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled +import com.android.systemui.communal.domain.interactor.setCommunalV2Enabled import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.flags.BrokenWithSceneContainer import com.android.systemui.flags.DisableSceneContainer @@ -153,6 +157,9 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest if (!SceneContainerFlag.isEnabled) { mSetFlagsRule.disableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) } + if (glanceableHubV2()) { + kosmos.setCommunalV2ConfigEnabled(true) + } featureFlags = FakeFeatureFlags() fromLockscreenTransitionInteractor.start() @@ -1948,6 +1955,39 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest @Test @DisableSceneContainer + @EnableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR, FLAG_GLANCEABLE_HUB_V2) + fun glanceableHubToDreaming_v2() = + testScope.runTest { + kosmos.setCommunalV2Enabled(true) + + // GIVEN a device that is not dreaming or dozing + keyguardRepository.setDreamingWithOverlay(false) + keyguardRepository.setDozeTransitionModel( + DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH) + ) + advanceTimeBy(600.milliseconds) + + // GIVEN a prior transition has run to glanceable hub + communalSceneInteractor.changeScene(CommunalScenes.Communal, "test") + runCurrent() + clearInvocations(transitionRepository) + + keyguardRepository.setDreamingWithOverlay(true) + advanceTimeBy(100.milliseconds) + + assertThat(transitionRepository) + .startedTransition( + ownerName = CommunalSceneTransitionInteractor::class.simpleName, + from = KeyguardState.GLANCEABLE_HUB, + to = KeyguardState.DREAMING, + animatorAssertion = { it.isNull() }, + ) + + coroutineContext.cancelChildren() + } + + @Test + @DisableSceneContainer @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) fun glanceableHubToDreaming() = testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt index 9d5bf4dbdc3f..a276f514b779 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt @@ -1045,6 +1045,41 @@ class WindowManagerLockscreenVisibilityInteractorTest : SysuiTestCase() { assertThat(usingKeyguardGoingAwayAnimation).isFalse() } + @Test + fun aodVisibility_visibleFullyInAod_falseOtherwise() = + testScope.runTest { + val aodVisibility by collectValues(underTest.value.aodVisibility) + + transitionRepository.sendTransitionStepsThroughRunning( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + testScope, + throughValue = 0.5f, + ) + + assertEquals(listOf(false), aodVisibility) + + transitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.FINISHED, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + ) + ) + runCurrent() + + assertEquals(listOf(false, true), aodVisibility) + + transitionRepository.sendTransitionStepsThroughRunning( + from = KeyguardState.AOD, + to = KeyguardState.GONE, + testScope, + ) + runCurrent() + + assertEquals(listOf(false, true, false), aodVisibility) + } + companion object { private val progress = MutableStateFlow(0f) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt index 83b821619659..e93ed39274fb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt @@ -20,6 +20,7 @@ import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.Flags.FLAG_BOUNCER_UI_REVAMP +import com.android.systemui.Flags.FLAG_NOTIFICATION_SHADE_BLUR import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectValues import com.android.systemui.flags.BrokenWithSceneContainer @@ -97,6 +98,7 @@ class AlternateBouncerToPrimaryBouncerTransitionViewModelTest : SysuiTestCase() } @Test + @EnableFlags(FLAG_NOTIFICATION_SHADE_BLUR) fun blurRadiusGoesToMaximumWhenShadeIsExpanded() = testScope.runTest { val values by collectValues(underTest.windowBlurRadius) @@ -123,8 +125,8 @@ class AlternateBouncerToPrimaryBouncerTransitionViewModelTest : SysuiTestCase() kosmos.keyguardWindowBlurTestUtil.assertTransitionToBlurRadius( transitionProgress = listOf(0f, 0f, 0.1f, 0.2f, 0.3f, 1f), - startValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f, - endValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f, + startValue = kosmos.blurConfig.maxBlurRadiusPx, + endValue = kosmos.blurConfig.maxBlurRadiusPx, transitionFactory = ::step, actualValuesProvider = { values }, checkInterpolatedValues = false, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelTest.kt new file mode 100644 index 000000000000..38829da69c28 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.data.repository.keyguardRepository +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.lifecycle.activateIn +import com.android.systemui.media.controls.data.repository.mediaFilterRepository +import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class KeyguardMediaViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + + private val underTest = kosmos.keyguardMediaViewModelFactory.create() + + @Before + fun setUp() { + underTest.activateIn(kosmos.testScope) + } + + @Test + fun onDozing_noActiveMedia_mediaIsHidden() = + kosmos.runTest { + keyguardRepository.setIsDozing(true) + + assertThat(underTest.isMediaVisible).isFalse() + } + + @Test + fun onDozing_activeMediaExists_mediaIsHidden() = + kosmos.runTest { + val userMedia = MediaData(active = true) + + mediaFilterRepository.addSelectedUserMediaEntry(userMedia) + keyguardRepository.setIsDozing(true) + + assertThat(underTest.isMediaVisible).isFalse() + } + + @Test + fun onDeviceAwake_activeMediaExists_mediaIsVisible() = + kosmos.runTest { + val userMedia = MediaData(active = true) + + mediaFilterRepository.addSelectedUserMediaEntry(userMedia) + keyguardRepository.setIsDozing(false) + + assertThat(underTest.isMediaVisible).isTrue() + } + + @Test + fun onDeviceAwake_noActiveMedia_mediaIsHidden() = + kosmos.runTest { + keyguardRepository.setIsDozing(false) + + assertThat(underTest.isMediaVisible).isFalse() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt index fdee8d0544f0..aaca603ecf77 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt @@ -21,6 +21,7 @@ import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState import com.android.systemui.Flags.FLAG_BOUNCER_UI_REVAMP +import com.android.systemui.Flags.FLAG_NOTIFICATION_SHADE_BLUR import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues @@ -155,6 +156,7 @@ class LockscreenToPrimaryBouncerTransitionViewModelTest(flags: FlagsParameteriza } @Test + @EnableFlags(FLAG_NOTIFICATION_SHADE_BLUR) @BrokenWithSceneContainer(388068805) fun blurRadiusIsMaxWhenShadeIsExpanded() = testScope.runTest { @@ -198,8 +200,8 @@ class LockscreenToPrimaryBouncerTransitionViewModelTest(flags: FlagsParameteriza kosmos.keyguardWindowBlurTestUtil.assertTransitionToBlurRadius( transitionProgress = listOf(0f, 0f, 0.1f, 0.2f, 0.3f, 1f), - startValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f, - endValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f, + startValue = kosmos.blurConfig.maxBlurRadiusPx, + endValue = kosmos.blurConfig.maxBlurRadiusPx, transitionFactory = ::step, actualValuesProvider = { values }, checkInterpolatedValues = false, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModelTest.kt index 6db876756d3a..0951df24c56f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModelTest.kt @@ -16,50 +16,96 @@ package com.android.systemui.keyguard.ui.viewmodel -import androidx.test.ext.junit.runners.AndroidJUnit4 +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_NOTIFICATION_SHADE_BLUR import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectValues -import com.android.systemui.flags.DisableSceneContainer +import com.android.systemui.flags.BrokenWithSceneContainer +import com.android.systemui.flags.andSceneContainer import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.keyguard.ui.transitions.blurConfig import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @ExperimentalCoroutinesApi @SmallTest -@RunWith(AndroidJUnit4::class) -class OccludedToPrimaryBouncerTransitionViewModelTest : SysuiTestCase() { +@RunWith(ParameterizedAndroidJunit4::class) +class OccludedToPrimaryBouncerTransitionViewModelTest(flags: FlagsParameterization) : + SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val underTest by lazy { kosmos.occludedToPrimaryBouncerTransitionViewModel } + private lateinit var underTest: OccludedToPrimaryBouncerTransitionViewModel + + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf().andSceneContainer() + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + + @Before + fun setup() { + underTest = kosmos.occludedToPrimaryBouncerTransitionViewModel + } @Test - @DisableSceneContainer - fun blurBecomesMaxValueImmediately() = + @BrokenWithSceneContainer(388068805) + fun notificationsAreBlurredImmediatelyWhenBouncerIsOpenedAndShadeIsExpanded() = + testScope.runTest { + val values by collectValues(underTest.notificationBlurRadius) + kosmos.keyguardWindowBlurTestUtil.shadeExpanded(true) + + kosmos.keyguardWindowBlurTestUtil.assertTransitionToBlurRadius( + transitionProgress = listOf(0.0f, 0.2f, 0.3f, 0.65f, 0.7f, 1.0f), + startValue = kosmos.blurConfig.maxBlurRadiusPx, + endValue = kosmos.blurConfig.maxBlurRadiusPx, + actualValuesProvider = { values }, + transitionFactory = ::step, + checkInterpolatedValues = false, + ) + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_SHADE_BLUR) + @BrokenWithSceneContainer(388068805) + fun blurBecomesMaxValueImmediatelyWhenShadeIsAlreadyExpanded() = testScope.runTest { val values by collectValues(underTest.windowBlurRadius) + kosmos.keyguardWindowBlurTestUtil.shadeExpanded(true) kosmos.keyguardWindowBlurTestUtil.assertTransitionToBlurRadius( transitionProgress = listOf(0.0f, 0.2f, 0.3f, 0.65f, 0.7f, 1.0f), startValue = kosmos.blurConfig.maxBlurRadiusPx, endValue = kosmos.blurConfig.maxBlurRadiusPx, actualValuesProvider = { values }, - transitionFactory = { step, transitionState -> - TransitionStep( - from = KeyguardState.OCCLUDED, - to = KeyguardState.PRIMARY_BOUNCER, - value = step, - transitionState = transitionState, - ownerName = "OccludedToPrimaryBouncerTransitionViewModelTest", - ) - }, + transitionFactory = ::step, checkInterpolatedValues = false, ) } + + fun step(value: Float, state: TransitionState = TransitionState.RUNNING): TransitionStep { + return TransitionStep( + from = KeyguardState.OCCLUDED, + to = KeyguardState.PRIMARY_BOUNCER, + value = value, + transitionState = state, + ownerName = "OccludedToPrimaryBouncerTransitionViewModelTest", + ) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt index 0db0c5fe8482..8fefb8d40b71 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt @@ -16,12 +16,16 @@ package com.android.systemui.keyguard.ui.viewmodel -import androidx.test.ext.junit.runners.AndroidJUnit4 +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_NOTIFICATION_SHADE_BLUR import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues +import com.android.systemui.flags.BrokenWithSceneContainer +import com.android.systemui.flags.andSceneContainer import com.android.systemui.keyguard.data.repository.biometricSettingsRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState @@ -35,13 +39,17 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @ExperimentalCoroutinesApi @SmallTest -@RunWith(AndroidJUnit4::class) -class PrimaryBouncerToLockscreenTransitionViewModelTest : SysuiTestCase() { +@RunWith(ParameterizedAndroidJunit4::class) +class PrimaryBouncerToLockscreenTransitionViewModelTest(flags: FlagsParameterization) : + SysuiTestCase() { val kosmos = testKosmos() val testScope = kosmos.testScope @@ -49,9 +57,27 @@ class PrimaryBouncerToLockscreenTransitionViewModelTest : SysuiTestCase() { val fingerprintPropertyRepository = kosmos.fingerprintPropertyRepository val biometricSettingsRepository = kosmos.biometricSettingsRepository - val underTest = kosmos.primaryBouncerToLockscreenTransitionViewModel + private lateinit var underTest: PrimaryBouncerToLockscreenTransitionViewModel + + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf().andSceneContainer() + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + + @Before + fun setup() { + underTest = kosmos.primaryBouncerToLockscreenTransitionViewModel + } @Test + @BrokenWithSceneContainer(392346450) fun lockscreenAlphaStartsFromViewStateAccessorAlpha() = testScope.runTest { val viewState = ViewStateAccessor(alpha = { 0.5f }) @@ -70,6 +96,7 @@ class PrimaryBouncerToLockscreenTransitionViewModelTest : SysuiTestCase() { } @Test + @BrokenWithSceneContainer(392346450) fun deviceEntryParentViewAlpha() = testScope.runTest { val deviceEntryParentViewAlpha by collectLastValue(underTest.deviceEntryParentViewAlpha) @@ -89,6 +116,7 @@ class PrimaryBouncerToLockscreenTransitionViewModelTest : SysuiTestCase() { } @Test + @BrokenWithSceneContainer(392346450) fun deviceEntryBackgroundViewAlpha_udfpsEnrolled_show() = testScope.runTest { fingerprintPropertyRepository.supportsUdfps() @@ -113,6 +141,7 @@ class PrimaryBouncerToLockscreenTransitionViewModelTest : SysuiTestCase() { } @Test + @BrokenWithSceneContainer(388068805) fun blurRadiusGoesFromMaxToMinWhenShadeIsNotExpanded() = testScope.runTest { val values by collectValues(underTest.windowBlurRadius) @@ -128,6 +157,8 @@ class PrimaryBouncerToLockscreenTransitionViewModelTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_NOTIFICATION_SHADE_BLUR) + @BrokenWithSceneContainer(388068805) fun blurRadiusRemainsAtMaxWhenShadeIsExpanded() = testScope.runTest { val values by collectValues(underTest.windowBlurRadius) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModelTest.kt index b0b4af5fea5b..fd7fb9f863c8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModelTest.kt @@ -16,11 +16,13 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectValues -import com.android.systemui.flags.DisableSceneContainer +import com.android.systemui.flags.BrokenWithSceneContainer import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.keyguard.ui.transitions.blurConfig @@ -40,10 +42,37 @@ class PrimaryBouncerToOccludedTransitionViewModelTest : SysuiTestCase() { private val underTest by lazy { kosmos.primaryBouncerToOccludedTransitionViewModel } @Test - @DisableSceneContainer - fun blurBecomesMaxValueImmediately() = + @BrokenWithSceneContainer(388068805) + fun blurBecomesMinValueImmediatelyWhenShadeIsNotExpanded() = testScope.runTest { val values by collectValues(underTest.windowBlurRadius) + kosmos.keyguardWindowBlurTestUtil.shadeExpanded(false) + + kosmos.keyguardWindowBlurTestUtil.assertTransitionToBlurRadius( + transitionProgress = listOf(0.0f, 0.2f, 0.3f, 0.65f, 0.7f, 1.0f), + startValue = kosmos.blurConfig.minBlurRadiusPx, + endValue = kosmos.blurConfig.minBlurRadiusPx, + actualValuesProvider = { values }, + transitionFactory = { step, transitionState -> + TransitionStep( + from = KeyguardState.PRIMARY_BOUNCER, + to = KeyguardState.OCCLUDED, + value = step, + transitionState = transitionState, + ownerName = "PrimaryBouncerToOccludedTransitionViewModelTest", + ) + }, + checkInterpolatedValues = false, + ) + } + + @Test + @BrokenWithSceneContainer(388068805) + @EnableFlags(Flags.FLAG_NOTIFICATION_SHADE_BLUR) + fun blurBecomesMaxValueImmediatelyWhenShadeIsExpanded() = + testScope.runTest { + val values by collectValues(underTest.windowBlurRadius) + kosmos.keyguardWindowBlurTestUtil.shadeExpanded(false) kosmos.keyguardWindowBlurTestUtil.assertTransitionToBlurRadius( transitionProgress = listOf(0.0f, 0.2f, 0.3f, 0.65f, 0.7f, 1.0f), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java index 83cae49fca2f..7478464772a4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java @@ -259,7 +259,6 @@ public class MediaOutputAdapterTest extends SysuiTestCase { mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); assertThat(mViewHolder.mSeekBar.getContentDescription()).isNotNull(); - assertThat(mViewHolder.mSeekBar.getAccessibilityDelegate()).isNotNull(); assertThat(mViewHolder.mContainerLayout.isFocusable()).isFalse(); } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/OWNERS b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/OWNERS new file mode 100644 index 000000000000..739d2ac2e87b --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/OWNERS @@ -0,0 +1 @@ +file:/packages/SystemUI/src/com/android/systemui/media/dialog/OWNERS diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/NavBarHelperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/NavBarHelperTest.java index a770ee199ba6..c1872f05aa1d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/NavBarHelperTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/NavBarHelperTest.java @@ -57,7 +57,7 @@ import com.android.systemui.accessibility.SystemActions; import com.android.systemui.assist.AssistManager; import com.android.systemui.dump.DumpManager; import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler; -import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.recents.LauncherProxyService; import com.android.systemui.settings.DisplayTracker; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.CommandQueue; @@ -104,7 +104,7 @@ public class NavBarHelperTest extends SysuiTestCase { @Mock SystemActions mSystemActions; @Mock - OverviewProxyService mOverviewProxyService; + LauncherProxyService mLauncherProxyService; @Mock Lazy<AssistManager> mAssistManagerLazy; @Mock @@ -161,7 +161,7 @@ public class NavBarHelperTest extends SysuiTestCase { mNavBarHelper = new NavBarHelper(mContext, mAccessibilityManager, mAccessibilityButtonModeObserver, mAccessibilityButtonTargetObserver, mAccessibilityGestureTargetObserver, - mSystemActions, mOverviewProxyService, mAssistManagerLazy, + mSystemActions, mLauncherProxyService, mAssistManagerLazy, () -> Optional.of(mock(CentralSurfaces.class)), mock(KeyguardStateController.class), mNavigationModeController, mEdgeBackGestureHandlerFactory, mWm, mUserTracker, mDisplayTracker, mNotificationShadeWindowController, mConfigurationController, @@ -171,7 +171,7 @@ public class NavBarHelperTest extends SysuiTestCase { @Test public void registerListenersInCtor() { verify(mNavigationModeController, times(1)).addListener(mNavBarHelper); - verify(mOverviewProxyService, times(1)).addCallback(mNavBarHelper); + verify(mLauncherProxyService, times(1)).addCallback(mNavBarHelper); verify(mCommandQueue, times(1)).addCallback(any()); } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt index 9bae7bd72f7d..cf0a25020d93 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt @@ -9,7 +9,7 @@ import com.android.systemui.dump.DumpManager import com.android.systemui.model.SysUiState import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler import com.android.systemui.plugins.statusbar.StatusBarStateController -import com.android.systemui.recents.OverviewProxyService +import com.android.systemui.recents.LauncherProxyService import com.android.systemui.settings.DisplayTracker import com.android.systemui.shared.system.QuickStepContract import com.android.systemui.shared.system.TaskStackChangeListeners @@ -49,7 +49,7 @@ class TaskbarDelegateTest : SysuiTestCase() { @Mock lateinit var mLightBarControllerFactory: LightBarTransitionsController.Factory @Mock lateinit var mLightBarTransitionController: LightBarTransitionsController @Mock lateinit var mCommandQueue: CommandQueue - @Mock lateinit var mOverviewProxyService: OverviewProxyService + @Mock lateinit var mLauncherProxyService: LauncherProxyService @Mock lateinit var mNavBarHelper: NavBarHelper @Mock lateinit var mNavigationModeController: NavigationModeController @Mock lateinit var mSysUiState: SysUiState @@ -87,7 +87,7 @@ class TaskbarDelegateTest : SysuiTestCase() { ) mTaskbarDelegate.setDependencies( mCommandQueue, - mOverviewProxyService, + mLauncherProxyService, mNavBarHelper, mNavigationModeController, mSysUiState, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarButtonTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarButtonTest.java index 00e79f5a3ac2..fd4bb4bb8a37 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarButtonTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarButtonTest.java @@ -40,7 +40,7 @@ import com.android.systemui.SysuiTestableContext; import com.android.systemui.assist.AssistManager; import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler; -import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.recents.LauncherProxyService; import com.android.systemui.settings.FakeDisplayTracker; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -80,7 +80,7 @@ public class NavigationBarButtonTest extends SysuiTestCase { .thenReturn(mEdgeBackGestureHandler); mDependency.injectMockDependency(AssistManager.class); - mDependency.injectMockDependency(OverviewProxyService.class); + mDependency.injectMockDependency(LauncherProxyService.class); mDependency.injectMockDependency(KeyguardStateController.class); mDependency.injectMockDependency(NavigationBarController.class); mDependency.injectTestDependency(EdgeBackGestureHandler.Factory.class, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarInflaterViewTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarInflaterViewTest.java index e58c8f281fc1..85c093c16d88 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarInflaterViewTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarInflaterViewTest.java @@ -35,7 +35,7 @@ import com.android.systemui.assist.AssistManager; import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.navigationbar.views.buttons.ButtonDispatcher; -import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.recents.LauncherProxyService; import org.junit.After; import org.junit.Before; @@ -55,7 +55,7 @@ public class NavigationBarInflaterViewTest extends SysuiTestCase { @Before public void setUp() { mDependency.injectMockDependency(AssistManager.class); - mDependency.injectMockDependency(OverviewProxyService.class); + mDependency.injectMockDependency(LauncherProxyService.class); mDependency.injectMockDependency(NavigationModeController.class); mDependency.injectMockDependency(NavigationBarController.class); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java index 7f9313cbeb5b..09e49eb217b0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java @@ -16,9 +16,9 @@ package com.android.systemui.navigationbar.views; -import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT; -import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN; -import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN; +import static android.app.StatusBarManager.NAVBAR_BACK_DISMISS_IME; +import static android.app.StatusBarManager.NAVBAR_IME_SWITCHER_BUTTON_VISIBLE; +import static android.app.StatusBarManager.NAVBAR_IME_VISIBLE; import static android.inputmethodservice.InputMethodService.BACK_DISPOSITION_ADJUST_NOTHING; import static android.inputmethodservice.InputMethodService.BACK_DISPOSITION_DEFAULT; import static android.inputmethodservice.InputMethodService.IME_VISIBLE; @@ -31,8 +31,9 @@ import static com.android.systemui.assist.AssistManager.INVOCATION_TYPE_HOME_BUT import static com.android.systemui.navigationbar.views.NavigationBar.NavBarActionEvent.NAVBAR_ASSIST_LONGPRESS; import static com.android.systemui.navigationbar.views.buttons.KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_LONGPRESS; import static com.android.systemui.navigationbar.views.buttons.KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_TAP; -import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING; -import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BACK_DISMISS_IME; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_VISIBLE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING; import static com.google.common.truth.Truth.assertThat; @@ -105,7 +106,7 @@ import com.android.systemui.navigationbar.views.buttons.KeyButtonView; import com.android.systemui.navigationbar.views.buttons.NavBarButtonClickLogger; import com.android.systemui.navigationbar.views.buttons.NavbarOrientationTrackingLogger; import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.recents.LauncherProxyService; import com.android.systemui.recents.Recents; import com.android.systemui.settings.DisplayTracker; import com.android.systemui.settings.FakeDisplayTracker; @@ -184,7 +185,7 @@ public class NavigationBarTest extends SysuiTestCase { @Mock private SystemActions mSystemActions; @Mock - private OverviewProxyService mOverviewProxyService; + private LauncherProxyService mLauncherProxyService; @Mock private StatusBarStateController mStatusBarStateController; @Mock @@ -284,14 +285,14 @@ public class NavigationBarTest extends SysuiTestCase { mDependency.injectMockDependency(KeyguardStateController.class); mDependency.injectTestDependency(StatusBarStateController.class, mStatusBarStateController); mDependency.injectMockDependency(NavigationBarController.class); - mDependency.injectTestDependency(OverviewProxyService.class, mOverviewProxyService); + mDependency.injectTestDependency(LauncherProxyService.class, mLauncherProxyService); mDependency.injectTestDependency(NavigationModeController.class, mNavigationModeController); TestableLooper.get(this).runWithLooper(() -> { mNavBarHelper = spy(new NavBarHelper(mContext, mock(AccessibilityManager.class), mock(AccessibilityButtonModeObserver.class), mock(AccessibilityButtonTargetsObserver.class), mock(AccessibilityGestureTargetsObserver.class), - mSystemActions, mOverviewProxyService, + mSystemActions, mLauncherProxyService, () -> mock(AssistManager.class), () -> Optional.of(mCentralSurfaces), mKeyguardStateController, mock(NavigationModeController.class), mEdgeBackGestureHandlerFactory, mock(IWindowManager.class), @@ -500,8 +501,9 @@ public class NavigationBarTest extends SysuiTestCase { mNavigationBar.setImeWindowStatus(DEFAULT_DISPLAY, IME_VISIBLE, BACK_DISPOSITION_DEFAULT, true /* showImeSwitcher */); - verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_SHOWING), eq(true)); - verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_SWITCHER_SHOWING), eq(true)); + verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_VISIBLE), eq(true)); + verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE), eq(true)); + verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_BACK_DISMISS_IME), eq(true)); } /** @@ -514,8 +516,9 @@ public class NavigationBarTest extends SysuiTestCase { mNavigationBar.setImeWindowStatus(DEFAULT_DISPLAY, IME_VISIBLE, BACK_DISPOSITION_DEFAULT, false /* showImeSwitcher */); - verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_SHOWING), eq(true)); - verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_SWITCHER_SHOWING), eq(false)); + verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_VISIBLE), eq(true)); + verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE), eq(false)); + verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_BACK_DISMISS_IME), eq(true)); } /** @@ -531,8 +534,9 @@ public class NavigationBarTest extends SysuiTestCase { mNavigationBar.setImeWindowStatus(DEFAULT_DISPLAY, 0 /* vis */, BACK_DISPOSITION_DEFAULT, true /* showImeSwitcher */); - verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_SHOWING), eq(false)); - verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_SWITCHER_SHOWING), eq(false)); + verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_VISIBLE), eq(false)); + verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE), eq(false)); + verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_BACK_DISMISS_IME), eq(false)); } /** @@ -545,8 +549,9 @@ public class NavigationBarTest extends SysuiTestCase { mNavigationBar.setImeWindowStatus(DEFAULT_DISPLAY, IME_VISIBLE, BACK_DISPOSITION_ADJUST_NOTHING, true /* showImeSwitcher */); - verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_SHOWING), eq(true)); - verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_SWITCHER_SHOWING), eq(true)); + verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_VISIBLE), eq(true)); + verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE), eq(true)); + verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_BACK_DISMISS_IME), eq(false)); } @Test @@ -564,29 +569,27 @@ public class NavigationBarTest extends SysuiTestCase { externalNavBar.init(); defaultNavBar.setImeWindowStatus(DEFAULT_DISPLAY, IME_VISIBLE, - BACK_DISPOSITION_DEFAULT, true); + BACK_DISPOSITION_DEFAULT, true /* showImeSwitcher */); // Verify IME window state will be updated in default NavBar & external NavBar state reset. - assertEquals(NAVIGATION_HINT_BACK_ALT | NAVIGATION_HINT_IME_SHOWN - | NAVIGATION_HINT_IME_SWITCHER_SHOWN, - defaultNavBar.getNavigationIconHints()); - assertFalse((externalNavBar.getNavigationIconHints() & NAVIGATION_HINT_BACK_ALT) != 0); - assertFalse((externalNavBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SHOWN) != 0); - assertFalse((externalNavBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SWITCHER_SHOWN) - != 0); + assertEquals(NAVBAR_BACK_DISMISS_IME | NAVBAR_IME_VISIBLE + | NAVBAR_IME_SWITCHER_BUTTON_VISIBLE, + defaultNavBar.getNavbarFlags()); + assertFalse((externalNavBar.getNavbarFlags() & NAVBAR_BACK_DISMISS_IME) != 0); + assertFalse((externalNavBar.getNavbarFlags() & NAVBAR_IME_VISIBLE) != 0); + assertFalse((externalNavBar.getNavbarFlags() & NAVBAR_IME_SWITCHER_BUTTON_VISIBLE) != 0); externalNavBar.setImeWindowStatus(EXTERNAL_DISPLAY_ID, IME_VISIBLE, - BACK_DISPOSITION_DEFAULT, true); + BACK_DISPOSITION_DEFAULT, true /* showImeSwitcher */); defaultNavBar.setImeWindowStatus(DEFAULT_DISPLAY, 0 /* vis */, - BACK_DISPOSITION_DEFAULT, false); + BACK_DISPOSITION_DEFAULT, false /* showImeSwitcher */); // Verify IME window state will be updated in external NavBar & default NavBar state reset. - assertEquals(NAVIGATION_HINT_BACK_ALT | NAVIGATION_HINT_IME_SHOWN - | NAVIGATION_HINT_IME_SWITCHER_SHOWN, - externalNavBar.getNavigationIconHints()); - assertFalse((defaultNavBar.getNavigationIconHints() & NAVIGATION_HINT_BACK_ALT) != 0); - assertFalse((defaultNavBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SHOWN) != 0); - assertFalse((defaultNavBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SWITCHER_SHOWN) - != 0); + assertEquals(NAVBAR_BACK_DISMISS_IME | NAVBAR_IME_VISIBLE + | NAVBAR_IME_SWITCHER_BUTTON_VISIBLE, + externalNavBar.getNavbarFlags()); + assertFalse((defaultNavBar.getNavbarFlags() & NAVBAR_BACK_DISMISS_IME) != 0); + assertFalse((defaultNavBar.getNavbarFlags() & NAVBAR_IME_VISIBLE) != 0); + assertFalse((defaultNavBar.getNavbarFlags() & NAVBAR_IME_SWITCHER_BUTTON_VISIBLE) != 0); } @Test @@ -601,32 +604,29 @@ public class NavigationBarTest extends SysuiTestCase { // Verify navbar altered back icon when an app is showing IME mNavigationBar.setImeWindowStatus(DEFAULT_DISPLAY, IME_VISIBLE, - BACK_DISPOSITION_DEFAULT, true); - assertTrue((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_BACK_ALT) != 0); - assertTrue((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SHOWN) != 0); - assertTrue((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SWITCHER_SHOWN) - != 0); + BACK_DISPOSITION_DEFAULT, true /* showImeSwitcher */); + assertTrue((mNavigationBar.getNavbarFlags() & NAVBAR_BACK_DISMISS_IME) != 0); + assertTrue((mNavigationBar.getNavbarFlags() & NAVBAR_IME_VISIBLE) != 0); + assertTrue((mNavigationBar.getNavbarFlags() & NAVBAR_IME_SWITCHER_BUTTON_VISIBLE) != 0); // Verify navbar didn't alter and showing back icon when the keyguard is showing without // requesting IME insets visible. doReturn(true).when(mKeyguardStateController).isShowing(); mNavigationBar.setImeWindowStatus(DEFAULT_DISPLAY, IME_VISIBLE, - BACK_DISPOSITION_DEFAULT, true); - assertFalse((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_BACK_ALT) != 0); - assertFalse((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SHOWN) != 0); - assertFalse((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SWITCHER_SHOWN) - != 0); + BACK_DISPOSITION_DEFAULT, true /* showImeSwitcher */); + assertFalse((mNavigationBar.getNavbarFlags() & NAVBAR_BACK_DISMISS_IME) != 0); + assertFalse((mNavigationBar.getNavbarFlags() & NAVBAR_IME_VISIBLE) != 0); + assertFalse((mNavigationBar.getNavbarFlags() & NAVBAR_IME_SWITCHER_BUTTON_VISIBLE) != 0); // Verify navbar altered and showing back icon when the keyguard is showing and // requesting IME insets visible. windowInsets = new WindowInsets.Builder().setVisible(ime(), true).build(); doReturn(windowInsets).when(mockShadeWindowView).getRootWindowInsets(); mNavigationBar.setImeWindowStatus(DEFAULT_DISPLAY, IME_VISIBLE, - BACK_DISPOSITION_DEFAULT, true); - assertTrue((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_BACK_ALT) != 0); - assertTrue((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SHOWN) != 0); - assertTrue((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SWITCHER_SHOWN) - != 0); + BACK_DISPOSITION_DEFAULT, true /* showImeSwitcher */); + assertTrue((mNavigationBar.getNavbarFlags() & NAVBAR_BACK_DISMISS_IME) != 0); + assertTrue((mNavigationBar.getNavbarFlags() & NAVBAR_IME_VISIBLE) != 0); + assertTrue((mNavigationBar.getNavbarFlags() & NAVBAR_IME_SWITCHER_BUTTON_VISIBLE) != 0); } @Test @@ -690,7 +690,7 @@ public class NavigationBarTest extends SysuiTestCase { mock(AccessibilityManager.class), deviceProvisionedController, new MetricsLogger(), - mOverviewProxyService, + mLauncherProxyService, mNavigationModeController, mStatusBarStateController, mStatusBarKeyguardViewManager, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarTransitionsTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarTransitionsTest.java index 3621ab975daf..cff9beccc729 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarTransitionsTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarTransitionsTest.java @@ -36,7 +36,7 @@ import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler; import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.recents.LauncherProxyService; import com.android.systemui.settings.FakeDisplayTracker; import com.android.systemui.shared.statusbar.phone.BarTransitions; import com.android.systemui.statusbar.phone.LightBarTransitionsController; @@ -72,7 +72,7 @@ public class NavigationBarTransitionsTest extends SysuiTestCase { when(mEdgeBackGestureHandlerFactory.create(any(Context.class))) .thenReturn(mEdgeBackGestureHandler); mDependency.injectMockDependency(AssistManager.class); - mDependency.injectMockDependency(OverviewProxyService.class); + mDependency.injectMockDependency(LauncherProxyService.class); mDependency.injectMockDependency(StatusBarStateController.class); mDependency.injectMockDependency(KeyguardStateController.class); mDependency.injectMockDependency(NavigationBarController.class); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/buttons/KeyButtonViewTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/buttons/KeyButtonViewTest.java index 403a883e1760..58ec0c7a0e72 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/buttons/KeyButtonViewTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/buttons/KeyButtonViewTest.java @@ -51,7 +51,7 @@ import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; import com.android.systemui.SysuiTestCase; import com.android.systemui.assist.AssistManager; -import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.recents.LauncherProxyService; import org.junit.Before; import org.junit.Test; @@ -76,7 +76,7 @@ public class KeyButtonViewTest extends SysuiTestCase { public void setup() throws Exception { MockitoAnnotations.initMocks(this); mMetricsLogger = mDependency.injectMockDependency(MetricsLogger.class); - mDependency.injectMockDependency(OverviewProxyService.class); + mDependency.injectMockDependency(LauncherProxyService.class); mDependency.injectMockDependency(AssistManager.class); mUiEventLogger = mDependency.injectMockDependency(UiEventLogger.class); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt index 855931c32671..52b9e47e6d3d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt @@ -21,7 +21,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.compose.animation.scene.Back import com.android.compose.animation.scene.Swipe -import com.android.compose.animation.scene.UserActionResult +import com.android.compose.animation.scene.UserActionResult.HideOverlay +import com.android.compose.animation.scene.UserActionResult.ShowOverlay +import com.android.compose.animation.scene.UserActionResult.ShowOverlay.HideCurrentOverlays import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.EnableSceneContainer @@ -53,7 +55,7 @@ class NotificationsShadeOverlayActionsViewModelTest : SysuiTestCase() { val actions by collectLastValue(underTest.actions) underTest.activateIn(this) - assertThat((actions?.get(Swipe.Up) as? UserActionResult.HideOverlay)?.overlay) + assertThat((actions?.get(Swipe.Up) as? HideOverlay)?.overlay) .isEqualTo(Overlays.NotificationsShade) assertThat(actions?.get(Swipe.Down)).isNull() } @@ -64,7 +66,7 @@ class NotificationsShadeOverlayActionsViewModelTest : SysuiTestCase() { val actions by collectLastValue(underTest.actions) underTest.activateIn(this) - assertThat((actions?.get(Back) as? UserActionResult.HideOverlay)?.overlay) + assertThat((actions?.get(Back) as? HideOverlay)?.overlay) .isEqualTo(Overlays.NotificationsShade) } @@ -74,11 +76,11 @@ class NotificationsShadeOverlayActionsViewModelTest : SysuiTestCase() { val actions by collectLastValue(underTest.actions) underTest.activateIn(this) - assertThat( - (actions?.get(Swipe.Down(fromSource = SceneContainerEdge.TopRight)) - as? UserActionResult.ReplaceByOverlay) - ?.overlay - ) - .isEqualTo(Overlays.QuickSettingsShade) + val action = + (actions?.get(Swipe.Down(fromSource = SceneContainerEdge.TopRight)) as? ShowOverlay) + assertThat(action?.overlay).isEqualTo(Overlays.QuickSettingsShade) + val overlaysToHide = action?.hideCurrentOverlays as? HideCurrentOverlays.Some + assertThat(overlaysToHide).isNotNull() + assertThat(overlaysToHide?.overlays).containsExactly(Overlays.NotificationsShade) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModelTest.kt deleted file mode 100644 index 46b02e92a4f9..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModelTest.kt +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.notifications.ui.viewmodel - -import android.testing.TestableLooper -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.android.compose.animation.scene.Swipe -import com.android.compose.animation.scene.UserActionResult -import com.android.systemui.SysuiTestCase -import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository -import com.android.systemui.authentication.shared.model.AuthenticationMethodModel -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository -import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor -import com.android.systemui.flags.EnableSceneContainer -import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository -import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor -import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus -import com.android.systemui.kosmos.testScope -import com.android.systemui.lifecycle.activateIn -import com.android.systemui.scene.domain.interactor.sceneInteractor -import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver -import com.android.systemui.scene.shared.model.SceneFamilies -import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.shade.ui.viewmodel.notificationsShadeUserActionsViewModel -import com.android.systemui.testKosmos -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.junit.runner.RunWith - -@OptIn(ExperimentalCoroutinesApi::class) -@SmallTest -@RunWith(AndroidJUnit4::class) -@TestableLooper.RunWithLooper -@EnableSceneContainer -class NotificationsShadeUserActionsViewModelTest : SysuiTestCase() { - - private val kosmos = testKosmos() - private val testScope = kosmos.testScope - private val sceneInteractor by lazy { kosmos.sceneInteractor } - private val deviceUnlockedInteractor by lazy { kosmos.deviceUnlockedInteractor } - - private val underTest by lazy { kosmos.notificationsShadeUserActionsViewModel } - - @Test - fun upTransitionSceneKey_deviceLocked_lockscreen() = - testScope.runTest { - val actions by collectLastValue(underTest.actions) - lockDevice() - underTest.activateIn(this) - - assertThat((actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene) - .isEqualTo(SceneFamilies.Home) - assertThat(actions?.get(Swipe.Down)).isNull() - assertThat(kosmos.homeSceneFamilyResolver.resolvedScene.value) - .isEqualTo(Scenes.Lockscreen) - } - - @Test - fun upTransitionSceneKey_deviceLocked_keyguardDisabled_gone() = - testScope.runTest { - val actions by collectLastValue(underTest.actions) - lockDevice() - kosmos.keyguardEnabledInteractor.notifyKeyguardEnabled(false) - underTest.activateIn(this) - - assertThat((actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene) - .isEqualTo(SceneFamilies.Home) - assertThat(kosmos.homeSceneFamilyResolver.resolvedScene.value).isEqualTo(Scenes.Gone) - } - - @Test - fun upTransitionSceneKey_deviceUnlocked_gone() = - testScope.runTest { - val actions by collectLastValue(underTest.actions) - lockDevice() - unlockDevice() - underTest.activateIn(this) - - assertThat((actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene) - .isEqualTo(SceneFamilies.Home) - assertThat(actions?.get(Swipe.Down)).isNull() - assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Gone) - } - - @Test - fun upTransitionSceneKey_authMethodSwipe_lockscreenNotDismissed_goesToLockscreen() = - testScope.runTest { - val actions by collectLastValue(underTest.actions) - kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) - kosmos.fakeAuthenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.None - ) - sceneInteractor.changeScene(Scenes.Lockscreen, "reason") - underTest.activateIn(this) - - assertThat((actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene) - .isEqualTo(SceneFamilies.Home) - assertThat(kosmos.homeSceneFamilyResolver.resolvedScene.value) - .isEqualTo(Scenes.Lockscreen) - } - - @Test - fun upTransitionSceneKey_authMethodSwipe_lockscreenDismissed_goesToGone() = - testScope.runTest { - val deviceUnlockStatus by collectLastValue(deviceUnlockedInteractor.deviceUnlockStatus) - val actions by collectLastValue(underTest.actions) - kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) - kosmos.fakeAuthenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.None - ) - assertThat(deviceUnlockStatus?.isUnlocked).isTrue() - sceneInteractor // force the lazy; this will kick off StateFlows - runCurrent() - sceneInteractor.changeScene(Scenes.Gone, "reason") - underTest.activateIn(this) - - assertThat((actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene) - .isEqualTo(SceneFamilies.Home) - assertThat(kosmos.homeSceneFamilyResolver.resolvedScene.value).isEqualTo(Scenes.Gone) - } - - private fun TestScope.lockDevice() { - val deviceUnlockStatus by collectLastValue(deviceUnlockedInteractor.deviceUnlockStatus) - - kosmos.fakeAuthenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - assertThat(deviceUnlockStatus?.isUnlocked).isFalse() - sceneInteractor.changeScene(Scenes.Lockscreen, "reason") - runCurrent() - } - - private fun TestScope.unlockDevice() { - val deviceUnlockStatus by collectLastValue(deviceUnlockedInteractor.deviceUnlockStatus) - - kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( - SuccessFingerprintAuthenticationStatus(0, true) - ) - assertThat(deviceUnlockStatus?.isUnlocked).isTrue() - sceneInteractor.changeScene(Scenes.Gone, "reason") - runCurrent() - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java index e9633f49f76d..ff005c2b767a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java @@ -16,7 +16,6 @@ package com.android.systemui.qs; -import static com.android.systemui.Flags.FLAG_QUICK_SETTINGS_VISUAL_HAPTICS_LONGPRESS; import static com.android.systemui.flags.SceneContainerFlagParameterizationKt.parameterizeSceneContainerFlag; import static com.google.common.truth.Truth.assertThat; @@ -41,8 +40,6 @@ import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow; import android.content.res.Configuration; import android.content.res.Resources; -import android.platform.test.annotations.DisableFlags; -import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.FlagsParameterization; import android.testing.TestableLooper.RunWithLooper; import android.view.ContextThemeWrapper; @@ -87,6 +84,7 @@ import javax.inject.Provider; import kotlinx.coroutines.flow.MutableStateFlow; import kotlinx.coroutines.flow.StateFlow; + import platform.test.runner.parameterized.ParameterizedAndroidJunit4; import platform.test.runner.parameterized.Parameters; @@ -505,7 +503,6 @@ public class QSPanelControllerBaseTest extends SysuiTestCase { } @Test - @EnableFlags(FLAG_QUICK_SETTINGS_VISUAL_HAPTICS_LONGPRESS) public void setTiles_longPressEffectEnabled_nonNullLongPressEffectsAreProvided() { mLongPressEffectProvider.mEffectsProvided = 0; when(mQSHost.getTiles()).thenReturn(List.of(mQSTile, mOtherTile)); @@ -516,16 +513,6 @@ public class QSPanelControllerBaseTest extends SysuiTestCase { } @Test - @DisableFlags(FLAG_QUICK_SETTINGS_VISUAL_HAPTICS_LONGPRESS) - public void setTiles_longPressEffectDisabled_noLongPressEffectsAreProvided() { - mLongPressEffectProvider.mEffectsProvided = 0; - when(mQSHost.getTiles()).thenReturn(List.of(mQSTile, mOtherTile)); - mController.setTiles(); - - assertThat(mLongPressEffectProvider.mEffectsProvided).isEqualTo(0); - } - - @Test public void setTiles_differentTiles_extraTileRemoved() { when(mQSHost.getTiles()).thenReturn(List.of(mQSTile, mOtherTile)); mController.setTiles(); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt index fecd8c3cacca..4c834b396df6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt @@ -37,6 +37,7 @@ import com.android.systemui.qs.QsEventLogger import com.android.systemui.qs.flags.QSComposeFragment import com.android.systemui.qs.flags.QsDetailedView import com.android.systemui.qs.logging.QSLogger +import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel import com.android.systemui.qs.tiles.dialog.InternetDialogManager import com.android.systemui.qs.tiles.dialog.WifiStateWorker import com.android.systemui.res.R @@ -109,6 +110,7 @@ class InternetTileNewImplTest(flags: FlagsParameterization) : SysuiTestCase() { @Mock private lateinit var dialogManager: InternetDialogManager @Mock private lateinit var wifiStateWorker: WifiStateWorker @Mock private lateinit var accessPointController: AccessPointController + @Mock private lateinit var internetDetailsViewModelFactory: InternetDetailsViewModel.Factory @Before fun setUp() { @@ -145,6 +147,7 @@ class InternetTileNewImplTest(flags: FlagsParameterization) : SysuiTestCase() { dialogManager, wifiStateWorker, accessPointController, + internetDetailsViewModelFactory ) underTest.initialize() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ModesTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ModesTileTest.kt index f005375a2ef9..7bb28dbabd5e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ModesTileTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ModesTileTest.kt @@ -49,6 +49,7 @@ import com.android.systemui.statusbar.policy.data.repository.zenModeRepository import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate import com.android.systemui.statusbar.policy.ui.dialog.modesDialogEventLogger +import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.modesDialogViewModel import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.settings.FakeSettings @@ -146,6 +147,7 @@ class ModesTileTest : SysuiTestCase() { tileDataInteractor, mapper, userActionInteractor, + kosmos.modesDialogViewModel, ) underTest.initialize() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt index e4a988860a6e..ce4a3432a5b4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt @@ -26,13 +26,13 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx +import com.android.systemui.qs.tiles.dialog.InternetDetailsContentManager +import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel import com.android.systemui.qs.tiles.dialog.InternetDialogManager import com.android.systemui.qs.tiles.dialog.WifiStateWorker import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel import com.android.systemui.statusbar.connectivity.AccessPointController -import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.nullable -import com.google.common.truth.Truth import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest import org.junit.Before @@ -40,9 +40,10 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.eq -import org.mockito.Mock -import org.mockito.Mockito.verify +import org.mockito.kotlin.any +import org.mockito.kotlin.mock import org.mockito.kotlin.times +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @SmallTest @@ -54,15 +55,27 @@ class InternetTileUserActionInteractorTest : SysuiTestCase() { private lateinit var underTest: InternetTileUserActionInteractor - @Mock private lateinit var internetDialogManager: InternetDialogManager - @Mock private lateinit var wifiStateWorker: WifiStateWorker - @Mock private lateinit var controller: AccessPointController + private lateinit var internetDialogManager: InternetDialogManager + private lateinit var wifiStateWorker: WifiStateWorker + private lateinit var controller: AccessPointController + private lateinit var internetDetailsViewModelFactory: InternetDetailsViewModel.Factory + private lateinit var internetDetailsContentManagerFactory: InternetDetailsContentManager.Factory + private lateinit var internetDetailsViewModel: InternetDetailsViewModel @Before fun setup() { internetDialogManager = mock<InternetDialogManager>() wifiStateWorker = mock<WifiStateWorker>() controller = mock<AccessPointController>() + internetDetailsViewModelFactory = mock<InternetDetailsViewModel.Factory>() + internetDetailsContentManagerFactory = mock<InternetDetailsContentManager.Factory>() + internetDetailsViewModel = + InternetDetailsViewModel( + onLongClick = {}, + accessPointController = mock<AccessPointController>(), + contentManagerFactory = internetDetailsContentManagerFactory, + ) + whenever(internetDetailsViewModelFactory.create(any())).thenReturn(internetDetailsViewModel) underTest = InternetTileUserActionInteractor( @@ -71,6 +84,7 @@ class InternetTileUserActionInteractorTest : SysuiTestCase() { wifiStateWorker, controller, inputHandler, + internetDetailsViewModelFactory, ) } @@ -102,7 +116,7 @@ class InternetTileUserActionInteractorTest : SysuiTestCase() { underTest.handleInput(QSTileInputTestKtx.longClick(input)) QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput { - Truth.assertThat(it.intent.action).isEqualTo(Settings.ACTION_WIFI_SETTINGS) + assertThat(it.intent.action).isEqualTo(Settings.ACTION_WIFI_SETTINGS) } } @@ -114,7 +128,7 @@ class InternetTileUserActionInteractorTest : SysuiTestCase() { underTest.handleInput(QSTileInputTestKtx.longClick(input)) QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput { - Truth.assertThat(it.intent.action).isEqualTo(Settings.ACTION_WIFI_SETTINGS) + assertThat(it.intent.action).isEqualTo(Settings.ACTION_WIFI_SETTINGS) } } @@ -141,8 +155,7 @@ class InternetTileUserActionInteractorTest : SysuiTestCase() { @Test fun detailsViewModel() = kosmos.testScope.runTest { - assertThat(underTest.detailsViewModel.getTitle()) - .isEqualTo("Internet") + assertThat(underTest.detailsViewModel.getTitle()).isEqualTo("Internet") assertThat(underTest.detailsViewModel.getSubTitle()) .isEqualTo("Tab a network to connect") } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt index 939644594d31..df2dd99c779e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt @@ -21,7 +21,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.compose.animation.scene.Back import com.android.compose.animation.scene.Swipe -import com.android.compose.animation.scene.UserActionResult +import com.android.compose.animation.scene.UserActionResult.HideOverlay +import com.android.compose.animation.scene.UserActionResult.ShowOverlay +import com.android.compose.animation.scene.UserActionResult.ShowOverlay.HideCurrentOverlays import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.EnableSceneContainer @@ -53,7 +55,7 @@ class QuickSettingsShadeOverlayActionsViewModelTest : SysuiTestCase() { val actions by collectLastValue(underTest.actions) underTest.activateIn(this) - assertThat((actions?.get(Swipe.Up) as? UserActionResult.HideOverlay)?.overlay) + assertThat((actions?.get(Swipe.Up) as? HideOverlay)?.overlay) .isEqualTo(Overlays.QuickSettingsShade) assertThat(actions?.get(Swipe.Down)).isNull() } @@ -66,7 +68,7 @@ class QuickSettingsShadeOverlayActionsViewModelTest : SysuiTestCase() { underTest.activateIn(this) assertThat(isEditing).isFalse() - assertThat((actions?.get(Back) as? UserActionResult.HideOverlay)?.overlay) + assertThat((actions?.get(Back) as? HideOverlay)?.overlay) .isEqualTo(Overlays.QuickSettingsShade) } @@ -87,11 +89,11 @@ class QuickSettingsShadeOverlayActionsViewModelTest : SysuiTestCase() { val actions by collectLastValue(underTest.actions) underTest.activateIn(this) - assertThat( - (actions?.get(Swipe.Down(fromSource = SceneContainerEdge.TopLeft)) - as? UserActionResult.ReplaceByOverlay) - ?.overlay - ) - .isEqualTo(Overlays.NotificationsShade) + val action = + (actions?.get(Swipe.Down(fromSource = SceneContainerEdge.TopLeft)) as? ShowOverlay) + assertThat(action?.overlay).isEqualTo(Overlays.NotificationsShade) + val overlaysToHide = action?.hideCurrentOverlays as? HideCurrentOverlays.Some + assertThat(overlaysToHide).isNotNull() + assertThat(overlaysToHide?.overlays).containsExactly(Overlays.QuickSettingsShade) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt index 27e9f07af168..3d5daf6cf9c2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt @@ -318,7 +318,7 @@ class TakeScreenshotExecutorTest : SysuiTestCase() { val displayId = 1 setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = displayId)) val onSaved = { _: Uri? -> } - focusedDisplayRepository.emit(displayId) + focusedDisplayRepository.setDisplayId(displayId) screenshotExecutor.executeScreenshots( createScreenshotRequest( @@ -345,7 +345,7 @@ class TakeScreenshotExecutorTest : SysuiTestCase() { display(TYPE_INTERNAL, id = Display.DEFAULT_DISPLAY), display(TYPE_EXTERNAL, id = 1), ) - focusedDisplayRepository.emit(5) // invalid display + focusedDisplayRepository.setDisplayId(5) // invalid display val onSaved = { _: Uri? -> } screenshotExecutor.executeScreenshots( createScreenshotRequest( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt index ee9cb141a700..555c717e1e65 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt @@ -20,7 +20,7 @@ import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.testing.TestableLooper.RunWithLooper import android.view.Choreographer -import android.view.MotionEvent +import android.view.accessibility.AccessibilityEvent import android.widget.FrameLayout import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -45,7 +45,10 @@ import com.android.systemui.res.R import com.android.systemui.settings.brightness.data.repository.BrightnessMirrorShowingRepository import com.android.systemui.settings.brightness.domain.interactor.BrightnessMirrorShowingInteractor import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler +import com.android.systemui.shade.data.repository.ShadeAnimationRepository +import com.android.systemui.shade.data.repository.ShadeRepositoryImpl import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor +import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractorLegacyImpl import com.android.systemui.statusbar.BlurUtils import com.android.systemui.statusbar.DragDownHelper import com.android.systemui.statusbar.LockscreenShadeTransitionController @@ -185,6 +188,10 @@ class NotificationShadeWindowViewTest : SysuiTestCase() { notificationShadeDepthController, underTest, shadeViewController, + ShadeAnimationInteractorLegacyImpl( + ShadeAnimationRepository(), + ShadeRepositoryImpl(testScope), + ), panelExpansionInteractor, ShadeExpansionStateManager(), notificationStackScrollLayoutController, @@ -259,6 +266,20 @@ class NotificationShadeWindowViewTest : SysuiTestCase() { verify(configurationForwarder).dispatchOnMovedToDisplay(eq(1), eq(config)) } + @Test + @EnableFlags(AConfigFlags.FLAG_SHADE_LAUNCH_ACCESSIBILITY) + fun requestSendAccessibilityEvent_duringLaunchAnimation_blocksFocusEvent() { + underTest.setAnimatingContentLaunch(true) + + assertThat( + underTest.requestSendAccessibilityEvent( + underTest.getChildAt(0), + AccessibilityEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED), + ) + ) + .isFalse() + } + private fun captureInteractionEventHandler() { verify(underTest).setInteractionEventHandler(interactionEventHandlerCaptor.capture()) interactionEventHandler = interactionEventHandlerCaptor.value diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QsBatteryModeControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QsBatteryModeControllerTest.kt index ab5fa8ef43fb..5566c10dc9e9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QsBatteryModeControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QsBatteryModeControllerTest.kt @@ -38,7 +38,7 @@ class QsBatteryModeControllerTest : SysuiTestCase() { private val kosmos = testKosmos() private val insetsProviderStore = kosmos.fakeStatusBarContentInsetsProviderStore - private val insetsProvider = insetsProviderStore.defaultDisplay + private val insetsProvider = insetsProviderStore.forDisplay(context.displayId) @JvmField @Rule val mockitoRule = MockitoJUnit.rule()!! diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt index 007a0fb87953..4f332d4bbed8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt @@ -17,15 +17,21 @@ package com.android.systemui.shade.data.repository import android.provider.Settings.Global.DEVELOPMENT_SHADE_DISPLAY_AWARENESS +import android.view.Display +import android.view.Display.TYPE_EXTERNAL import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues +import com.android.systemui.display.data.repository.display import com.android.systemui.display.data.repository.displayRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.shade.display.AnyExternalShadeDisplayPolicy import com.android.systemui.shade.display.DefaultDisplayShadePolicy +import com.android.systemui.shade.display.FakeShadeDisplayPolicy import com.android.systemui.shade.display.StatusBarTouchShadeDisplayPolicy import com.android.systemui.testKosmos import com.android.systemui.util.settings.fakeGlobalSettings @@ -37,24 +43,28 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class ShadeDisplaysRepositoryTest : SysuiTestCase() { - private val kosmos = testKosmos() + private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val testScope = kosmos.testScope private val globalSettings = kosmos.fakeGlobalSettings private val displayRepository = kosmos.displayRepository private val defaultPolicy = DefaultDisplayShadePolicy() private val policies = kosmos.shadeDisplayPolicies + private val keyguardRepository = kosmos.fakeKeyguardRepository - private val underTest = + private fun createUnderTest(shadeOnDefaultDisplayWhenLocked: Boolean = false) = ShadeDisplaysRepositoryImpl( globalSettings, defaultPolicy, testScope.backgroundScope, policies, + shadeOnDefaultDisplayWhenLocked = shadeOnDefaultDisplayWhenLocked, + keyguardRepository, ) @Test fun policy_changing_propagatedFromTheLatestPolicy() = testScope.runTest { + val underTest = createUnderTest() val displayIds by collectValues(underTest.displayId) assertThat(displayIds).containsExactly(0) @@ -81,30 +91,54 @@ class ShadeDisplaysRepositoryTest : SysuiTestCase() { @Test fun policy_updatesBasedOnSettingValue_defaultDisplay() = testScope.runTest { - val policy by collectLastValue(underTest.policy) - + val underTest = createUnderTest() globalSettings.putString(DEVELOPMENT_SHADE_DISPLAY_AWARENESS, "default_display") - assertThat(policy).isInstanceOf(DefaultDisplayShadePolicy::class.java) + assertThat(underTest.currentPolicy).isInstanceOf(DefaultDisplayShadePolicy::class.java) } @Test fun policy_updatesBasedOnSettingValue_anyExternal() = testScope.runTest { - val policy by collectLastValue(underTest.policy) - + val underTest = createUnderTest() globalSettings.putString(DEVELOPMENT_SHADE_DISPLAY_AWARENESS, "any_external_display") - assertThat(policy).isInstanceOf(AnyExternalShadeDisplayPolicy::class.java) + assertThat(underTest.currentPolicy) + .isInstanceOf(AnyExternalShadeDisplayPolicy::class.java) } @Test fun policy_updatesBasedOnSettingValue_focusBased() = testScope.runTest { - val policy by collectLastValue(underTest.policy) - + val underTest = createUnderTest() globalSettings.putString(DEVELOPMENT_SHADE_DISPLAY_AWARENESS, "status_bar_latest_touch") - assertThat(policy).isInstanceOf(StatusBarTouchShadeDisplayPolicy::class.java) + assertThat(underTest.currentPolicy) + .isInstanceOf(StatusBarTouchShadeDisplayPolicy::class.java) + } + + @Test + fun displayId_afterKeyguardHides_goesBackToPreviousDisplay() = + testScope.runTest { + val underTest = createUnderTest(shadeOnDefaultDisplayWhenLocked = true) + globalSettings.putString( + DEVELOPMENT_SHADE_DISPLAY_AWARENESS, + FakeShadeDisplayPolicy.name, + ) + + val displayId by collectLastValue(underTest.displayId) + + displayRepository.addDisplays(display(id = 2, type = TYPE_EXTERNAL)) + FakeShadeDisplayPolicy.setDisplayId(2) + + assertThat(displayId).isEqualTo(2) + + keyguardRepository.setKeyguardShowing(true) + + assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY) + + keyguardRepository.setKeyguardShowing(false) + + assertThat(displayId).isEqualTo(2) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt index eeb3e6b31c69..fd6bc98b006c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt @@ -25,7 +25,7 @@ import com.android.systemui.display.data.repository.displayRepository import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.shade.ShadePrimaryDisplayCommand -import com.android.systemui.shade.display.ShadeDisplayPolicy +import com.android.systemui.shade.display.FakeShadeDisplayPolicy import com.android.systemui.statusbar.commandline.commandRegistry import com.android.systemui.testKosmos import com.android.systemui.util.settings.fakeGlobalSettings @@ -33,8 +33,6 @@ import com.google.common.truth.StringSubject import com.google.common.truth.Truth.assertThat import java.io.PrintWriter import java.io.StringWriter -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -118,23 +116,12 @@ class ShadePrimaryDisplayCommandTest : SysuiTestCase() { @Test fun policies_setsNewPolicy() = testScope.runTest { - val policy by collectLastValue(shadeDisplaysRepository.policy) - val newPolicy = policies.last().name + val newPolicy = FakeShadeDisplayPolicy.name commandRegistry.onShellCommand(pw, arrayOf("shade_display_override", newPolicy)) - assertThat(policy!!.name).isEqualTo(newPolicy) + assertThat(shadeDisplaysRepository.currentPolicy.name).isEqualTo(newPolicy) } - - private fun makePolicy(policyName: String): ShadeDisplayPolicy { - return object : ShadeDisplayPolicy { - override val name: String - get() = policyName - - override val displayId: StateFlow<Int> - get() = MutableStateFlow(0) - } - } } private fun StringSubject.containsAllIn(strings: List<String>) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/display/FocusShadeDisplayPolicyTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/display/FocusShadeDisplayPolicyTest.kt new file mode 100644 index 000000000000..b4249ef72e62 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/display/FocusShadeDisplayPolicyTest.kt @@ -0,0 +1,58 @@ +/* + * 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.shade.display + +import android.view.Display +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.shade.data.repository.fakeFocusedDisplayRepository +import com.android.systemui.shade.data.repository.focusShadeDisplayPolicy +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import kotlinx.coroutines.test.runTest +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class FocusShadeDisplayPolicyTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + private val testScope = kosmos.testScope + private val focusedDisplayRepository = kosmos.fakeFocusedDisplayRepository + + private val underTest = kosmos.focusShadeDisplayPolicy + + @Test + fun displayId_propagatedFromRepository() = + testScope.runTest { + val displayId by collectLastValue(underTest.displayId) + + assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY) + + focusedDisplayRepository.setDisplayId(2) + + assertThat(displayId).isEqualTo(2) + + focusedDisplayRepository.setDisplayId(3) + + assertThat(displayId).isEqualTo(3) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicyTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicyTest.kt index 20dfd3e11947..e43c46b36a06 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicyTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicyTest.kt @@ -26,12 +26,11 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.display.data.repository.display import com.android.systemui.display.data.repository.displayRepository -import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.shade.data.repository.statusBarTouchShadeDisplayPolicy import com.android.systemui.shade.domain.interactor.notificationElement import com.android.systemui.shade.domain.interactor.qsElement -import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test @@ -45,22 +44,9 @@ import org.mockito.kotlin.mock class StatusBarTouchShadeDisplayPolicyTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val testScope = kosmos.testScope - private val keyguardRepository = kosmos.fakeKeyguardRepository private val displayRepository = kosmos.displayRepository - private fun createUnderTest( - shadeOnDefaultDisplayWhenLocked: Boolean = false - ): StatusBarTouchShadeDisplayPolicy { - return StatusBarTouchShadeDisplayPolicy( - displayRepository, - keyguardRepository, - testScope.backgroundScope, - shadeOnDefaultDisplayWhenLocked = shadeOnDefaultDisplayWhenLocked, - shadeInteractor = { kosmos.shadeInteractor }, - { kosmos.qsElement }, - { kosmos.notificationElement }, - ) - } + private val underTest = kosmos.statusBarTouchShadeDisplayPolicy private fun createMotionEventForDisplay(displayId: Int, xCoordinate: Float = 0f): MotionEvent { return mock<MotionEvent> { @@ -71,15 +57,12 @@ class StatusBarTouchShadeDisplayPolicyTest : SysuiTestCase() { @Test fun displayId_defaultToDefaultDisplay() { - val underTest = createUnderTest() - assertThat(underTest.displayId.value).isEqualTo(Display.DEFAULT_DISPLAY) } @Test fun onStatusBarTouched_called_updatesDisplayId() = testScope.runTest { - val underTest = createUnderTest() val displayId by collectLastValue(underTest.displayId) displayRepository.addDisplays(display(id = 2, type = TYPE_EXTERNAL)) @@ -91,7 +74,6 @@ class StatusBarTouchShadeDisplayPolicyTest : SysuiTestCase() { @Test fun onStatusBarTouched_notExistentDisplay_displayIdNotUpdated() = testScope.runTest { - val underTest = createUnderTest() val displayIds by collectValues(underTest.displayId) assertThat(displayIds).isEqualTo(listOf(Display.DEFAULT_DISPLAY)) @@ -104,7 +86,6 @@ class StatusBarTouchShadeDisplayPolicyTest : SysuiTestCase() { @Test fun onStatusBarTouched_afterDisplayRemoved_goesBackToDefaultDisplay() = testScope.runTest { - val underTest = createUnderTest() val displayId by collectLastValue(underTest.displayId) displayRepository.addDisplays(display(id = 2, type = TYPE_EXTERNAL)) @@ -118,46 +99,8 @@ class StatusBarTouchShadeDisplayPolicyTest : SysuiTestCase() { } @Test - fun onStatusBarTouched_afterKeyguardVisible_goesBackToDefaultDisplay() = - testScope.runTest { - val underTest = createUnderTest(shadeOnDefaultDisplayWhenLocked = true) - val displayId by collectLastValue(underTest.displayId) - - displayRepository.addDisplays(display(id = 2, type = TYPE_EXTERNAL)) - underTest.onStatusBarTouched(createMotionEventForDisplay(2), STATUS_BAR_WIDTH) - - assertThat(displayId).isEqualTo(2) - - keyguardRepository.setKeyguardShowing(true) - - assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY) - } - - @Test - fun onStatusBarTouched_afterKeyguardHides_goesBackToPreviousDisplay() = - testScope.runTest { - val underTest = createUnderTest(shadeOnDefaultDisplayWhenLocked = true) - val displayId by collectLastValue(underTest.displayId) - - displayRepository.addDisplays(display(id = 2, type = TYPE_EXTERNAL)) - underTest.onStatusBarTouched(createMotionEventForDisplay(2), STATUS_BAR_WIDTH) - - assertThat(displayId).isEqualTo(2) - - keyguardRepository.setKeyguardShowing(true) - - assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY) - - keyguardRepository.setKeyguardShowing(false) - - assertThat(displayId).isEqualTo(2) - } - - @Test fun onStatusBarTouched_leftSide_intentSetToNotifications() = testScope.runTest { - val underTest = createUnderTest(shadeOnDefaultDisplayWhenLocked = true) - underTest.onStatusBarTouched( createMotionEventForDisplay(2, STATUS_BAR_WIDTH * 0.1f), STATUS_BAR_WIDTH, @@ -169,8 +112,6 @@ class StatusBarTouchShadeDisplayPolicyTest : SysuiTestCase() { @Test fun onStatusBarTouched_rightSide_intentSetToQs() = testScope.runTest { - val underTest = createUnderTest(shadeOnDefaultDisplayWhenLocked = true) - underTest.onStatusBarTouched( createMotionEventForDisplay(2, STATUS_BAR_WIDTH * 0.95f), STATUS_BAR_WIDTH, @@ -182,8 +123,6 @@ class StatusBarTouchShadeDisplayPolicyTest : SysuiTestCase() { @Test fun onStatusBarTouched_nullAfterConsumed() = testScope.runTest { - val underTest = createUnderTest(shadeOnDefaultDisplayWhenLocked = true) - underTest.onStatusBarTouched( createMotionEventForDisplay(2, STATUS_BAR_WIDTH * 0.1f), STATUS_BAR_WIDTH, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt index a47db2ec728b..668f568d7f46 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt @@ -16,15 +16,11 @@ package com.android.systemui.shade.domain.interactor -import android.platform.test.annotations.DisableFlags -import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope -import com.android.systemui.shade.data.repository.shadeRepository -import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat @@ -48,82 +44,79 @@ class ShadeModeInteractorImplTest : SysuiTestCase() { } @Test - @DisableFlags(DualShade.FLAG_NAME) fun legacyShadeMode_narrowScreen_singleShade() = testScope.runTest { val shadeMode by collectLastValue(underTest.shadeMode) - kosmos.shadeRepository.setShadeLayoutWide(false) + kosmos.enableSingleShade() assertThat(shadeMode).isEqualTo(ShadeMode.Single) } @Test - @DisableFlags(DualShade.FLAG_NAME) fun legacyShadeMode_wideScreen_splitShade() = testScope.runTest { val shadeMode by collectLastValue(underTest.shadeMode) - kosmos.shadeRepository.setShadeLayoutWide(true) + kosmos.enableSplitShade() assertThat(shadeMode).isEqualTo(ShadeMode.Split) } @Test - @EnableFlags(DualShade.FLAG_NAME) fun shadeMode_wideScreen_isDual() = testScope.runTest { val shadeMode by collectLastValue(underTest.shadeMode) - kosmos.shadeRepository.setShadeLayoutWide(true) + kosmos.enableDualShade(wideLayout = true) assertThat(shadeMode).isEqualTo(ShadeMode.Dual) } @Test - @EnableFlags(DualShade.FLAG_NAME) fun shadeMode_narrowScreen_isDual() = testScope.runTest { val shadeMode by collectLastValue(underTest.shadeMode) - kosmos.shadeRepository.setShadeLayoutWide(false) + kosmos.enableDualShade(wideLayout = false) assertThat(shadeMode).isEqualTo(ShadeMode.Dual) } @Test - @EnableFlags(DualShade.FLAG_NAME) - fun isDualShade_flagEnabled_true() = + fun isDualShade_settingEnabled_returnsTrue() = testScope.runTest { - // Initiate collection. + // TODO(b/391578667): Add a test case for user switching once the bug is fixed. val shadeMode by collectLastValue(underTest.shadeMode) + kosmos.enableDualShade() + assertThat(shadeMode).isEqualTo(ShadeMode.Dual) assertThat(underTest.isDualShade).isTrue() } @Test - @DisableFlags(DualShade.FLAG_NAME) - fun isDualShade_flagDisabled_false() = + fun isDualShade_settingDisabled_returnsFalse() = testScope.runTest { - // Initiate collection. val shadeMode by collectLastValue(underTest.shadeMode) + kosmos.disableDualShade() + assertThat(shadeMode).isNotEqualTo(ShadeMode.Dual) assertThat(underTest.isDualShade).isFalse() } @Test fun getTopEdgeSplitFraction_narrowScreen_splitInHalf() = testScope.runTest { - // Ensure isShadeLayoutWide is collected. - val isShadeLayoutWide by collectLastValue(underTest.isShadeLayoutWide) - kosmos.shadeRepository.setShadeLayoutWide(false) + val shadeMode by collectLastValue(underTest.shadeMode) + kosmos.enableDualShade(wideLayout = false) + assertThat(shadeMode).isEqualTo(ShadeMode.Dual) assertThat(underTest.getTopEdgeSplitFraction()).isEqualTo(0.5f) } @Test fun getTopEdgeSplitFraction_wideScreen_splitInHalf() = testScope.runTest { - // Ensure isShadeLayoutWide is collected. - val isShadeLayoutWide by collectLastValue(underTest.isShadeLayoutWide) - kosmos.shadeRepository.setShadeLayoutWide(true) + val shadeMode by collectLastValue(underTest.shadeMode) + kosmos.enableDualShade(wideLayout = true) + assertThat(shadeMode).isEqualTo(ShadeMode.Dual) assertThat(underTest.getTopEdgeSplitFraction()).isEqualTo(0.5f) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java index 0713a247a4a3..baaf6c9a76ae 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java @@ -84,7 +84,7 @@ import com.android.systemui.flags.FakeFeatureFlagsClassic; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.log.LogWtfHandlerRule; import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.recents.LauncherProxyService; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.NotificationLockscreenUserManager.NotificationStateChangedListener; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -156,7 +156,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { @Mock private NotificationClickNotifier mClickNotifier; @Mock - private OverviewProxyService mOverviewProxyService; + private LauncherProxyService mLauncherProxyService; @Mock private KeyguardManager mKeyguardManager; @Mock @@ -1142,7 +1142,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { (() -> mVisibilityProvider), (() -> mNotifCollection), mClickNotifier, - (() -> mOverviewProxyService), + (() -> mLauncherProxyService), NotificationLockscreenUserManagerTest.this.mKeyguardManager, mStatusBarStateController, mMainExecutor, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt index eec23d3ffb1a..942e6554e5d9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt @@ -36,6 +36,7 @@ import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifCh import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.core.StatusBarConnectedDisplays +import com.android.systemui.statusbar.core.StatusBarRootModernization import com.android.systemui.statusbar.notification.data.model.activeNotificationModel import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore import com.android.systemui.statusbar.notification.data.repository.UnconfinedFakeHeadsUpRowRepository @@ -44,6 +45,7 @@ import com.android.systemui.statusbar.notification.headsup.PinnedStatus import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository +import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test @@ -609,7 +611,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { listOf( activeNotificationModel( key = "notif", - statusBarChipIcon = mock<StatusBarIconView>(), + statusBarChipIcon = createStatusBarIconViewOrNull(), promotedContent = promotedContentBuilder.build(), ) ) @@ -629,22 +631,26 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test - @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) - fun chips_clickingChipNotifiesInteractor() = + @DisableFlags( + FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY, + StatusBarRootModernization.FLAG_NAME, + StatusBarChipsModernization.FLAG_NAME, + ) + fun chips_chipsModernizationDisabled_clickingChipNotifiesInteractor() = kosmos.runTest { val latest by collectLastValue(underTest.chips) - val latestChipTap by + val latestChipTapKey by collectLastValue( kosmos.statusBarNotificationChipsInteractor.promotedNotificationChipTapEvent ) + val key = "clickTest" setNotifs( listOf( activeNotificationModel( - key = "clickTest", + key, statusBarChipIcon = createStatusBarIconViewOrNull(), - promotedContent = - PromotedNotificationContentModel.Builder("clickTest").build(), + promotedContent = PromotedNotificationContentModel.Builder(key).build(), ) ) ) @@ -652,7 +658,41 @@ class NotifChipsViewModelTest : SysuiTestCase() { chip.onClickListenerLegacy!!.onClick(mock<View>()) - assertThat(latestChipTap).isEqualTo("clickTest") + assertThat(latestChipTapKey).isEqualTo(key) + } + + @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) + @EnableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) + fun chips_chipsModernizationEnabled_clickingChipNotifiesInteractor() = + kosmos.runTest { + val latest by collectLastValue(underTest.chips) + val latestChipTapKey by + collectLastValue( + kosmos.statusBarNotificationChipsInteractor.promotedNotificationChipTapEvent + ) + val key = "clickTest" + + setNotifs( + listOf( + activeNotificationModel( + key, + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = PromotedNotificationContentModel.Builder(key).build(), + ) + ) + ) + val chip = latest!![0] + + assertThat(chip.clickBehavior) + .isInstanceOf( + OngoingActivityChipModel.ClickBehavior.ShowHeadsUpNotification::class.java + ) + + (chip.clickBehavior as OngoingActivityChipModel.ClickBehavior.ShowHeadsUpNotification) + .onClick() + + assertThat(latestChipTapKey).isEqualTo(key) } private fun setNotifs(notifs: List<ActiveNotificationModel>) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolverTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolverTest.kt index 22906b8724b5..c515d940d2aa 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolverTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolverTest.kt @@ -144,7 +144,8 @@ class TargetSdkResolverTest : SysuiTestCase() { /* rankingAdjustment = */ 0, /* isBubble = */ false, /* proposedImportance = */ 0, - /* sensitiveContent = */ false + /* sensitiveContent = */ false, + /* summarization = */ null ) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java index 7b120947b1d6..2aa1efaa429f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java @@ -107,7 +107,7 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase { @Parameters(name = "{0}") public static List<FlagsParameterization> getParams() { return SceneContainerFlagParameterizationKt - .andSceneContainer(allCombinationsOf(Flags.FLAG_STABILIZE_HEADS_UP_GROUP)); + .andSceneContainer(allCombinationsOf(Flags.FLAG_STABILIZE_HEADS_UP_GROUP_V2)); } private VisualStabilityCoordinator mCoordinator; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt index 3c772fdbe0b2..356eedbc9a45 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt @@ -29,6 +29,7 @@ import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_PUBLIC +import com.android.systemui.statusbar.RankingBuilder import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection @@ -242,4 +243,42 @@ class NotifUiAdjustmentProviderTest : SysuiTestCase() { // Then: need no re-inflation assertFalse(NotifUiAdjustment.needReinflate(oldAdjustment, newAdjustment)) } + + @Test + @EnableFlags(android.app.Flags.FLAG_NM_SUMMARIZATION_UI) + fun changeIsSummarization_needReInflation_newlySummarized() { + // Given: an Entry with no summarization + val oldAdjustment = adjustmentProvider.calculateAdjustment(entry) + assertThat(oldAdjustment.summarization).isNull() + + // When: the Entry now has a summarization + val rb = RankingBuilder(entry.ranking) + rb.setSummarization("summary!") + entry.ranking = rb.build() + val newAdjustment = adjustmentProvider.calculateAdjustment(entry) + assertThat(newAdjustment).isNotEqualTo(oldAdjustment) + + // Then: Need re-inflation + assertTrue(NotifUiAdjustment.needReinflate(oldAdjustment, newAdjustment)) + } + + @Test + @EnableFlags(android.app.Flags.FLAG_NM_SUMMARIZATION_UI) + fun changeIsSummarization_needReInflation_summarizationChanged() { + // Given: an Entry with no summarization + val rb = RankingBuilder(entry.ranking) + rb.setSummarization("summary!") + entry.ranking = rb.build() + val oldAdjustment = adjustmentProvider.calculateAdjustment(entry) + + // When: the Entry now has a new summarization + val rb2 = RankingBuilder(entry.ranking) + rb2.setSummarization("summary new!") + entry.ranking = rb2.build() + val newAdjustment = adjustmentProvider.calculateAdjustment(entry) + assertThat(newAdjustment).isNotEqualTo(oldAdjustment) + + // Then: Need re-inflation + assertTrue(NotifUiAdjustment.needReinflate(oldAdjustment, newAdjustment)) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/BundleNotificationInfoTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/BundleNotificationInfoTest.java index b2962eeb9001..66277e2d13a6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/BundleNotificationInfoTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/BundleNotificationInfoTest.java @@ -198,7 +198,8 @@ public class BundleNotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); // and the feedback button is clicked, final View feedbackButton = mInfo.findViewById(R.id.notification_guts_bundle_feedback); feedbackButton.performClick(); @@ -253,7 +254,8 @@ public class BundleNotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); final View feedbackButton = mInfo.findViewById(R.id.notification_guts_bundle_feedback); feedbackButton.performClick(); @@ -294,7 +296,8 @@ public class BundleNotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); final View feedbackButton = mInfo.findViewById(R.id.notification_guts_bundle_feedback); feedbackButton.performClick(); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.kt index 6a0a5bb3b191..39c42f183481 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.kt @@ -544,6 +544,7 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase( /* wasShownHighPriority = */ eq(true), eq(assistantFeedbackController), eq(metricsLogger), + any<View.OnClickListener>(), ) } @@ -580,6 +581,7 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase( /* wasShownHighPriority = */ eq(false), eq(assistantFeedbackController), eq(metricsLogger), + any<View.OnClickListener>(), ) } @@ -614,6 +616,7 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase( /* wasShownHighPriority = */ eq(false), eq(assistantFeedbackController), eq(metricsLogger), + any<View.OnClickListener>(), ) } @@ -651,6 +654,7 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase( /* wasShownHighPriority = */ eq(false), eq(assistantFeedbackController), eq(metricsLogger), + any<View.OnClickListener>(), ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java index 245a6a0b130c..fdba7ba34855 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java @@ -187,7 +187,7 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, null); final TextView textView = mNotificationInfo.findViewById(R.id.pkg_name); assertTrue(textView.getText().toString().contains("App Name")); assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.header).getVisibility()); @@ -213,7 +213,7 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, null); final ImageView iconView = mNotificationInfo.findViewById(R.id.pkg_icon); assertEquals(iconDrawable, iconView.getDrawable()); } @@ -235,7 +235,7 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, null); final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name); assertEquals(GONE, nameView.getVisibility()); } @@ -266,7 +266,7 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, null); final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name); assertEquals(VISIBLE, nameView.getVisibility()); assertTrue(nameView.getText().toString().contains("Proxied")); @@ -289,7 +289,7 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, null); final TextView groupNameView = mNotificationInfo.findViewById(R.id.group_name); assertEquals(GONE, groupNameView.getVisibility()); } @@ -317,7 +317,7 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, null); final TextView groupNameView = mNotificationInfo.findViewById(R.id.group_name); assertEquals(View.VISIBLE, groupNameView.getVisibility()); assertEquals("Test Group Name", groupNameView.getText()); @@ -340,7 +340,7 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, null); final TextView textView = mNotificationInfo.findViewById(R.id.channel_name); assertEquals(TEST_CHANNEL_NAME, textView.getText()); } @@ -362,7 +362,7 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, null); final TextView textView = mNotificationInfo.findViewById(R.id.channel_name); assertEquals(GONE, textView.getVisibility()); } @@ -388,7 +388,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); final TextView textView = mNotificationInfo.findViewById(R.id.channel_name); assertEquals(VISIBLE, textView.getVisibility()); } @@ -410,7 +411,8 @@ public class NotificationInfoTest extends SysuiTestCase { true, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); final TextView textView = mNotificationInfo.findViewById(R.id.channel_name); assertEquals(VISIBLE, textView.getVisibility()); } @@ -436,7 +438,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); final View settingsButton = mNotificationInfo.findViewById(R.id.info); settingsButton.performClick(); @@ -461,7 +464,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); final View settingsButton = mNotificationInfo.findViewById(R.id.info); assertTrue(settingsButton.getVisibility() != View.VISIBLE); } @@ -486,7 +490,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); final View settingsButton = mNotificationInfo.findViewById(R.id.info); assertTrue(settingsButton.getVisibility() != View.VISIBLE); } @@ -508,7 +513,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); mNotificationInfo.bindNotification( mMockPackageManager, mMockINotificationManager, @@ -524,7 +530,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); final View settingsButton = mNotificationInfo.findViewById(R.id.info); assertEquals(View.VISIBLE, settingsButton.getVisibility()); } @@ -546,7 +553,8 @@ public class NotificationInfoTest extends SysuiTestCase { true, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); final TextView view = mNotificationInfo.findViewById(R.id.non_configurable_text); assertEquals(View.VISIBLE, view.getVisibility()); assertEquals(mContext.getString(R.string.notification_unblockable_desc), @@ -589,7 +597,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); final TextView view = mNotificationInfo.findViewById(R.id.non_configurable_call_text); assertEquals(View.VISIBLE, view.getVisibility()); assertEquals(mContext.getString(R.string.notification_unblockable_call_desc), @@ -632,7 +641,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); assertEquals(GONE, mNotificationInfo.findViewById(R.id.non_configurable_call_text).getVisibility()); assertEquals(VISIBLE, @@ -659,7 +669,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.automatic).getVisibility()); assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.automatic_summary).getVisibility()); } @@ -681,7 +692,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); assertEquals(GONE, mNotificationInfo.findViewById(R.id.automatic).getVisibility()); assertEquals(GONE, mNotificationInfo.findViewById(R.id.automatic_summary).getVisibility()); } @@ -705,7 +717,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); assertTrue(mNotificationInfo.findViewById(R.id.automatic).isSelected()); } @@ -726,7 +739,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); assertTrue(mNotificationInfo.findViewById(R.id.alert).isSelected()); } @@ -747,7 +761,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, false, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); assertTrue(mNotificationInfo.findViewById(R.id.silence).isSelected()); } @@ -768,7 +783,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); mTestableLooper.processAllMessages(); verify(mMockINotificationManager, never()).updateNotificationChannelForPackage( anyString(), eq(TEST_UID), any()); @@ -791,7 +807,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); assertEquals(1, mUiEventLogger.numLogs()); assertEquals(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.getId(), mUiEventLogger.eventId(0)); @@ -815,7 +832,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, false, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); mNotificationInfo.findViewById(R.id.alert).performClick(); mTestableLooper.processAllMessages(); @@ -842,7 +860,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); mNotificationInfo.findViewById(R.id.silence).performClick(); mTestableLooper.processAllMessages(); @@ -869,7 +888,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); mNotificationInfo.findViewById(R.id.automatic).performClick(); mTestableLooper.processAllMessages(); @@ -897,7 +917,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); mNotificationInfo.handleCloseControls(true, false); mTestableLooper.processAllMessages(); @@ -924,7 +945,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); mNotificationInfo.handleCloseControls(true, false); mTestableLooper.processAllMessages(); @@ -959,7 +981,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); mNotificationInfo.handleCloseControls(true, false); @@ -987,7 +1010,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); mNotificationInfo.findViewById(R.id.silence).performClick(); mNotificationInfo.findViewById(R.id.done).performClick(); @@ -1028,7 +1052,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, false, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); mNotificationInfo.findViewById(R.id.alert).performClick(); mNotificationInfo.findViewById(R.id.done).performClick(); @@ -1065,7 +1090,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); mNotificationInfo.findViewById(R.id.automatic).performClick(); mNotificationInfo.findViewById(R.id.done).performClick(); @@ -1097,7 +1123,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); mNotificationInfo.findViewById(R.id.silence).performClick(); mNotificationInfo.findViewById(R.id.done).performClick(); @@ -1133,7 +1160,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, false, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); assertEquals(mContext.getString(R.string.inline_done_button), ((TextView) mNotificationInfo.findViewById(R.id.done)).getText()); @@ -1171,7 +1199,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, false, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); mNotificationInfo.findViewById(R.id.silence).performClick(); mNotificationInfo.handleCloseControls(false, false); @@ -1202,7 +1231,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, false, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); assertEquals(mContext.getString(R.string.inline_done_button), ((TextView) mNotificationInfo.findViewById(R.id.done)).getText()); @@ -1240,7 +1270,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, true, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); mNotificationInfo.findViewById(R.id.silence).performClick(); mNotificationInfo.findViewById(R.id.done).performClick(); @@ -1269,7 +1300,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, false, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); assertEquals(mContext.getString(R.string.inline_done_button), ((TextView) mNotificationInfo.findViewById(R.id.done)).getText()); @@ -1300,7 +1332,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, false, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); mNotificationInfo.findViewById(R.id.alert).performClick(); mNotificationInfo.findViewById(R.id.done).performClick(); @@ -1335,7 +1368,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, false, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); mNotificationInfo.findViewById(R.id.alert).performClick(); mNotificationInfo.findViewById(R.id.done).performClick(); @@ -1368,7 +1402,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, false, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); mNotificationInfo.findViewById(R.id.alert).performClick(); mNotificationInfo.findViewById(R.id.done).performClick(); @@ -1401,7 +1436,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, false, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); mNotificationInfo.findViewById(R.id.alert).performClick(); @@ -1427,7 +1463,8 @@ public class NotificationInfoTest extends SysuiTestCase { false, false, mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + null); assertFalse(mNotificationInfo.willBeRemoved()); } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfoTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfoTest.java new file mode 100644 index 000000000000..b33f93d5b523 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfoTest.java @@ -0,0 +1,156 @@ +/* + * 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 static android.app.Notification.EXTRA_BUILDER_APPLICATION_INFO; +import static android.app.NotificationManager.IMPORTANCE_LOW; + +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.INotificationManager; +import android.app.Notification; +import android.app.NotificationChannel; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.UserHandle; +import android.platform.test.annotations.EnableFlags; +import android.service.notification.StatusBarNotification; +import android.telecom.TelecomManager; +import android.testing.TestableLooper; +import android.view.LayoutInflater; +import android.view.View; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.testing.UiEventLoggerFake; +import com.android.systemui.Dependency; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.res.R; +import com.android.systemui.statusbar.notification.AssistantFeedbackController; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.concurrent.CountDownLatch; + +@SmallTest +@RunWith(AndroidJUnit4.class) +@TestableLooper.RunWithLooper +public class PromotedNotificationInfoTest extends SysuiTestCase { + private static final String TEST_PACKAGE_NAME = "test_package"; + private static final int TEST_UID = 1; + private static final String TEST_CHANNEL = "test_channel"; + private static final String TEST_CHANNEL_NAME = "TEST CHANNEL NAME"; + + private TestableLooper mTestableLooper; + private PromotedNotificationInfo mInfo; + private NotificationChannel mNotificationChannel; + private StatusBarNotification mSbn; + private NotificationEntry mEntry; + private UiEventLoggerFake mUiEventLogger = new UiEventLoggerFake(); + + @Rule + public MockitoRule mockito = MockitoJUnit.rule(); + @Mock + private MetricsLogger mMetricsLogger; + @Mock + private INotificationManager mMockINotificationManager; + @Mock + private PackageManager mMockPackageManager; + @Mock + private OnUserInteractionCallback mOnUserInteractionCallback; + @Mock + private ChannelEditorDialogController mChannelEditorDialogController; + @Mock + private AssistantFeedbackController mAssistantFeedbackController; + @Mock + private TelecomManager mTelecomManager; + + @Before + public void setUp() throws Exception { + final ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.uid = TEST_UID; // non-zero + + mNotificationChannel = new NotificationChannel( + TEST_CHANNEL, TEST_CHANNEL_NAME, IMPORTANCE_LOW); + Notification notification = new Notification(); + notification.extras.putParcelable(EXTRA_BUILDER_APPLICATION_INFO, applicationInfo); + mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID, 0, + notification, UserHandle.getUserHandleForUid(TEST_UID), null, 0); + mEntry = new NotificationEntryBuilder().setSbn(mSbn).build(); + when(mAssistantFeedbackController.isFeedbackEnabled()).thenReturn(false); + + mTestableLooper = TestableLooper.get(this); + + mContext.addMockSystemService(TelecomManager.class, mTelecomManager); + + mDependency.injectTestDependency(Dependency.BG_LOOPER, mTestableLooper.getLooper()); + // Inflate the layout + final LayoutInflater layoutInflater = LayoutInflater.from(mContext); + mInfo = (PromotedNotificationInfo) layoutInflater.inflate( + R.layout.promoted_notification_info, null); + mInfo.setGutsParent(mock(NotificationGuts.class)); + // Our view is never attached to a window so the View#post methods in + // BundleNotificationInfo never get called. Setting this will skip the post and do the + // action immediately. + mInfo.mSkipPost = true; + } + + @Test + @EnableFlags(android.app.Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI) + public void testBindNotification_setsOnClickListenerForFeedback() throws Exception { + + // Bind the notification to the Info object + final CountDownLatch latch = new CountDownLatch(1); + mInfo.bindNotification( + mMockPackageManager, + mMockINotificationManager, + mOnUserInteractionCallback, + mChannelEditorDialogController, + TEST_PACKAGE_NAME, + mNotificationChannel, + mEntry, + null, + null, + mUiEventLogger, + true, + false, + true, + mAssistantFeedbackController, + mMetricsLogger, + null); + // Click demote button + final View demoteButton = mInfo.findViewById(R.id.promoted_demote); + demoteButton.performClick(); + // verify that notiManager tried to demote + verify(mMockINotificationManager, atLeastOnce()).setCanBePromoted(TEST_PACKAGE_NAME, + mSbn.getUid(), false, true); + + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt index 50db9f7268e4..4b8a0c21f03d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.stack import android.annotation.DimenRes +import android.platform.test.annotations.EnableFlags import android.service.notification.StatusBarNotification import android.view.View.VISIBLE import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -30,6 +31,7 @@ import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController @@ -152,6 +154,29 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() { } @Test + @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME) + fun maxKeyguardNotificationsForPromotedOngoing_onLockscreenSpaceForMinHeightButNotIntrinsicHeight_returnsOne() { + setGapHeight(0f) + // No divider height since we're testing one element where index = 0 + + whenever(sysuiStatusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) + whenever(lockscreenShadeTransitionController.fractionToShade).thenReturn(0f) + + val row = createMockRow(10f, isPromotedOngoing = true) + whenever(row.getMinHeight(any())).thenReturn(5) + + val maxNotifications = + computeMaxKeyguardNotifications( + listOf(row), + /* spaceForNotifications= */ 5f, + /* spaceForShelf= */ 0f, + /* shelfHeight= */ 0f, + ) + + assertThat(maxNotifications).isEqualTo(1) + } + + @Test fun computeMaxKeyguardNotifications_spaceForTwo_returnsTwo() { setGapHeight(gapHeight) val shelfHeight = shelfHeight + dividerHeight @@ -257,6 +282,26 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() { } @Test + @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME) + fun getSpaceNeeded_onLockscreenEnoughSpacePromotedOngoing_intrinsicHeight() { + setGapHeight(0f) + // No divider height since we're testing one element where index = 0 + + val row = createMockRow(10f, isPromotedOngoing = true) + whenever(row.getMinHeight(any())).thenReturn(5) + + val space = + sizeCalculator.getSpaceNeeded( + row, + visibleIndex = 0, + previousView = null, + stack = stackLayout, + onLockscreen = true, + ) + assertThat(space.whenEnoughSpace).isEqualTo(10f) + } + + @Test fun getSpaceNeeded_onLockscreenEnoughSpaceNotStickyHun_minHeight() { setGapHeight(0f) // No divider height since we're testing one element where index = 0 @@ -296,6 +341,26 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() { } @Test + @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME) + fun getSpaceNeeded_onLockscreenSavingSpacePromotedOngoing_minHeight() { + setGapHeight(0f) + // No divider height since we're testing one element where index = 0 + + val expandableView = createMockRow(10f, isPromotedOngoing = true) + whenever(expandableView.getMinHeight(any())).thenReturn(5) + + val space = + sizeCalculator.getSpaceNeeded( + expandableView, + visibleIndex = 0, + previousView = null, + stack = stackLayout, + onLockscreen = true, + ) + assertThat(space.whenSavingSpace).isEqualTo(5) + } + + @Test fun getSpaceNeeded_onLockscreenSavingSpaceNotStickyHun_minHeight() { setGapHeight(0f) // No divider height since we're testing one element where index = 0 @@ -366,6 +431,7 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() { isSticky: Boolean = false, isRemoved: Boolean = false, visibility: Int = VISIBLE, + isPromotedOngoing: Boolean = false, ): ExpandableNotificationRow { val row = mock(ExpandableNotificationRow::class.java) val entry = mock(NotificationEntry::class.java) @@ -378,6 +444,7 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() { whenever(row.getMinHeight(any())).thenReturn(height.toInt()) whenever(row.intrinsicHeight).thenReturn(height.toInt()) whenever(row.heightWithoutLockscreenConstraints).thenReturn(height.toInt()) + whenever(row.isPromotedOngoing).thenReturn(isPromotedOngoing) return row } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt index 43ad042ecf78..57b7df7a8d31 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt @@ -162,7 +162,7 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { Mockito.`when`(iconManagerFactory.create(ArgumentMatchers.any(), ArgumentMatchers.any())) .thenReturn(iconManager) - Mockito.`when`(statusBarContentInsetsProviderStore.defaultDisplay) + Mockito.`when`(statusBarContentInsetsProviderStore.forDisplay(context.displayId)) .thenReturn(kosmos.mockStatusBarContentInsetsProvider) allowTestableLooperAsMainThread() looper.runWithLooper { @@ -180,6 +180,7 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { private fun createController(): KeyguardStatusBarViewController { return KeyguardStatusBarViewController( kosmos.testDispatcher, + context, keyguardStatusBarView, carrierTextController, configurationController, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorTest.kt index fec186e862be..b837253f44a4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorTest.kt @@ -16,12 +16,16 @@ package com.android.systemui.volume.dialog.domain.interactor +import android.media.AudioManager.RINGER_MODE_NORMAL +import android.media.AudioManager.RINGER_MODE_SILENT +import android.media.AudioManager.RINGER_MODE_VIBRATE import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.plugins.fakeVolumeDialogController import com.android.systemui.testKosmos import com.android.systemui.volume.dialog.domain.model.VolumeDialogEventModel import com.google.common.truth.Truth.assertThat @@ -44,4 +48,30 @@ class VolumeDialogCallbacksInteractorTest : SysuiTestCase() { val event by collectLastValue(underTest.event) assertThat(event).isInstanceOf(VolumeDialogEventModel.SubscribedToEvents::class.java) } + + @Test + fun showSilentHint_setsRingerModeToNormal() = + kosmos.runTest { + fakeVolumeDialogController.setRingerMode(RINGER_MODE_VIBRATE, false) + + underTest // It should eagerly collect the values and update the controller + fakeVolumeDialogController.onShowSilentHint() + fakeVolumeDialogController.getState() + + assertThat(fakeVolumeDialogController.state.ringerModeInternal) + .isEqualTo(RINGER_MODE_NORMAL) + } + + @Test + fun showVibrateHint_setsRingerModeToSilent() = + kosmos.runTest { + fakeVolumeDialogController.setRingerMode(RINGER_MODE_VIBRATE, false) + + underTest // It should eagerly collect the values and update the controller + fakeVolumeDialogController.onShowVibrateHint() + fakeVolumeDialogController.getState() + + assertThat(fakeVolumeDialogController.state.ringerModeInternal) + .isEqualTo(RINGER_MODE_SILENT) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorTest.kt index 7d5559933cd8..12885a83a70b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorTest.kt @@ -25,14 +25,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.volume.shared.model.RingerMode import com.android.systemui.SysuiTestCase -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.plugins.fakeVolumeDialogController import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -43,8 +42,7 @@ import org.junit.runner.RunWith @TestableLooper.RunWithLooper class VolumeDialogRingerInteractorTest : SysuiTestCase() { - private val kosmos = testKosmos() - private val testScope = kosmos.testScope + private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val controller = kosmos.fakeVolumeDialogController private lateinit var underTest: VolumeDialogRingerInteractor @@ -57,13 +55,11 @@ class VolumeDialogRingerInteractorTest : SysuiTestCase() { @Test fun setRingerMode_normal() = - testScope.runTest { - runCurrent() + kosmos.runTest { val ringerModel by collectLastValue(underTest.ringerModel) underTest.setRingerMode(RingerMode(RINGER_MODE_NORMAL)) controller.getState() - runCurrent() assertThat(ringerModel).isNotNull() assertThat(ringerModel?.currentRingerMode).isEqualTo(RingerMode(RINGER_MODE_NORMAL)) @@ -71,13 +67,11 @@ class VolumeDialogRingerInteractorTest : SysuiTestCase() { @Test fun setRingerMode_silent() = - testScope.runTest { - runCurrent() + kosmos.runTest { val ringerModel by collectLastValue(underTest.ringerModel) underTest.setRingerMode(RingerMode(RINGER_MODE_SILENT)) controller.getState() - runCurrent() assertThat(ringerModel).isNotNull() assertThat(ringerModel?.currentRingerMode).isEqualTo(RingerMode(RINGER_MODE_SILENT)) @@ -85,13 +79,11 @@ class VolumeDialogRingerInteractorTest : SysuiTestCase() { @Test fun setRingerMode_vibrate() = - testScope.runTest { - runCurrent() + kosmos.runTest { val ringerModel by collectLastValue(underTest.ringerModel) underTest.setRingerMode(RingerMode(RINGER_MODE_VIBRATE)) controller.getState() - runCurrent() assertThat(ringerModel).isNotNull() assertThat(ringerModel?.currentRingerMode).isEqualTo(RingerMode(RINGER_MODE_VIBRATE)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInputEventsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInputEventsInteractorTest.kt index 799ca4a49038..0a50722d8fed 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInputEventsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInputEventsInteractorTest.kt @@ -18,7 +18,6 @@ package com.android.systemui.volume.dialog.sliders.domain.interactor import android.app.ActivityManager import android.testing.TestableLooper -import android.view.MotionEvent import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -29,6 +28,7 @@ import com.android.systemui.testKosmos import com.android.systemui.volume.Events import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel +import com.android.systemui.volume.dialog.sliders.shared.model.SliderInputEvent import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlin.time.Duration.Companion.seconds @@ -73,16 +73,7 @@ class VolumeDialogSliderInputEventsInteractorTest : SysuiTestCase() { assertThat(dialogVisibility) .isInstanceOf(VolumeDialogVisibilityModel.Visible::class.java) - underTest.onTouchEvent( - MotionEvent.obtain( - /* downTime = */ 0, - /* eventTime = */ 0, - /* action = */ 0, - /* x = */ 0f, - /* y = */ 0f, - /* metaState = */ 0, - ) - ) + underTest.onTouchEvent(SliderInputEvent.Touch.Start(0f, 0f)) advanceTimeBy(volumeDialogTimeout / 2) assertThat(dialogVisibility) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt index ba6ea9f5e8bb..89410593fe62 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt @@ -16,11 +16,14 @@ package com.android.systemui.wallpapers +import android.app.Flags import android.content.Context import android.graphics.Canvas import android.graphics.Paint import android.graphics.Rect import android.graphics.RectF +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.service.wallpaper.WallpaperService.Engine import android.testing.TestableLooper.RunWithLooper import android.view.Surface @@ -37,6 +40,7 @@ import org.mockito.Mockito.spy import org.mockito.MockitoAnnotations import org.mockito.kotlin.any import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyZeroInteractions import org.mockito.kotlin.whenever @SmallTest @@ -70,6 +74,18 @@ class GradientColorWallpaperTest : SysuiTestCase() { } @Test + @DisableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER) + fun onSurfaceRedrawNeeded_flagDisabled_shouldNotDrawInCanvas() { + val engine = createGradientColorWallpaperEngine() + engine.onCreate(surfaceHolder) + + engine.onSurfaceRedrawNeeded(surfaceHolder) + + verifyZeroInteractions(canvas) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER) fun onSurfaceRedrawNeeded_shouldDrawInCanvas() { val engine = createGradientColorWallpaperEngine() engine.onCreate(surfaceHolder) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt index 2985053f56d5..d6343c840d9b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt @@ -26,4 +26,5 @@ class FakeWallpaperRepository : WallpaperRepository { override val wallpaperInfo = MutableStateFlow<WallpaperInfo?>(null) override val wallpaperSupportsAmbientMode = flowOf(false) override var rootView: View? = null + override val shouldSendFocalArea = MutableStateFlow(false) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt index 03753d9aa884..115edd0d3bb0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt @@ -67,7 +67,6 @@ class WallpaperRepositoryImplTest : SysuiTestCase() { fakeBroadcastDispatcher, userRepository, keyguardRepository, - keyguardClockRepository, wallpaperManager, context, ) @@ -252,7 +251,7 @@ class WallpaperRepositoryImplTest : SysuiTestCase() { @EnableFlags(Flags.FLAG_MAGIC_PORTRAIT_WALLPAPERS) fun shouldSendNotificationLayout_setMagicPortraitWallpaper_launchSendLayoutJob() = testScope.runTest { - val latest by collectLastValue(underTest.shouldSendNotificationLayout) + val latest by collectLastValue(underTest.shouldSendFocalArea) val magicPortraitWallpaper = mock<WallpaperInfo>().apply { whenever(this.component) @@ -273,7 +272,7 @@ class WallpaperRepositoryImplTest : SysuiTestCase() { @EnableFlags(Flags.FLAG_MAGIC_PORTRAIT_WALLPAPERS) fun shouldSendNotificationLayout_setNotMagicPortraitWallpaper_cancelSendLayoutJob() = testScope.runTest { - val latest by collectLastValue(underTest.shouldSendNotificationLayout) + val latest by collectLastValue(underTest.shouldSendFocalArea) val magicPortraitWallpaper = MAGIC_PORTRAIT_WP whenever(wallpaperManager.getWallpaperInfoForUser(any())) .thenReturn(magicPortraitWallpaper) diff --git a/packages/SystemUI/res-keyguard/layout/bouncer_message_view.xml b/packages/SystemUI/res-keyguard/layout/bouncer_message_view.xml index f7dac13888c0..ea494b4642d5 100644 --- a/packages/SystemUI/res-keyguard/layout/bouncer_message_view.xml +++ b/packages/SystemUI/res-keyguard/layout/bouncer_message_view.xml @@ -21,7 +21,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/keyguard_lock_padding" - android:focusable="true" + android:focusable="false" /> <com.android.keyguard.BouncerKeyguardMessageArea @@ -30,6 +30,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/secondary_message_padding" - android:focusable="true" /> + android:focusable="false" /> </merge> diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml b/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml index 2a8f1b596711..f231df2f1a10 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml @@ -66,7 +66,7 @@ <com.android.systemui.bouncer.ui.BouncerMessageView android:id="@+id/bouncer_message_view" - android:importantForAccessibility="noHideDescendants" + android:screenReaderFocusable="true" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" /> diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml index 76f6f599c54c..04457229d573 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml @@ -31,7 +31,7 @@ <com.android.systemui.bouncer.ui.BouncerMessageView android:id="@+id/bouncer_message_view" - android:importantForAccessibility="noHideDescendants" + android:screenReaderFocusable="true" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml index 5879c110d8a1..b184344f2f24 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml @@ -67,7 +67,7 @@ <com.android.systemui.bouncer.ui.BouncerMessageView android:id="@+id/bouncer_message_view" - android:importantForAccessibility="noHideDescendants" + android:screenReaderFocusable="true" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" /> diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml index 3f7b02835357..0e15ff66f3ee 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml @@ -35,7 +35,7 @@ <com.android.systemui.bouncer.ui.BouncerMessageView android:id="@+id/bouncer_message_view" - android:importantForAccessibility="noHideDescendants" + android:screenReaderFocusable="true" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" /> diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml index b464fb3bafed..f6ac02aee657 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml @@ -74,7 +74,7 @@ <com.android.systemui.bouncer.ui.BouncerMessageView android:id="@+id/bouncer_message_view" - android:importantForAccessibility="noHideDescendants" + android:screenReaderFocusable="true" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" /> diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml index 21580731aed2..ba4da794d777 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml @@ -32,7 +32,7 @@ <com.android.systemui.bouncer.ui.BouncerMessageView android:id="@+id/bouncer_message_view" - android:importantForAccessibility="noHideDescendants" + android:screenReaderFocusable="true" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" /> diff --git a/packages/SystemUI/res-keyguard/layout/shade_carrier_new.xml b/packages/SystemUI/res-keyguard/layout/shade_carrier_new.xml index cc99f5e125f3..dd5f7e4e2ed4 100644 --- a/packages/SystemUI/res-keyguard/layout/shade_carrier_new.xml +++ b/packages/SystemUI/res-keyguard/layout/shade_carrier_new.xml @@ -30,7 +30,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" - android:textAppearance="@style/TextAppearance.QS.Status.Carriers" + android:textAppearance="@style/TextAppearance.QS.Status" android:layout_marginEnd="@dimen/qs_carrier_margin_width" android:visibility="gone" android:textDirection="locale" diff --git a/packages/SystemUI/res-keyguard/values/styles.xml b/packages/SystemUI/res-keyguard/values/styles.xml index 433c0a71008d..e7d6b2fe08f4 100644 --- a/packages/SystemUI/res-keyguard/values/styles.xml +++ b/packages/SystemUI/res-keyguard/values/styles.xml @@ -159,6 +159,17 @@ <item name="android:shadowRadius">?attr/shadowRadius</item> </style> + <style name="TextAppearance.Keyguard.BottomArea.DoubleShadow"> + <item name="keyShadowBlur">0.5dp</item> + <item name="keyShadowOffsetX">0.5dp</item> + <item name="keyShadowOffsetY">0.5dp</item> + <item name="keyShadowAlpha">0.8</item> + <item name="ambientShadowBlur">0.5dp</item> + <item name="ambientShadowOffsetX">0.5dp</item> + <item name="ambientShadowOffsetY">0.5dp</item> + <item name="ambientShadowAlpha">0.6</item> + </style> + <style name="TextAppearance.Keyguard.BottomArea.Button"> <item name="android:shadowRadius">0</item> </style> diff --git a/packages/SystemUI/res/drawable/notif_footer_btn_background.xml b/packages/SystemUI/res/drawable/notif_footer_btn_background.xml index 1d5e09d9b260..e1e60920ab01 100644 --- a/packages/SystemUI/res/drawable/notif_footer_btn_background.xml +++ b/packages/SystemUI/res/drawable/notif_footer_btn_background.xml @@ -26,6 +26,9 @@ <padding android:left="20dp" android:right="20dp" /> + <!-- TODO(b/294830092): Update to the blur surface effect color token when + the resource workaround is resolved and + notification_shade_blur is enabled. --> <solid android:color="@androidprv:color/materialColorSurfaceContainerHigh" /> </shape> </inset> diff --git a/packages/SystemUI/res/layout/low_light_clock_dream.xml b/packages/SystemUI/res/layout/low_light_clock_dream.xml new file mode 100644 index 000000000000..3d74a9fd8ae3 --- /dev/null +++ b/packages/SystemUI/res/layout/low_light_clock_dream.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/low_light_clock_dream" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/low_light_clock_background_color"> + + <TextClock + android:id="@+id/low_light_text_clock" + android:layout_width="match_parent" + android:layout_height="@dimen/low_light_clock_text_size" + android:layout_gravity="center" + android:fontFamily="google-sans-clock" + android:gravity="center_horizontal" + android:textColor="@color/low_light_clock_text_color" + android:autoSizeTextType="uniform" + android:autoSizeMaxTextSize="@dimen/low_light_clock_text_size" + android:format12Hour="h:mm" + android:format24Hour="H:mm"/> + + <TextView + android:id="@+id/charging_status_text_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="@dimen/keyguard_indication_margin_bottom" + android:gravity="center" + android:minHeight="@dimen/low_light_clock_charging_text_min_height" + android:layout_gravity="center_horizontal|bottom" + android:paddingStart="@dimen/keyguard_indication_text_padding" + android:paddingEnd="@dimen/keyguard_indication_text_padding" + android:textAppearance="@style/TextAppearance.Keyguard.BottomArea" + android:textSize="@dimen/low_light_clock_charging_text_size" + android:textFontWeight="@integer/low_light_clock_charging_text_font_weight" + android:maxLines="2" + android:ellipsize="end" + android:accessibilityLiveRegion="polite" /> + </FrameLayout> + diff --git a/packages/SystemUI/res/layout/promoted_notification_info.xml b/packages/SystemUI/res/layout/promoted_notification_info.xml new file mode 100644 index 000000000000..5d170a98a806 --- /dev/null +++ b/packages/SystemUI/res/layout/promoted_notification_info.xml @@ -0,0 +1,387 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2024, The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<com.android.systemui.statusbar.notification.row.PromotedNotificationInfo + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:id="@+id/notification_guts" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:focusable="true" + android:clipChildren="false" + android:clipToPadding="true" + android:orientation="vertical" + android:paddingStart="@dimen/notification_shade_content_margin_horizontal"> + + <!-- Package Info --> + <LinearLayout + android:id="@+id/header" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center_vertical" + android:clipChildren="false" + android:paddingTop="@dimen/notification_guts_header_top_padding" + android:clipToPadding="true"> + <ImageView + android:id="@+id/pkg_icon" + android:layout_width="@dimen/notification_guts_conversation_icon_size" + android:layout_height="@dimen/notification_guts_conversation_icon_size" + android:layout_centerVertical="true" + android:layout_alignParentStart="true" + android:layout_marginEnd="15dp" /> + <LinearLayout + android:id="@+id/names" + android:layout_weight="1" + android:layout_width="0dp" + android:orientation="vertical" + android:layout_height="wrap_content" + android:minHeight="@dimen/notification_guts_conversation_icon_size" + android:layout_centerVertical="true" + android:gravity="center_vertical" + android:layout_alignEnd="@id/pkg_icon" + android:layout_toEndOf="@id/pkg_icon"> + <TextView + android:id="@+id/channel_name" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textDirection="locale" + style="@style/TextAppearance.NotificationImportanceChannel"/> + <TextView + android:id="@+id/group_name" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textDirection="locale" + android:ellipsize="end" + style="@style/TextAppearance.NotificationImportanceChannelGroup"/> + <TextView + android:id="@+id/pkg_name" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="@style/TextAppearance.NotificationImportanceApp" + android:ellipsize="end" + android:textDirection="locale" + android:maxLines="1"/> + <TextView + android:id="@+id/delegate_name" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_centerVertical="true" + style="@style/TextAppearance.NotificationImportanceHeader" + android:layout_marginStart="2dp" + android:layout_marginEnd="2dp" + android:ellipsize="end" + android:textDirection="locale" + android:text="@string/notification_delegate_header" + android:maxLines="1" /> + + </LinearLayout> + + <!-- end aligned fields --> + <!-- Optional link to app. Only appears if the channel is not disabled and the app +asked for it --> + <ImageButton + android:id="@+id/app_settings" + android:layout_width="@dimen/notification_importance_toggle_size" + android:layout_height="@dimen/notification_importance_toggle_size" + android:layout_centerVertical="true" + android:visibility="gone" + android:background="@drawable/ripple_drawable" + android:contentDescription="@string/notification_app_settings" + android:src="@drawable/ic_info" + android:layout_toStartOf="@id/info" + android:tint="@androidprv:color/materialColorPrimary"/> + <ImageButton + android:id="@+id/info" + android:layout_width="@dimen/notification_importance_toggle_size" + android:layout_height="@dimen/notification_importance_toggle_size" + android:layout_centerVertical="true" + android:contentDescription="@string/notification_more_settings" + android:background="@drawable/ripple_drawable_20dp" + android:src="@drawable/ic_settings" + android:tint="@androidprv:color/materialColorPrimary" + android:layout_alignParentEnd="true" /> + + </LinearLayout> + + <LinearLayout + android:id="@+id/inline_controls" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingEnd="@dimen/notification_shade_content_margin_horizontal" + android:layout_marginTop="@dimen/notification_guts_option_vertical_padding" + android:clipChildren="false" + android:clipToPadding="false" + android:orientation="vertical"> + + <!-- Non configurable app/channel text. appears instead of @+id/interruptiveness_settings--> + <TextView + android:id="@+id/non_configurable_text" + android:text="@string/notification_unblockable_desc" + android:visibility="gone" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="@*android:style/TextAppearance.DeviceDefault.Notification" /> + + <!-- Non configurable app/channel text. appears instead of @+id/interruptiveness_settings--> + <TextView + android:id="@+id/non_configurable_call_text" + android:text="@string/notification_unblockable_call_desc" + android:visibility="gone" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="@*android:style/TextAppearance.DeviceDefault.Notification" /> + + <!-- Non configurable multichannel text. appears instead of @+id/interruptiveness_settings--> + <TextView + android:id="@+id/non_configurable_multichannel_text" + android:text="@string/notification_multichannel_desc" + android:visibility="gone" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="@*android:style/TextAppearance.DeviceDefault.Notification" /> + + <LinearLayout + android:id="@+id/interruptiveness_settings" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center" + android:orientation="vertical"> + <com.android.systemui.statusbar.notification.row.ButtonLinearLayout + android:id="@+id/automatic" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="@dimen/notification_importance_button_separation" + android:padding="@dimen/notification_importance_button_padding" + android:clickable="true" + android:focusable="true" + android:background="@drawable/notification_guts_priority_button_bg" + android:orientation="vertical" + android:visibility="gone"> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:gravity="center" + > + <ImageView + android:id="@+id/automatic_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/ic_notifications_automatic" + android:background="@android:color/transparent" + android:tint="@color/notification_guts_priority_contents" + android:clickable="false" + android:focusable="false"/> + <TextView + android:id="@+id/automatic_label" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/notification_importance_drawable_padding" + android:layout_weight="1" + android:ellipsize="end" + android:maxLines="1" + android:clickable="false" + android:focusable="false" + android:textAppearance="@style/TextAppearance.NotificationImportanceButton" + android:text="@string/notification_automatic_title"/> + </LinearLayout> + <TextView + android:id="@+id/automatic_summary" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/notification_importance_button_description_top_margin" + android:visibility="gone" + android:text="@string/notification_channel_summary_automatic" + android:clickable="false" + android:focusable="false" + android:ellipsize="end" + android:maxLines="2" + android:textAppearance="@style/TextAppearance.NotificationImportanceDetail"/> + </com.android.systemui.statusbar.notification.row.ButtonLinearLayout> + + <com.android.systemui.statusbar.notification.row.ButtonLinearLayout + android:id="@+id/alert" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="@dimen/notification_importance_button_padding" + android:clickable="true" + android:focusable="true" + android:background="@drawable/notification_guts_priority_button_bg" + android:orientation="vertical"> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:gravity="center" + > + <ImageView + android:id="@+id/alert_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/ic_notifications_alert" + android:background="@android:color/transparent" + android:tint="@color/notification_guts_priority_contents" + android:clickable="false" + android:focusable="false"/> + <TextView + android:id="@+id/alert_label" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/notification_importance_drawable_padding" + android:layout_weight="1" + android:ellipsize="end" + android:maxLines="1" + android:clickable="false" + android:focusable="false" + android:textAppearance="@style/TextAppearance.NotificationImportanceButton" + android:text="@string/notification_alert_title"/> + </LinearLayout> + <TextView + android:id="@+id/alert_summary" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/notification_importance_button_description_top_margin" + android:visibility="gone" + android:text="@string/notification_channel_summary_default" + android:clickable="false" + android:focusable="false" + android:ellipsize="end" + android:maxLines="2" + android:textAppearance="@style/TextAppearance.NotificationImportanceDetail"/> + </com.android.systemui.statusbar.notification.row.ButtonLinearLayout> + + <com.android.systemui.statusbar.notification.row.ButtonLinearLayout + android:id="@+id/silence" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/notification_importance_button_separation" + android:padding="@dimen/notification_importance_button_padding" + android:clickable="true" + android:focusable="true" + android:background="@drawable/notification_guts_priority_button_bg" + android:orientation="vertical"> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:gravity="center" + > + <ImageView + android:id="@+id/silence_icon" + android:src="@drawable/ic_notifications_silence" + android:background="@android:color/transparent" + android:tint="@color/notification_guts_priority_contents" + android:layout_gravity="center" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:clickable="false" + android:focusable="false"/> + <TextView + android:id="@+id/silence_label" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ellipsize="end" + android:maxLines="1" + android:clickable="false" + android:focusable="false" + android:layout_toEndOf="@id/silence_icon" + android:layout_marginStart="@dimen/notification_importance_drawable_padding" + android:textAppearance="@style/TextAppearance.NotificationImportanceButton" + android:text="@string/notification_silence_title"/> + </LinearLayout> + <TextView + android:id="@+id/silence_summary" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/notification_importance_button_description_top_margin" + android:visibility="gone" + android:text="@string/notification_channel_summary_low" + android:clickable="false" + android:focusable="false" + android:ellipsize="end" + android:maxLines="2" + android:textAppearance="@style/TextAppearance.NotificationImportanceDetail"/> + </com.android.systemui.statusbar.notification.row.ButtonLinearLayout> + + </LinearLayout> + + <RelativeLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="60dp" + android:gravity="center_vertical" + android:paddingStart="4dp" + android:paddingEnd="4dp" + > + <TextView + android:id="@+id/promoted_demote" + android:text="@string/notification_inline_disable_promotion" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:gravity="start|center_vertical" + android:minWidth="@dimen/notification_importance_toggle_size" + android:minHeight="@dimen/notification_importance_toggle_size" + android:maxWidth="200dp" + style="@style/TextAppearance.NotificationInfo.Button"/> + <TextView + android:id="@+id/promoted_dismiss" + android:text="@string/notification_inline_dismiss" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentEnd="true" + android:gravity="end|center_vertical" + android:minWidth="@dimen/notification_importance_toggle_size" + android:minHeight="@dimen/notification_importance_toggle_size" + android:maxWidth="125dp" + style="@style/TextAppearance.NotificationInfo.Button"/> + </RelativeLayout> + + <RelativeLayout + android:id="@+id/bottom_buttons" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="60dp" + android:gravity="center_vertical" + android:paddingStart="4dp" + android:paddingEnd="4dp" + > + <TextView + android:id="@+id/turn_off_notifications" + android:text="@string/inline_turn_off_notifications" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:gravity="start|center_vertical" + android:minWidth="@dimen/notification_importance_toggle_size" + android:minHeight="@dimen/notification_importance_toggle_size" + android:maxWidth="200dp" + style="@style/TextAppearance.NotificationInfo.Button"/> + <TextView + android:id="@+id/done" + android:text="@string/inline_ok_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentEnd="true" + android:gravity="end|center_vertical" + android:minWidth="@dimen/notification_importance_toggle_size" + android:minHeight="@dimen/notification_importance_toggle_size" + android:maxWidth="125dp" + style="@style/TextAppearance.NotificationInfo.Button"/> + </RelativeLayout> + </LinearLayout> +</com.android.systemui.statusbar.notification.row.PromotedNotificationInfo> diff --git a/packages/SystemUI/res/layout/shade_carrier.xml b/packages/SystemUI/res/layout/shade_carrier.xml index 0fed393a7ed3..6a5df9c3ed10 100644 --- a/packages/SystemUI/res/layout/shade_carrier.xml +++ b/packages/SystemUI/res/layout/shade_carrier.xml @@ -33,7 +33,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" - android:textAppearance="@style/TextAppearance.QS.Status.Carriers" + android:textAppearance="@style/TextAppearance.QS.Status" android:textDirection="locale" android:marqueeRepeatLimit="marquee_forever" android:singleLine="true" diff --git a/packages/SystemUI/res/layout/shade_carrier_group.xml b/packages/SystemUI/res/layout/shade_carrier_group.xml index 2e8f98cbd190..6551f3b8160d 100644 --- a/packages/SystemUI/res/layout/shade_carrier_group.xml +++ b/packages/SystemUI/res/layout/shade_carrier_group.xml @@ -32,7 +32,7 @@ android:minWidth="48dp" android:minHeight="48dp" android:gravity="center_vertical" - android:textAppearance="@style/TextAppearance.QS.Status.Carriers.NoCarrierText" + android:textAppearance="@style/TextAppearance.QS.Status" android:textDirection="locale" android:marqueeRepeatLimit="marquee_forever" android:singleLine="true" diff --git a/packages/SystemUI/res/layout/volume_dialog_slider.xml b/packages/SystemUI/res/layout/volume_dialog_slider.xml index c5f468e731f5..2628f4991b49 100644 --- a/packages/SystemUI/res/layout/volume_dialog_slider.xml +++ b/packages/SystemUI/res/layout/volume_dialog_slider.xml @@ -18,14 +18,10 @@ android:layout_height="match_parent" android:maxHeight="@dimen/volume_dialog_slider_height"> - <com.google.android.material.slider.Slider + <androidx.compose.ui.platform.ComposeView android:id="@+id/volume_dialog_slider" - style="@style/SystemUI.Material3.Slider.Volume" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center" - android:layout_marginTop="-20dp" - android:layout_marginBottom="-20dp" - android:orientation="vertical" - android:theme="@style/Theme.Material3.DayNight" /> + android:orientation="vertical" /> </FrameLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/values-xlarge-land/config.xml b/packages/SystemUI/res/values-xlarge-land/config.xml index 5e4304e1c13a..6d8b64ade259 100644 --- a/packages/SystemUI/res/values-xlarge-land/config.xml +++ b/packages/SystemUI/res/values-xlarge-land/config.xml @@ -16,4 +16,5 @@ <resources> <item name="shortcut_helper_screen_width_fraction" format="float" type="dimen">0.8</item> + <bool name="center_align_magic_portrait_shape">true</bool> </resources> diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 36ede64f91d9..015e0e83e57d 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -24,6 +24,7 @@ <color name="qs_tile_divider">#29ffffff</color><!-- 16% white --> <color name="qs_detail_button_white">#B3FFFFFF</color><!-- 70% white --> <color name="status_bar_clock_color">#FFFFFFFF</color> + <color name="shade_header_text_color">#FFFFFFFF</color> <color name="qs_tile_disabled_color">#9E9E9E</color> <!-- 38% black --> <color name="status_bar_icons_hover_color_light">#38FFFFFF</color> <!-- 22% white --> <color name="status_bar_icons_hover_color_dark">#38000000</color> <!-- 22% black --> @@ -260,4 +261,8 @@ <!-- Rear Display Education --> <color name="rear_display_overlay_animation_background_color">#1E1B17</color> <color name="rear_display_overlay_dialog_background_color">#1E1B17</color> + + <!-- Low light Dream --> + <color name="low_light_clock_background_color">#000000</color> + <color name="low_light_clock_text_color">#CCCCCC</color> </resources> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 940e87d3d163..68e33f27aefa 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -1104,7 +1104,10 @@ --> <bool name="config_userSwitchingMustGoThroughLoginScreen">false</bool> - <!-- The dream component used when the device is low light environment. --> <string translatable="false" name="config_lowLightDreamComponent"/> + + <!--Whether we should position magic portrait shape effects in the center of lockscreen + it's false by default, and only be true in tablet landscape --> + <bool name="center_align_magic_portrait_shape">false</bool> </resources> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 8a0ffb90bf09..c7f037f3d619 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -266,6 +266,9 @@ <!-- Height of a large notification in the status bar --> <dimen name="notification_max_height">358dp</dimen> + <!-- Height of a large promoted ongoing notification in the status bar --> + <dimen name="notification_max_height_for_promoted_ongoing">272dp</dimen> + <!-- Height of a heads up notification in the status bar for legacy custom views --> <dimen name="notification_max_heads_up_height_legacy">128dp</dimen> @@ -1611,6 +1614,18 @@ <!-- GLANCEABLE_HUB -> DREAMING transition: Amount to shift dream overlay on entering --> <dimen name="hub_to_dreaming_transition_dream_overlay_translation_x">824dp</dimen> + <!-- Low light clock --> + <!-- The text size of the low light clock is intentionally defined in dp to avoid scaling --> + <dimen name="low_light_clock_text_size">260dp</dimen> + <dimen name="low_light_clock_charging_text_size">14sp</dimen> + <dimen name="low_light_clock_charging_text_min_height">48dp</dimen> + <integer name="low_light_clock_charging_text_font_weight">500</integer> + + <dimen name="low_light_clock_translate_animation_offset">40dp</dimen> + <integer name="low_light_clock_translate_animation_duration_ms">1167</integer> + <integer name="low_light_clock_alpha_animation_in_start_delay_ms">233</integer> + <integer name="low_light_clock_alpha_animation_duration_ms">250</integer> + <!-- Distance that the full shade transition takes in order for media to fully transition to the shade --> <dimen name="lockscreen_shade_media_transition_distance">120dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 1e217de60bad..3b89e9c42c93 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2083,6 +2083,12 @@ <!-- [CHAR LIMIT=80] Text shown in feedback button in notification guts for a bundled notification --> <string name="notification_guts_bundle_feedback" translatable="false">Provide Bundle Feedback</string> + <!-- [CHAR LIMIT=30] Text shown in button used to dismiss this single notification. --> + <string name="notification_inline_dismiss">Dismiss</string> + + <!-- [CHAR LIMIT=30] Text shown in button used to prevent app from showing Live Updates, for this notification and all future ones --> + <string name="notification_inline_disable_promotion">Don\'t show again</string> + <!-- Notification: Control panel: Label that displays when the app's notifications cannot be blocked. --> <string name="notification_unblockable_desc">These notifications can\'t be modified.</string> @@ -2310,9 +2316,6 @@ <string name="group_system_lock_screen">Lock screen</string> <!-- User visible title for the keyboard shortcut that pulls up Notes app for quick memo. [CHAR LIMIT=70] --> <string name="group_system_quick_memo">Take a note</string> - <!-- TODO(b/383734125): make it translatable once string is finalized by UXW.--> - <!-- User visible title for the keyboard shortcut that toggles Voice Access. [CHAR LIMIT=70] --> - <string name="group_system_toggle_voice_access" translatable="false">Toggle Voice Access</string> <!-- User visible title for the multitasking keyboard shortcuts list. [CHAR LIMIT=70] --> <string name="keyboard_shortcut_group_system_multitasking">Multitasking</string> @@ -2371,6 +2374,23 @@ <!-- User visible title for the keyboard shortcut that takes the user to the maps app. [CHAR LIMIT=70] --> <string name="keyboard_shortcut_group_applications_maps">Maps</string> + <!-- User visible title for the keyboard shortcut that toggles bounce keys. [CHAR LIMIT=70]--> + <string name="group_accessibility_toggle_bounce_keys">Toggle bounce keys</string> + <!-- User visible title for the keyboard shortcut that toggles mouse keys. [CHAR LIMIT=70]--> + <string name="group_accessibility_toggle_mouse_keys">Toggle mouse keys</string> + <!-- User visible title for the keyboard shortcut that toggles sticky keys. [CHAR LIMIT=70]--> + <string name="group_accessibility_toggle_sticky_keys">Toggle sticky keys</string> + <!-- User visible title for the keyboard shortcut that toggles slow keys. [CHAR LIMIT=70]--> + <string name="group_accessibility_toggle_slow_keys">Toggle slow keys</string> + <!-- User visible title for the keyboard shortcut that toggles Voice Access. [CHAR LIMIT=70] --> + <string name="group_accessibility_toggle_voice_access">Toggle Voice Access</string> + <!-- User visible title for the keyboard shortcut that toggles Talkback. [CHAR LIMIT=70] --> + <string name="group_accessibility_toggle_talkback">Toggle Talkback</string> + <!-- User visible title for the keyboard shortcut that toggles Magnification. [CHAR LIMIT=70] --> + <string name="group_accessibility_toggle_magnification">Toggle Magnification</string> + <!-- User visible title for the keyboard shortcut that activates Select to Speak service. [CHAR LIMIT=70] --> + <string name="group_accessibility_activate_select_to_speak">Activate Select to Speak</string> + <!-- SysUI Tuner: Label for screen about do not disturb settings [CHAR LIMIT=60] --> <string name="volume_and_do_not_disturb">Do Not Disturb</string> @@ -4005,13 +4025,13 @@ <!-- Touchpad switch apps gesture action name in tutorial [CHAR LIMIT=NONE] --> <string name="touchpad_switch_apps_gesture_action_title">Switch apps</string> <!-- Touchpad switch apps gesture guidance in gestures tutorial [CHAR LIMIT=NONE] --> - <string name="touchpad_switch_apps_gesture_guidance">Swipe left using four fingers on your touchpad</string> + <string name="touchpad_switch_apps_gesture_guidance">Swipe right using four fingers on your touchpad</string> <!-- Screen title after switch apps gesture was done successfully [CHAR LIMIT=NONE] --> <string name="touchpad_switch_apps_gesture_success_title">Great job!</string> <!-- Text shown to the user after they complete switch apps gesture tutorial [CHAR LIMIT=NONE] --> <string name="touchpad_switch_apps_gesture_success_body">You completed the switch apps gesture.</string> <!-- Text shown to the user after switch gesture was not done correctly [CHAR LIMIT=NONE] --> - <string name="touchpad_switch_gesture_error_body">Swipe left using four fingers on your touchpad to switch apps</string> + <string name="touchpad_switch_gesture_error_body">Swipe right using four fingers on your touchpad to switch apps</string> <!-- KEYBOARD TUTORIAL--> <!-- Action key tutorial title [CHAR LIMIT=NONE] --> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index b0d9bed05e27..fa6a41a74ca9 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -176,17 +176,11 @@ <style name="TextAppearance.QS.Status"> <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item> - <item name="android:textColor">?attr/onSurface</item> + <item name="android:textColor">@color/shade_header_text_color</item> <item name="android:textSize">14sp</item> <item name="android:letterSpacing">0.01</item> </style> - <style name="TextAppearance.QS.Status.Carriers" /> - - <style name="TextAppearance.QS.Status.Carriers.NoCarrierText"> - <item name="android:textColor">?attr/onSurfaceVariant</item> - </style> - <style name="TextAppearance.QS.Status.Build"> <item name="android:textColor">?attr/onSurfaceVariant</item> </style> @@ -565,16 +559,6 @@ <item name="android:windowNoTitle">true</item> </style> - <style name="SystemUI.Material3.Slider.Volume"> - <item name="trackHeight">40dp</item> - <item name="thumbHeight">52dp</item> - <item name="trackCornerSize">12dp</item> - <item name="trackInsideCornerSize">2dp</item> - <item name="trackStopIndicatorSize">6dp</item> - <item name="trackIconSize">20dp</item> - <item name="labelBehavior">gone</item> - </style> - <style name="SystemUI.Material3.Slider" parent="@style/Widget.Material3.Slider"> <item name="labelStyle">@style/Widget.Material3.Slider.Label</item> <item name="thumbColor">@color/thumb_color</item> diff --git a/packages/SystemUI/res/xml/gradient_color_wallpaper.xml b/packages/SystemUI/res/xml/gradient_color_wallpaper.xml new file mode 100644 index 000000000000..f453a4450750 --- /dev/null +++ b/packages/SystemUI/res/xml/gradient_color_wallpaper.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License + --> +<wallpaper + xmlns:android="http://schemas.android.com/apk/res/android" + android:showMetadataInPreview="false" + android:supportsMultipleDisplays="true" />
\ No newline at end of file diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ILauncherProxy.aidl index d363e524a9f2..b43ffc530289 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ILauncherProxy.aidl @@ -23,8 +23,8 @@ import android.os.IRemoteCallback; import android.view.MotionEvent; import com.android.systemui.shared.recents.ISystemUiProxy; -// Next ID: 38 -oneway interface IOverviewProxy { +// Next ID: 39 +oneway interface ILauncherProxy { void onActiveNavBarRegionChanges(in Region activeRegion) = 11; @@ -140,7 +140,7 @@ oneway interface IOverviewProxy { void appTransitionPending(boolean pending) = 34; /** - * Sent right after OverviewProxy calls unbindService() on the TouchInteractionService. + * Sent right after LauncherProxyService calls unbindService() on the TouchInteractionService. * TouchInteractionService is expected to send the reply once it has finished cleaning up. */ void onUnbind(IRemoteCallback reply) = 35; @@ -154,4 +154,9 @@ oneway interface IOverviewProxy { * Sent when {@link TaskbarDelegate#onDisplayRemoved} is called. */ void onDisplayRemoved(int displayId) = 37; + + /** + * Sent when {@link TaskbarDelegate#onDisplayRemoveSystemDecorations} is called. + */ + void onDisplayRemoveSystemDecorations(int displayId) = 38; } 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 e332280bc31a..1f6bea18d53a 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 @@ -69,10 +69,10 @@ interface ISystemUiProxy { /** * Indicates that the given Assist invocation types should be handled by Launcher via - * OverviewProxy#onAssistantOverrideInvoked and should not be invoked by SystemUI. + * LauncherProxy#onAssistantOverrideInvoked and should not be invoked by SystemUI. * * @param invocationTypes The invocation types that will henceforth be handled via - * OverviewProxy (Launcher); other invocation types should be handled by SysUI. + * LauncherProxy (Launcher); other invocation types should be handled by SysUI. */ oneway void setAssistantOverridesRequested(in int[] invocationTypes) = 53; diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java index 8576a6ebac42..a518c57bdd16 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java @@ -72,6 +72,25 @@ public class Task { @ViewDebug.ExportedProperty(category = "recents") public final int displayId; + /** + * The component of the first activity in the task, can be considered the "application" of + * this task. + */ + @Nullable + public ComponentName baseActivity; + /** + * The number of activities in this task (including running). + */ + public int numActivities; + /** + * Whether the top activity is to be displayed. See {@link android.R.attr#windowNoDisplay}. + */ + public boolean isTopActivityNoDisplay; + /** + * Whether fillsParent() is false for every activity in the tasks stack. + */ + public boolean isActivityStackTransparent; + // The source component name which started this task public final ComponentName sourceComponent; @@ -90,6 +109,10 @@ public class Task { this.userId = t.userId; this.lastActiveTime = t.lastActiveTime; this.displayId = t.displayId; + this.baseActivity = t.baseActivity; + this.numActivities = t.numActivities; + this.isTopActivityNoDisplay = t.isTopActivityNoDisplay; + this.isActivityStackTransparent = t.isActivityStackTransparent; updateHashCode(); } @@ -106,7 +129,9 @@ public class Task { } public TaskKey(int id, int windowingMode, @NonNull Intent intent, - ComponentName sourceComponent, int userId, long lastActiveTime, int displayId) { + ComponentName sourceComponent, int userId, long lastActiveTime, int displayId, + @Nullable ComponentName baseActivity, int numActivities, + boolean isTopActivityNoDisplay, boolean isActivityStackTransparent) { this.id = id; this.windowingMode = windowingMode; this.baseIntent = intent; @@ -114,6 +139,10 @@ public class Task { this.userId = userId; this.lastActiveTime = lastActiveTime; this.displayId = displayId; + this.baseActivity = baseActivity; + this.numActivities = numActivities; + this.isTopActivityNoDisplay = isTopActivityNoDisplay; + this.isActivityStackTransparent = isActivityStackTransparent; updateHashCode(); } @@ -185,6 +214,10 @@ public class Task { parcel.writeLong(lastActiveTime); parcel.writeInt(displayId); parcel.writeTypedObject(sourceComponent, flags); + parcel.writeTypedObject(baseActivity, flags); + parcel.writeInt(numActivities); + parcel.writeBoolean(isTopActivityNoDisplay); + parcel.writeBoolean(isActivityStackTransparent); } private static TaskKey readFromParcel(Parcel parcel) { @@ -195,9 +228,14 @@ public class Task { long lastActiveTime = parcel.readLong(); int displayId = parcel.readInt(); ComponentName sourceComponent = parcel.readTypedObject(ComponentName.CREATOR); + ComponentName baseActivity = parcel.readTypedObject(ComponentName.CREATOR); + int numActivities = parcel.readInt(); + boolean isTopActivityNoDisplay = parcel.readBoolean(); + boolean isActivityStackTransparent = parcel.readBoolean(); return new TaskKey(id, windowingMode, baseIntent, sourceComponent, userId, - lastActiveTime, displayId); + lastActiveTime, displayId, baseActivity, numActivities, isTopActivityNoDisplay, + isActivityStackTransparent); } @Override diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java index 41ad4373455e..9ebb15f43307 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java @@ -16,11 +16,12 @@ package com.android.systemui.shared.recents.utilities; -import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT; -import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN; -import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN; +import static android.app.StatusBarManager.NAVBAR_BACK_DISMISS_IME; +import static android.app.StatusBarManager.NAVBAR_IME_SWITCHER_BUTTON_VISIBLE; +import static android.app.StatusBarManager.NAVBAR_IME_VISIBLE; import android.annotation.TargetApi; +import android.app.StatusBarManager.NavbarFlags; import android.content.Context; import android.content.res.Resources; import android.graphics.Color; @@ -103,38 +104,46 @@ public class Utilities { } /** - * @return updated set of flags from InputMethodService based off {@param oldHints} - * Leaves original hints unmodified + * Updates the navigation bar state flags with the given IME state. + * + * @param oldFlags current navigation bar state flags. + * @param backDisposition the IME back disposition mode. Only takes effect if + * {@code isImeVisible} is {@code true}. + * @param isImeVisible whether the IME is currently visible. + * @param showImeSwitcher whether the IME Switcher button should be shown. Only takes effect if + * {@code isImeVisible} is {@code true}. */ - public static int calculateBackDispositionHints(int oldHints, - @BackDispositionMode int backDisposition, boolean imeShown, boolean showImeSwitcher) { - int hints = oldHints; + @NavbarFlags + public static int updateNavbarFlagsFromIme(@NavbarFlags int oldFlags, + @BackDispositionMode int backDisposition, boolean isImeVisible, + boolean showImeSwitcher) { + int flags = oldFlags; switch (backDisposition) { case InputMethodService.BACK_DISPOSITION_DEFAULT: case InputMethodService.BACK_DISPOSITION_WILL_NOT_DISMISS: case InputMethodService.BACK_DISPOSITION_WILL_DISMISS: - if (imeShown) { - hints |= NAVIGATION_HINT_BACK_ALT; + if (isImeVisible) { + flags |= NAVBAR_BACK_DISMISS_IME; } else { - hints &= ~NAVIGATION_HINT_BACK_ALT; + flags &= ~NAVBAR_BACK_DISMISS_IME; } break; case InputMethodService.BACK_DISPOSITION_ADJUST_NOTHING: - hints &= ~NAVIGATION_HINT_BACK_ALT; + flags &= ~NAVBAR_BACK_DISMISS_IME; break; } - if (imeShown) { - hints |= NAVIGATION_HINT_IME_SHOWN; + if (isImeVisible) { + flags |= NAVBAR_IME_VISIBLE; } else { - hints &= ~NAVIGATION_HINT_IME_SHOWN; + flags &= ~NAVBAR_IME_VISIBLE; } - if (showImeSwitcher) { - hints |= NAVIGATION_HINT_IME_SWITCHER_SHOWN; + if (showImeSwitcher && isImeVisible) { + flags |= NAVBAR_IME_SWITCHER_BUTTON_VISIBLE; } else { - hints &= ~NAVIGATION_HINT_IME_SWITCHER_SHOWN; + flags &= ~NAVBAR_IME_SWITCHER_BUTTON_VISIBLE; } - return hints; + return flags; } /** @return whether or not {@param context} represents that of a large screen device or not */ diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt index bd20777c7102..e928525afc14 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt @@ -16,6 +16,7 @@ package com.android.systemui.shared.shadow import android.content.Context +import android.content.res.TypedArray import android.graphics.Canvas import android.graphics.drawable.Drawable import android.util.AttributeSet @@ -31,19 +32,23 @@ constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, - defStyleRes: Int = 0 + defStyleRes: Int = 0, ) : TextView(context, attrs, defStyleAttr, defStyleRes) { - private val mKeyShadowInfo: ShadowInfo - private val mAmbientShadowInfo: ShadowInfo + private lateinit var mKeyShadowInfo: ShadowInfo + private lateinit var mAmbientShadowInfo: ShadowInfo init { - val attributes = + updateShadowDrawables( context.obtainStyledAttributes( attrs, R.styleable.DoubleShadowTextView, defStyleAttr, - defStyleRes + defStyleRes, ) + ) + } + + private fun updateShadowDrawables(attributes: TypedArray) { val drawableSize: Int val drawableInsetSize: Int try { @@ -70,17 +75,17 @@ constructor( ambientShadowBlur, ambientShadowOffsetX, ambientShadowOffsetY, - ambientShadowAlpha + ambientShadowAlpha, ) drawableSize = attributes.getDimensionPixelSize( R.styleable.DoubleShadowTextView_drawableIconSize, - 0 + 0, ) drawableInsetSize = attributes.getDimensionPixelSize( R.styleable.DoubleShadowTextView_drawableIconInsetSize, - 0 + 0, ) } finally { attributes.recycle() @@ -95,12 +100,19 @@ constructor( mAmbientShadowInfo, drawable, drawableSize, - drawableInsetSize + drawableInsetSize, ) } setCompoundDrawablesRelative(drawables[0], drawables[1], drawables[2], drawables[3]) } + override fun setTextAppearance(resId: Int) { + super.setTextAppearance(resId) + updateShadowDrawables( + context.obtainStyledAttributes(resId, R.styleable.DoubleShadowTextView) + ) + } + public override fun onDraw(canvas: Canvas) { applyShadows(mKeyShadowInfo, mAmbientShadowInfo, this, canvas) { super.onDraw(canvas) } } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java index 6f13d637d5c5..82ac78c6db15 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java @@ -98,12 +98,12 @@ public class QuickStepContract { public static final long SYSUI_STATE_ONE_HANDED_ACTIVE = 1L << 16; // Allow system gesture no matter the system bar(s) is visible or not public static final long SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY = 1L << 17; - // The IME is showing - public static final long SYSUI_STATE_IME_SHOWING = 1L << 18; + // The IME is visible. + public static final long SYSUI_STATE_IME_VISIBLE = 1L << 18; // The window magnification is overlapped with system gesture insets at the bottom. public static final long SYSUI_STATE_MAGNIFICATION_OVERLAP = 1L << 19; - // ImeSwitcher is showing - public static final long SYSUI_STATE_IME_SWITCHER_SHOWING = 1L << 20; + // The IME Switcher button is visible. + public static final long SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE = 1L << 20; // Device dozing/AOD state public static final long SYSUI_STATE_DEVICE_DOZING = 1L << 21; // The home feature is disabled (either by SUW/SysUI/device policy) @@ -134,6 +134,10 @@ public class QuickStepContract { public static final long SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING = 1L << 34; // Communal hub is showing public static final long SYSUI_STATE_COMMUNAL_HUB_SHOWING = 1L << 35; + // The back button is visually adjusted to indicate that it will dismiss the IME when pressed. + // This only takes effect while the IME is visible. By default, it is set while the IME is + // visible, but may be overridden by the backDispositionMode set by the IME. + public static final long SYSUI_STATE_BACK_DISMISS_IME = 1L << 36; // Mask for SystemUiStateFlags to isolate SYSUI_STATE_AWAKE and // SYSUI_STATE_WAKEFULNESS_TRANSITION, to match WAKEFULNESS_* constants @@ -168,9 +172,9 @@ public class QuickStepContract { SYSUI_STATE_DIALOG_SHOWING, SYSUI_STATE_ONE_HANDED_ACTIVE, SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY, - SYSUI_STATE_IME_SHOWING, + SYSUI_STATE_IME_VISIBLE, SYSUI_STATE_MAGNIFICATION_OVERLAP, - SYSUI_STATE_IME_SWITCHER_SHOWING, + SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE, SYSUI_STATE_DEVICE_DOZING, SYSUI_STATE_BACK_DISABLED, SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED, @@ -185,6 +189,7 @@ public class QuickStepContract { SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED, SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING, SYSUI_STATE_COMMUNAL_HUB_SHOWING, + SYSUI_STATE_BACK_DISMISS_IME, }) public @interface SystemUiStateFlags {} @@ -244,14 +249,14 @@ public class QuickStepContract { if ((flags & SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) != 0) { str.add("allow_gesture"); } - if ((flags & SYSUI_STATE_IME_SHOWING) != 0) { + if ((flags & SYSUI_STATE_IME_VISIBLE) != 0) { str.add("ime_visible"); } if ((flags & SYSUI_STATE_MAGNIFICATION_OVERLAP) != 0) { str.add("magnification_overlap"); } - if ((flags & SYSUI_STATE_IME_SWITCHER_SHOWING) != 0) { - str.add("ime_switcher_showing"); + if ((flags & SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE) != 0) { + str.add("ime_switcher_button_visible"); } if ((flags & SYSUI_STATE_DEVICE_DOZING) != 0) { str.add("device_dozing"); @@ -295,6 +300,9 @@ public class QuickStepContract { if ((flags & SYSUI_STATE_COMMUNAL_HUB_SHOWING) != 0) { str.add("communal_hub_showing"); } + if ((flags & SYSUI_STATE_BACK_DISMISS_IME) != 0) { + str.add("back_dismiss_ime"); + } return str.toString(); } diff --git a/packages/SystemUI/src/com/android/keyguard/CarrierText.java b/packages/SystemUI/src/com/android/keyguard/CarrierText.java index ae282c7e14bd..e2ac6a494020 100644 --- a/packages/SystemUI/src/com/android/keyguard/CarrierText.java +++ b/packages/SystemUI/src/com/android/keyguard/CarrierText.java @@ -22,6 +22,7 @@ import android.text.TextUtils; import android.text.method.SingleLineTransformationMethod; import android.util.AttributeSet; import android.view.View; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.TextView; import com.android.systemui.res.R; @@ -65,6 +66,14 @@ public class CarrierText extends TextView { } } + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + // Clear selected state set by CarrierTextController so "selected" not announced by + // accessibility but we can still marquee. + info.setSelected(false); + } + public boolean getShowAirplaneMode() { return mShowAirplaneMode; } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardFingerprintListenModel.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardFingerprintListenModel.kt index 953cf88feccb..943cbe87c8c2 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardFingerprintListenModel.kt +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardFingerprintListenModel.kt @@ -49,6 +49,7 @@ data class KeyguardFingerprintListenModel( var systemUser: Boolean = false, var udfps: Boolean = false, var userDoesNotHaveTrust: Boolean = false, + var communalShowing: Boolean = false, ) : KeyguardListenModel() { /** List of [String] to be used as a [Row] with [DumpsysTableLogger]. */ @@ -81,6 +82,7 @@ data class KeyguardFingerprintListenModel( systemUser.toString(), udfps.toString(), userDoesNotHaveTrust.toString(), + communalShowing.toString(), ) } @@ -122,6 +124,7 @@ data class KeyguardFingerprintListenModel( systemUser = model.systemUser udfps = model.udfps userDoesNotHaveTrust = model.userDoesNotHaveTrust + communalShowing = model.communalShowing } } @@ -170,6 +173,7 @@ data class KeyguardFingerprintListenModel( "systemUser", "underDisplayFingerprint", "userDoesNotHaveTrust", + "communalShowing", ) } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java index 7f176de547bc..0e9d8fec9717 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java @@ -16,6 +16,8 @@ package com.android.keyguard; +import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED; + import static com.android.systemui.Flags.pinInputFieldStyledFocusState; import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; @@ -164,6 +166,8 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB layoutParams.height = (int) getResources().getDimension( R.dimen.keyguard_pin_field_height); } + + mPasswordEntry.sendAccessibilityEvent(TYPE_VIEW_FOCUSED); } private void setKeyboardBasedFocusOutline(boolean isAnyKeyboardConnected) { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 101fcdfce7e5..c266a5b47cff 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -43,6 +43,7 @@ import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STR import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN; +import static com.android.systemui.Flags.glanceableHubV2; import static com.android.systemui.Flags.simPinBouncerReset; import static com.android.systemui.Flags.simPinUseSlotId; import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED; @@ -128,6 +129,7 @@ import com.android.systemui.biometrics.AuthController; import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider; import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; @@ -294,6 +296,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private final Provider<JavaAdapter> mJavaAdapter; private final Provider<SceneInteractor> mSceneInteractor; private final Provider<AlternateBouncerInteractor> mAlternateBouncerInteractor; + private final CommunalSceneInteractor mCommunalSceneInteractor; private final AuthController mAuthController; private final UiEventLogger mUiEventLogger; private final Set<String> mAllowFingerprintOnOccludingActivitiesFromPackage; @@ -404,6 +407,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab protected int mFingerprintRunningState = BIOMETRIC_STATE_STOPPED; private boolean mFingerprintDetectRunning; private boolean mIsDreaming; + private boolean mCommunalShowing; private int mActiveMobileDataSubscription = SubscriptionManager.INVALID_SUBSCRIPTION_ID; private final FingerprintInteractiveToAuthProvider mFingerprintInteractiveToAuthProvider; @@ -2205,7 +2209,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab IActivityTaskManager activityTaskManagerService, Provider<AlternateBouncerInteractor> alternateBouncerInteractor, Provider<JavaAdapter> javaAdapter, - Provider<SceneInteractor> sceneInteractor) { + Provider<SceneInteractor> sceneInteractor, + CommunalSceneInteractor communalSceneInteractor) { mContext = context; mSubscriptionManager = subscriptionManager; mUserTracker = userTracker; @@ -2254,6 +2259,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mAlternateBouncerInteractor = alternateBouncerInteractor; mJavaAdapter = javaAdapter; mSceneInteractor = sceneInteractor; + mCommunalSceneInteractor = communalSceneInteractor; mHandler = new Handler(mainLooper) { @Override @@ -2535,6 +2541,13 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab ); } + if (glanceableHubV2()) { + mJavaAdapter.get().alwaysCollectFlow( + mCommunalSceneInteractor.isCommunalVisible(), + this::onCommunalShowingChanged + ); + } + // start() can be invoked in the middle of user switching, so check for this state and issue // the call manually as that important event was missed. if (mUserTracker.isUserSwitching()) { @@ -2837,6 +2850,15 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } /** + * Sets whether the communal hub is showing. + */ + @VisibleForTesting + void onCommunalShowingChanged(boolean showing) { + mCommunalShowing = showing; + updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); + } + + /** * Whether the alternate bouncer is showing. */ public void setAlternateBouncerShowing(boolean showing) { @@ -2998,11 +3020,14 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab final boolean strongerAuthRequired = !isUnlockingWithFingerprintAllowed(); final boolean shouldListenBouncerState = !strongerAuthRequired || !isPrimaryBouncerShowingOrWillBeShowing(); + final boolean isUdfpsAuthRequiredOnCommunal = + !mCommunalShowing || isAlternateBouncerShowing(); final boolean shouldListenUdfpsState = !isUdfps || (!userCanSkipBouncer && !strongerAuthRequired - && userDoesNotHaveTrust); + && userDoesNotHaveTrust + && (!glanceableHubV2() || isUdfpsAuthRequiredOnCommunal)); boolean shouldListen = shouldListenKeyguardState && shouldListenUserState @@ -3033,7 +3058,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mSwitchingUser, mIsSystemUser, isUdfps, - userDoesNotHaveTrust)); + userDoesNotHaveTrust, + mCommunalShowing)); return shouldListen; } diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index 40c1f0f9895d..4a8e4ed3f6f1 100644 --- a/packages/SystemUI/src/com/android/systemui/Dependency.java +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -39,7 +39,7 @@ import com.android.systemui.plugins.DarkIconDispatcher; import com.android.systemui.plugins.PluginManager; import com.android.systemui.plugins.VolumeDialogController; import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.recents.LauncherProxyService; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationMediaManager; @@ -129,7 +129,7 @@ public class Dependency { @Inject Lazy<MetricsLogger> mMetricsLogger; @Inject Lazy<UiOffloadThread> mUiOffloadThread; @Inject Lazy<LightBarController> mLightBarController; - @Inject Lazy<OverviewProxyService> mOverviewProxyService; + @Inject Lazy<LauncherProxyService> mLauncherProxyService; @Inject Lazy<NavigationModeController> mNavBarModeController; @Inject Lazy<NavigationBarController> mNavigationBarController; @Inject Lazy<StatusBarStateController> mStatusBarStateController; @@ -175,7 +175,7 @@ public class Dependency { mProviders.put(MetricsLogger.class, mMetricsLogger::get); mProviders.put(UiOffloadThread.class, mUiOffloadThread::get); mProviders.put(LightBarController.class, mLightBarController::get); - mProviders.put(OverviewProxyService.class, mOverviewProxyService::get); + mProviders.put(LauncherProxyService.class, mLauncherProxyService::get); mProviders.put(NavigationModeController.class, mNavBarModeController::get); mProviders.put(NavigationBarController.class, mNavigationBarController::get); mProviders.put(StatusBarStateController.class, mStatusBarStateController::get); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java index 5cba464fc24c..5482c3d3ea18 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java @@ -50,7 +50,7 @@ import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.model.SysUiState; -import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.recents.LauncherProxyService; import com.android.systemui.settings.DisplayTracker; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.util.settings.SecureSettings; @@ -79,7 +79,7 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks private final Executor mExecutor; private final AccessibilityManager mAccessibilityManager; private final CommandQueue mCommandQueue; - private final OverviewProxyService mOverviewProxyService; + private final LauncherProxyService mLauncherProxyService; private final DisplayTracker mDisplayTracker; private final AccessibilityLogger mA11yLogger; @@ -225,13 +225,13 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks public MagnificationImpl(Context context, @Main Handler mainHandler, @Main Executor executor, CommandQueue commandQueue, ModeSwitchesController modeSwitchesController, - SysUiState sysUiState, OverviewProxyService overviewProxyService, + SysUiState sysUiState, LauncherProxyService launcherProxyService, SecureSettings secureSettings, DisplayTracker displayTracker, DisplayManager displayManager, AccessibilityLogger a11yLogger, IWindowManager iWindowManager, AccessibilityManager accessibilityManager, ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) { this(context, mainHandler.getLooper(), executor, commandQueue, - modeSwitchesController, sysUiState, overviewProxyService, secureSettings, + modeSwitchesController, sysUiState, launcherProxyService, secureSettings, displayTracker, displayManager, a11yLogger, iWindowManager, accessibilityManager, viewCaptureAwareWindowManager); } @@ -239,7 +239,7 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks @VisibleForTesting public MagnificationImpl(Context context, Looper looper, @Main Executor executor, CommandQueue commandQueue, ModeSwitchesController modeSwitchesController, - SysUiState sysUiState, OverviewProxyService overviewProxyService, + SysUiState sysUiState, LauncherProxyService launcherProxyService, SecureSettings secureSettings, DisplayTracker displayTracker, DisplayManager displayManager, AccessibilityLogger a11yLogger, IWindowManager iWindowManager, @@ -258,7 +258,7 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks mCommandQueue = commandQueue; mModeSwitchesController = modeSwitchesController; mSysUiState = sysUiState; - mOverviewProxyService = overviewProxyService; + mLauncherProxyService = launcherProxyService; mDisplayTracker = displayTracker; mA11yLogger = a11yLogger; mWindowMagnificationControllerSupplier = new WindowMagnificationControllerSupplier(context, @@ -279,7 +279,7 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks @Override public void start() { mCommandQueue.addCallback(this); - mOverviewProxyService.addCallback(new OverviewProxyService.OverviewProxyListener() { + mLauncherProxyService.addCallback(new LauncherProxyService.LauncherProxyListener() { @Override public void onConnectionChanged(boolean isConnected) { if (isConnected) { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java index e7470a34a065..102efcf7badd 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java @@ -17,6 +17,7 @@ package com.android.systemui.accessibility.floatingmenu; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_UNRESTRICTED_GESTURE_EXCLUSION; import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; import android.content.Context; @@ -90,9 +91,11 @@ class MenuViewLayerController implements IAccessibilityFloatingMenu { WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT); + params.setTitle("FloatingMenu"); params.receiveInsetsIgnoringZOrder = true; params.privateFlags |= - PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION | SYSTEM_FLAG_SHOW_FOR_ALL_USERS; + PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION | SYSTEM_FLAG_SHOW_FOR_ALL_USERS + | PRIVATE_FLAG_UNRESTRICTED_GESTURE_EXCLUSION; params.windowAnimations = android.R.style.Animation_Translucent; // Insets are configured to allow the menu to display over navigation and system bars. params.setFitInsetsTypes(0); diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java index 939d96e67f8f..da1c1bc49d23 100644 --- a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java +++ b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java @@ -37,7 +37,7 @@ import com.android.systemui.assist.ui.DefaultUiController; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.model.SysUiState; -import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.recents.LauncherProxyService; import com.android.systemui.res.R; import com.android.systemui.settings.DisplayTracker; import com.android.systemui.settings.UserTracker; @@ -142,7 +142,7 @@ public class AssistManager { protected final Context mContext; private final AssistDisclosure mAssistDisclosure; private final PhoneStateMonitor mPhoneStateMonitor; - private final OverviewProxyService mOverviewProxyService; + private final LauncherProxyService mLauncherProxyService; private final UiController mUiController; protected final Lazy<SysUiState> mSysUiState; protected final AssistLogger mAssistLogger; @@ -176,7 +176,7 @@ public class AssistManager { private final CommandQueue mCommandQueue; protected final AssistUtils mAssistUtils; - // Invocation types that should be sent over OverviewProxy instead of handled here. + // Invocation types that should be sent over LauncherProxy instead of handled here. private int[] mAssistOverrideInvocationTypes; @Inject @@ -186,7 +186,7 @@ public class AssistManager { AssistUtils assistUtils, CommandQueue commandQueue, PhoneStateMonitor phoneStateMonitor, - OverviewProxyService overviewProxyService, + LauncherProxyService launcherProxyService, Lazy<SysUiState> sysUiState, DefaultUiController defaultUiController, AssistLogger assistLogger, @@ -203,7 +203,7 @@ public class AssistManager { mCommandQueue = commandQueue; mAssistUtils = assistUtils; mAssistDisclosure = new AssistDisclosure(context, uiHandler, viewCaptureAwareWindowManager); - mOverviewProxyService = overviewProxyService; + mLauncherProxyService = launcherProxyService; mPhoneStateMonitor = phoneStateMonitor; mAssistLogger = assistLogger; mUserTracker = userTracker; @@ -220,7 +220,7 @@ public class AssistManager { mSysUiState = sysUiState; - mOverviewProxyService.addCallback(new OverviewProxyService.OverviewProxyListener() { + mLauncherProxyService.addCallback(new LauncherProxyService.LauncherProxyListener() { @Override public void onAssistantProgress(float progress) { // Progress goes from 0 to 1 to indicate how close the assist gesture is to @@ -288,14 +288,14 @@ public class AssistManager { } if (shouldOverrideAssist(args)) { try { - if (mOverviewProxyService.getProxy() == null) { - Log.w(TAG, "No OverviewProxyService to invoke assistant override"); + if (mLauncherProxyService.getProxy() == null) { + Log.w(TAG, "No LauncherProxyService to invoke assistant override"); return; } - mOverviewProxyService.getProxy().onAssistantOverrideInvoked( + mLauncherProxyService.getProxy().onAssistantOverrideInvoked( args.getInt(INVOCATION_TYPE_KEY)); } catch (RemoteException e) { - Log.w(TAG, "Unable to invoke assistant via OverviewProxyService override", e); + Log.w(TAG, "Unable to invoke assistant via LauncherProxyService override", e); } return; } @@ -333,7 +333,7 @@ public class AssistManager { return shouldOverrideAssist(invocationType); } - /** @return true if the invocation type should be handled by OverviewProxy instead of SysUI. */ + /** @return true if the invocation type should be handled by LauncherProxy instead of SysUI. */ public boolean shouldOverrideAssist(int invocationType) { return mAssistOverrideInvocationTypes != null && Arrays.stream(mAssistOverrideInvocationTypes).anyMatch( @@ -342,7 +342,7 @@ public class AssistManager { /** * @param invocationTypes The invocation types that will henceforth be handled via - * OverviewProxy (Launcher); other invocation types should be handled by + * LauncherProxy (Launcher); other invocation types should be handled by * this class. */ public void setAssistantOverridesRequested(int[] invocationTypes) { diff --git a/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java b/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java new file mode 100644 index 000000000000..2e1b5ad177b5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java @@ -0,0 +1,101 @@ +/* + * 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.communal; + +import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP; +import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_GOING_TO_SLEEP; + +import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.keyguard.KeyguardUpdateMonitorCallback; +import com.android.systemui.dagger.qualifiers.Application; +import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.shared.condition.Condition; +import com.android.systemui.statusbar.policy.KeyguardStateController; + +import kotlinx.coroutines.CoroutineScope; + +import javax.inject.Inject; + +/** + * Condition which estimates device inactivity in order to avoid launching a full-screen activity + * while the user is actively using the device. + */ +public class DeviceInactiveCondition extends Condition { + private final KeyguardStateController mKeyguardStateController; + private final WakefulnessLifecycle mWakefulnessLifecycle; + private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; + private final KeyguardStateController.Callback mKeyguardStateCallback = + new KeyguardStateController.Callback() { + @Override + public void onKeyguardShowingChanged() { + updateState(); + } + }; + private final WakefulnessLifecycle.Observer mWakefulnessObserver = + new WakefulnessLifecycle.Observer() { + @Override + public void onStartedGoingToSleep() { + updateState(); + } + }; + private final KeyguardUpdateMonitorCallback mKeyguardUpdateCallback = + new KeyguardUpdateMonitorCallback() { + @Override + public void onDreamingStateChanged(boolean dreaming) { + updateState(); + } + }; + + @Inject + public DeviceInactiveCondition(@Application CoroutineScope scope, + KeyguardStateController keyguardStateController, + WakefulnessLifecycle wakefulnessLifecycle, + KeyguardUpdateMonitor keyguardUpdateMonitor) { + super(scope); + mKeyguardStateController = keyguardStateController; + mWakefulnessLifecycle = wakefulnessLifecycle; + mKeyguardUpdateMonitor = keyguardUpdateMonitor; + } + + @Override + protected void start() { + updateState(); + mKeyguardStateController.addCallback(mKeyguardStateCallback); + mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateCallback); + mWakefulnessLifecycle.addObserver(mWakefulnessObserver); + } + + @Override + protected void stop() { + mKeyguardStateController.removeCallback(mKeyguardStateCallback); + mKeyguardUpdateMonitor.removeCallback(mKeyguardUpdateCallback); + mWakefulnessLifecycle.removeObserver(mWakefulnessObserver); + } + + @Override + protected int getStartStrategy() { + return START_EAGERLY; + } + + private void updateState() { + final boolean asleep = + mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_ASLEEP + || mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_GOING_TO_SLEEP; + updateCondition(asleep || mKeyguardStateController.isShowing() + || mKeyguardUpdateMonitor.isDreaming()); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index c02784dfab1b..fe5a82cb5b8c 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -81,6 +81,7 @@ import com.android.systemui.keyguard.ui.composable.LockscreenContent; import com.android.systemui.log.dagger.LogModule; import com.android.systemui.log.dagger.MonitorLog; import com.android.systemui.log.table.TableLogBuffer; +import com.android.systemui.lowlightclock.dagger.LowLightModule; import com.android.systemui.mediaprojection.MediaProjectionModule; import com.android.systemui.mediaprojection.appselector.MediaProjectionActivitiesModule; import com.android.systemui.mediaprojection.taskswitcher.MediaProjectionTaskSwitcherModule; @@ -285,7 +286,8 @@ import javax.inject.Named; UserModule.class, UtilModule.class, NoteTaskModule.class, - WalletModule.class + WalletModule.class, + LowLightModule.class }, subcomponents = { ComplicationComponent.class, diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java index faab31eff4f7..15f73ee0eda6 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java @@ -48,6 +48,8 @@ import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig; import com.android.systemui.res.R; import com.android.systemui.touch.TouchInsetManager; +import com.google.android.systemui.lowlightclock.LowLightClockDreamService; + import dagger.Binds; import dagger.BindsOptionalOf; import dagger.Module; @@ -238,15 +240,24 @@ public interface DreamModule { ComponentName bindsLowLightClockDream(); /** + * Provides low light clock dream service component. + */ + @Provides + @Named(LOW_LIGHT_CLOCK_DREAM) + static ComponentName providesLowLightClockDream(Context context) { + return new ComponentName(context, LowLightClockDreamService.class); + } + + /** * Provides the component name of the low light dream, or null if not configured. */ @Provides @Nullable @Named(LOW_LIGHT_DREAM_SERVICE) static ComponentName providesLowLightDreamService(Context context, - @Named(LOW_LIGHT_CLOCK_DREAM) Optional<ComponentName> clockDream) { - if (Flags.lowLightClockDream() && clockDream.isPresent()) { - return clockDream.get(); + @Named(LOW_LIGHT_CLOCK_DREAM) ComponentName clockDream) { + if (Flags.lowLightClockDream()) { + return clockDream; } String lowLightDreamComponent = context.getResources().getString( diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt index d7a4dba3188a..9bdf812713d7 100644 --- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt @@ -37,8 +37,8 @@ import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType.KEYBOARD import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType.TOUCHPAD import com.android.systemui.inputdevice.tutorial.data.repository.TutorialSchedulerRepository -import com.android.systemui.recents.OverviewProxyService -import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener +import com.android.systemui.recents.LauncherProxyService +import com.android.systemui.recents.LauncherProxyService.LauncherProxyListener import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import java.time.Clock import java.time.Instant @@ -67,7 +67,7 @@ constructor( private val contextualEducationInteractor: ContextualEducationInteractor, private val userInputDeviceRepository: UserInputDeviceRepository, private val tutorialRepository: TutorialSchedulerRepository, - private val overviewProxyService: OverviewProxyService, + private val launcherProxyService: LauncherProxyService, private val metricsLogger: ContextualEducationMetricsLogger, @EduClock private val clock: Clock, ) : CoreStartable { @@ -100,8 +100,8 @@ constructor( val educationTriggered = _educationTriggered.asStateFlow() private val statsUpdateRequests: Flow<StatsUpdateRequest> = conflatedCallbackFlow { - val listener: OverviewProxyListener = - object : OverviewProxyListener { + val listener: LauncherProxyListener = + object : LauncherProxyListener { override fun updateContextualEduStats( isTrackpadGesture: Boolean, gestureType: GestureType, @@ -113,8 +113,8 @@ constructor( } } - overviewProxyService.addCallback(listener) - awaitClose { overviewProxyService.removeCallback(listener) } + launcherProxyService.addCallback(listener) + awaitClose { launcherProxyService.removeCallback(listener) } } private val gestureModelMap: Flow<Map<GestureType, GestureEduModel>> = diff --git a/packages/SystemUI/src/com/android/systemui/grid/ui/compose/SpannedGrids.kt b/packages/SystemUI/src/com/android/systemui/grid/ui/compose/SpannedGrids.kt index 62ab18bbb738..96ef03c996a9 100644 --- a/packages/SystemUI/src/com/android/systemui/grid/ui/compose/SpannedGrids.kt +++ b/packages/SystemUI/src/com/android/systemui/grid/ui/compose/SpannedGrids.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.runtime.Composable +import androidx.compose.runtime.key import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.layout.Layout @@ -63,6 +64,7 @@ fun HorizontalSpannedGrid( rowSpacing: Dp, spans: List<Int>, modifier: Modifier = Modifier, + keys: (spanIndex: Int) -> Any = { it }, composables: @Composable BoxScope.(spanIndex: Int) -> Unit, ) { SpannedGrid( @@ -72,6 +74,7 @@ fun HorizontalSpannedGrid( spans = spans, isVertical = false, modifier = modifier, + keys = keys, composables = composables, ) } @@ -103,6 +106,7 @@ fun VerticalSpannedGrid( rowSpacing: Dp, spans: List<Int>, modifier: Modifier = Modifier, + keys: (spanIndex: Int) -> Any = { it }, composables: @Composable BoxScope.(spanIndex: Int) -> Unit, ) { SpannedGrid( @@ -112,6 +116,7 @@ fun VerticalSpannedGrid( spans = spans, isVertical = true, modifier = modifier, + keys = keys, composables = composables, ) } @@ -124,6 +129,7 @@ private fun SpannedGrid( spans: List<Int>, isVertical: Boolean, modifier: Modifier = Modifier, + keys: (spanIndex: Int) -> Any = { it }, composables: @Composable BoxScope.(spanIndex: Int) -> Unit, ) { val crossAxisArrangement = Arrangement.spacedBy(crossAxisSpacing) @@ -167,17 +173,19 @@ private fun SpannedGrid( Layout( { (0 until spans.size).map { spanIndex -> - Box( - Modifier.semantics { - collectionItemInfo = - if (isVertical) { - CollectionItemInfo(spanIndex, 1, 0, 1) - } else { - CollectionItemInfo(0, 1, spanIndex, 1) - } + key(keys(spanIndex)) { + Box( + Modifier.semantics { + collectionItemInfo = + if (isVertical) { + CollectionItemInfo(spanIndex, 1, 0, 1) + } else { + CollectionItemInfo(0, 1, spanIndex, 1) + } + } + ) { + composables(spanIndex) } - ) { - composables(spanIndex) } } }, diff --git a/packages/SystemUI/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModel.kt b/packages/SystemUI/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModel.kt index 84c4bdf1621a..49625f8f9360 100644 --- a/packages/SystemUI/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.haptics.msdl.qs import android.service.quicksettings.Tile +import androidx.compose.runtime.Stable import com.android.systemui.Flags import com.android.systemui.animation.Expandable import com.android.systemui.dagger.SysUISingleton @@ -175,6 +176,7 @@ constructor( } @SysUISingleton +@Stable class TileHapticsViewModelFactoryProvider @Inject constructor(private val tileHapticsViewModelFactory: TileHapticsViewModel.Factory) { diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepository.kt index e5c638cbdfba..d355f761e5ae 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepository.kt @@ -32,18 +32,19 @@ import com.android.systemui.keyboard.shared.model.ShortcutCustomizationRequestRe import com.android.systemui.keyboard.shared.model.ShortcutCustomizationRequestResult.ERROR_RESERVED_COMBINATION import com.android.systemui.keyboard.shared.model.ShortcutCustomizationRequestResult.SUCCESS import com.android.systemui.settings.UserTracker +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.withContext -import javax.inject.Inject -import kotlin.coroutines.CoroutineContext @SysUISingleton class CustomInputGesturesRepository @Inject -constructor(private val userTracker: UserTracker, - @Background private val bgCoroutineContext: CoroutineContext) -{ +constructor( + private val userTracker: UserTracker, + @Background private val bgCoroutineContext: CoroutineContext, +) { private val userContext: Context get() = userTracker.createCurrentUserContext(userTracker.userContext) @@ -55,8 +56,7 @@ constructor(private val userTracker: UserTracker, private val _customInputGesture = MutableStateFlow<List<InputGestureData>>(emptyList()) - val customInputGestures = - _customInputGesture.onStart { refreshCustomInputGestures() } + val customInputGestures = _customInputGesture.onStart { refreshCustomInputGestures() } fun refreshCustomInputGestures() { setCustomInputGestures(inputGestures = retrieveCustomInputGestures()) @@ -72,24 +72,24 @@ constructor(private val userTracker: UserTracker, } else emptyList() } - suspend fun addCustomInputGesture(inputGesture: InputGestureData): ShortcutCustomizationRequestResult { + suspend fun addCustomInputGesture( + inputGesture: InputGestureData + ): ShortcutCustomizationRequestResult { return withContext(bgCoroutineContext) { when (val result = inputManager.addCustomInputGesture(inputGesture)) { CUSTOM_INPUT_GESTURE_RESULT_SUCCESS -> { refreshCustomInputGestures() SUCCESS } - CUSTOM_INPUT_GESTURE_RESULT_ERROR_ALREADY_EXISTS -> - ERROR_RESERVED_COMBINATION + CUSTOM_INPUT_GESTURE_RESULT_ERROR_ALREADY_EXISTS -> ERROR_RESERVED_COMBINATION - CUSTOM_INPUT_GESTURE_RESULT_ERROR_RESERVED_GESTURE -> - ERROR_RESERVED_COMBINATION + CUSTOM_INPUT_GESTURE_RESULT_ERROR_RESERVED_GESTURE -> ERROR_RESERVED_COMBINATION else -> { Log.w( TAG, "Attempted to add inputGesture: $inputGesture " + - "but ran into an error with code: $result", + "but ran into an error with code: $result", ) ERROR_OTHER } @@ -97,11 +97,11 @@ constructor(private val userTracker: UserTracker, } } - suspend fun deleteCustomInputGesture(inputGesture: InputGestureData): ShortcutCustomizationRequestResult { - return withContext(bgCoroutineContext){ - when ( - val result = inputManager.removeCustomInputGesture(inputGesture) - ) { + suspend fun deleteCustomInputGesture( + inputGesture: InputGestureData + ): ShortcutCustomizationRequestResult { + return withContext(bgCoroutineContext) { + when (val result = inputManager.removeCustomInputGesture(inputGesture)) { CUSTOM_INPUT_GESTURE_RESULT_SUCCESS -> { refreshCustomInputGestures() SUCCESS @@ -110,7 +110,7 @@ constructor(private val userTracker: UserTracker, Log.w( TAG, "Attempted to delete inputGesture: $inputGesture " + - "but ran into an error with code: $result", + "but ran into an error with code: $result", ) ERROR_OTHER } @@ -134,7 +134,10 @@ constructor(private val userTracker: UserTracker, } } + suspend fun getInputGestureByTrigger(trigger: InputGestureData.Trigger): InputGestureData? = + withContext(bgCoroutineContext) { inputManager.getInputGesture(trigger) } + private companion object { private const val TAG = "CustomInputGesturesRepository" } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt index 18ca877775df..6ae948d2da2e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyboard.shortcut.data.repository import android.hardware.input.InputGestureData import android.hardware.input.InputGestureData.Builder +import android.hardware.input.InputGestureData.Trigger import android.hardware.input.InputGestureData.createKeyTrigger import android.hardware.input.InputManager import android.hardware.input.KeyGestureEvent.KeyGestureType @@ -175,6 +176,11 @@ constructor( return customInputGesturesRepository.resetAllCustomInputGestures() } + suspend fun isSelectedKeyCombinationAvailable(): Boolean { + val trigger = buildTriggerFromSelectedKeyCombination() ?: return false + return customInputGesturesRepository.getInputGestureByTrigger(trigger) == null + } + private fun Builder.addKeyGestureTypeForShortcutBeingCustomized(): Builder { val keyGestureType = getKeyGestureTypeForShortcutBeingCustomized() @@ -222,7 +228,10 @@ constructor( ) } - private fun Builder.addTriggerFromSelectedKeyCombination(): Builder { + private fun Builder.addTriggerFromSelectedKeyCombination(): Builder = + setTrigger(buildTriggerFromSelectedKeyCombination()) + + private fun buildTriggerFromSelectedKeyCombination(): Trigger? { val selectedKeyCombination = _selectedKeyCombination.value if (selectedKeyCombination?.keyCode == null) { Log.w( @@ -230,16 +239,14 @@ constructor( "User requested to set shortcut but selected key combination is " + "$selectedKeyCombination", ) - return this + return null } - return setTrigger( - createKeyTrigger( - /* keycode = */ selectedKeyCombination.keyCode, - /* modifierState = */ shortcutCategoriesUtils.removeUnsupportedModifiers( - selectedKeyCombination.modifiers - ), - ) + return createKeyTrigger( + /* keycode= */ selectedKeyCombination.keyCode, + /* modifierState= */ shortcutCategoriesUtils.removeUnsupportedModifiers( + selectedKeyCombination.modifiers + ), ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt index 6a42cdc876ca..0908e3b5bf85 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyboard.shortcut.data.repository import android.content.Context +import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_BACK import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT @@ -39,10 +40,16 @@ import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFO import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT +import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS +import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW +import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL +import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS +import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_STICKY_KEYS +import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS -import com.android.hardware.input.Flags.enableVoiceAccessKeyGestures +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.Accessibility import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.AppCategories import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System @@ -65,7 +72,6 @@ class InputGestureMaps @Inject constructor(private val context: Context) { KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to System, KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to System, KEY_GESTURE_TYPE_ALL_APPS to System, - KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS to System, // Multitasking Category KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER to MultiTasking, @@ -82,6 +88,16 @@ class InputGestureMaps @Inject constructor(private val context: Context) { // App Category KEY_GESTURE_TYPE_LAUNCH_APPLICATION to AppCategories, + + // Accessibility Category + KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS to Accessibility, + KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS to Accessibility, + KEY_GESTURE_TYPE_TOGGLE_STICKY_KEYS to Accessibility, + KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS to Accessibility, + KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS to Accessibility, + KEY_GESTURE_TYPE_TOGGLE_TALKBACK to Accessibility, + KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION to Accessibility, + KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK to Accessibility, ) val gestureToInternalKeyboardShortcutGroupLabelResIdMap = @@ -103,7 +119,6 @@ class InputGestureMaps @Inject constructor(private val context: Context) { KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to R.string.shortcut_helper_category_system_apps, KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to R.string.shortcut_helper_category_system_apps, - KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS to R.string.shortcut_helper_category_system_apps, // Multitasking Category KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT to @@ -128,6 +143,17 @@ class InputGestureMaps @Inject constructor(private val context: Context) { // App Category KEY_GESTURE_TYPE_LAUNCH_APPLICATION to R.string.keyboard_shortcut_group_applications, + + // Accessibility Category + KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS to R.string.shortcutHelper_category_accessibility, + KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS to R.string.shortcutHelper_category_accessibility, + KEY_GESTURE_TYPE_TOGGLE_STICKY_KEYS to R.string.shortcutHelper_category_accessibility, + KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS to R.string.shortcutHelper_category_accessibility, + KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS to R.string.shortcutHelper_category_accessibility, + KEY_GESTURE_TYPE_TOGGLE_TALKBACK to R.string.shortcutHelper_category_accessibility, + KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION to R.string.shortcutHelper_category_accessibility, + KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK to + R.string.shortcutHelper_category_accessibility, ) /** @@ -152,7 +178,6 @@ class InputGestureMaps @Inject constructor(private val context: Context) { KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to R.string.group_system_access_google_assistant, KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to R.string.group_system_access_google_assistant, - KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS to R.string.group_system_toggle_voice_access, // Multitasking Category KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER to R.string.group_system_cycle_forward, @@ -169,6 +194,19 @@ class InputGestureMaps @Inject constructor(private val context: Context) { R.string.system_desktop_mode_toggle_maximize_window, KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY to R.string.system_multitasking_move_to_next_display, + + // Accessibility Category + KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS to R.string.group_accessibility_toggle_bounce_keys, + KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS to R.string.group_accessibility_toggle_mouse_keys, + KEY_GESTURE_TYPE_TOGGLE_STICKY_KEYS to R.string.group_accessibility_toggle_sticky_keys, + KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS to R.string.group_accessibility_toggle_slow_keys, + KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS to + R.string.group_accessibility_toggle_voice_access, + KEY_GESTURE_TYPE_TOGGLE_TALKBACK to R.string.group_accessibility_toggle_talkback, + KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION to + R.string.group_accessibility_toggle_magnification, + KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK to + R.string.group_accessibility_activate_select_to_speak, ) val shortcutLabelToKeyGestureTypeMap: Map<String, Int> diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/AccessibilityShortcutsSource.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/AccessibilityShortcutsSource.kt index d92c45591522..fdb80b2e0f87 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/AccessibilityShortcutsSource.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/AccessibilityShortcutsSource.kt @@ -17,9 +17,24 @@ package com.android.systemui.keyboard.shortcut.data.source import android.content.res.Resources +import android.hardware.input.InputSettings +import android.view.KeyEvent.KEYCODE_3 +import android.view.KeyEvent.KEYCODE_4 +import android.view.KeyEvent.KEYCODE_5 +import android.view.KeyEvent.KEYCODE_6 +import android.view.KeyEvent.KEYCODE_M +import android.view.KeyEvent.KEYCODE_S +import android.view.KeyEvent.KEYCODE_T +import android.view.KeyEvent.KEYCODE_V +import android.view.KeyEvent.META_ALT_ON +import android.view.KeyEvent.META_META_ON import android.view.KeyboardShortcutGroup import android.view.KeyboardShortcutInfo +import com.android.hardware.input.Flags.enableTalkbackAndMagnifierKeyGestures +import com.android.hardware.input.Flags.enableVoiceAccessKeyGestures +import com.android.hardware.input.Flags.keyboardA11yShortcutControl import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyboard.shortcut.data.model.shortcutInfo import com.android.systemui.res.R import javax.inject.Inject @@ -33,5 +48,96 @@ class AccessibilityShortcutsSource @Inject constructor(@Main private val resourc ) ) - private fun accessibilityShortcuts() = listOf<KeyboardShortcutInfo>() + private fun accessibilityShortcuts(): List<KeyboardShortcutInfo> { + val shortcuts = mutableListOf<KeyboardShortcutInfo>() + + if (keyboardA11yShortcutControl()) { + if (InputSettings.isAccessibilityBounceKeysFeatureEnabled()) { + shortcuts.add( + // Toggle bounce keys: + // - Meta + Alt + 3 + shortcutInfo( + resources.getString(R.string.group_accessibility_toggle_bounce_keys) + ) { + command(META_META_ON or META_ALT_ON, KEYCODE_3) + } + ) + } + if (InputSettings.isAccessibilityMouseKeysFeatureFlagEnabled()) { + shortcuts.add( + // Toggle mouse keys: + // - Meta + Alt + 4 + shortcutInfo( + resources.getString(R.string.group_accessibility_toggle_mouse_keys) + ) { + command(META_META_ON or META_ALT_ON, KEYCODE_4) + } + ) + } + if (InputSettings.isAccessibilityStickyKeysFeatureEnabled()) { + shortcuts.add( + // Toggle sticky keys: + // - Meta + Alt + 5 + shortcutInfo( + resources.getString(R.string.group_accessibility_toggle_sticky_keys) + ) { + command(META_META_ON or META_ALT_ON, KEYCODE_5) + } + ) + } + if (InputSettings.isAccessibilitySlowKeysFeatureFlagEnabled()) { + shortcuts.add( + // Toggle slow keys: + // - Meta + Alt + 6 + shortcutInfo( + resources.getString(R.string.group_accessibility_toggle_slow_keys) + ) { + command(META_META_ON or META_ALT_ON, KEYCODE_6) + } + ) + } + } + + if (enableVoiceAccessKeyGestures()) { + shortcuts.add( + // Toggle voice access: + // - Meta + Alt + V + shortcutInfo( + resources.getString(R.string.group_accessibility_toggle_voice_access) + ) { + command(META_META_ON or META_ALT_ON, KEYCODE_V) + } + ) + } + + if (enableTalkbackAndMagnifierKeyGestures()) { + shortcuts.add( + // Toggle talkback: + // - Meta + Alt + T + shortcutInfo(resources.getString(R.string.group_accessibility_toggle_talkback)) { + command(META_META_ON or META_ALT_ON, KEYCODE_T) + } + ) + shortcuts.add( + // Toggle magnification: + // - Meta + Alt + M + shortcutInfo( + resources.getString(R.string.group_accessibility_toggle_magnification) + ) { + command(META_META_ON or META_ALT_ON, KEYCODE_M) + } + ) + shortcuts.add( + // Activate Select to Speak: + // - Meta + Alt + S + shortcutInfo( + resources.getString(R.string.group_accessibility_activate_select_to_speak) + ) { + command(META_META_ON or META_ALT_ON, KEYCODE_S) + } + ) + } + + return shortcuts + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/MultitaskingShortcutsSource.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/MultitaskingShortcutsSource.kt index d785b5b5a7e7..464201f6ec12 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/MultitaskingShortcutsSource.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/MultitaskingShortcutsSource.kt @@ -29,12 +29,12 @@ import android.view.KeyEvent.KEYCODE_RIGHT_BRACKET import android.view.KeyEvent.META_CTRL_ON import android.view.KeyEvent.META_META_ON import android.view.KeyboardShortcutGroup +import android.window.DesktopModeFlags import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyboard.shortcut.data.model.shortcutInfo import com.android.systemui.res.R import com.android.window.flags.Flags.enableMoveToNextDisplayShortcut -import com.android.window.flags.Flags.enableTaskResizingKeyboardShortcuts import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import javax.inject.Inject @@ -85,7 +85,8 @@ constructor(@Main private val resources: Resources, @Application private val con ) } if ( - DesktopModeStatus.canEnterDesktopMode(context) && enableTaskResizingKeyboardShortcuts() + DesktopModeStatus.canEnterDesktopMode(context) && + DesktopModeFlags.ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS.isTrue ) { // Snap a freeform window to the left // - Meta + Left bracket diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt index c3c9df97a682..5060abdda247 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt @@ -32,14 +32,12 @@ import android.view.KeyEvent.KEYCODE_RECENT_APPS import android.view.KeyEvent.KEYCODE_S import android.view.KeyEvent.KEYCODE_SLASH import android.view.KeyEvent.KEYCODE_TAB -import android.view.KeyEvent.KEYCODE_V import android.view.KeyEvent.META_ALT_ON import android.view.KeyEvent.META_CTRL_ON import android.view.KeyEvent.META_META_ON import android.view.KeyEvent.META_SHIFT_ON import android.view.KeyboardShortcutGroup import android.view.KeyboardShortcutInfo -import com.android.hardware.input.Flags.enableVoiceAccessKeyGestures import com.android.systemui.Flags.shortcutHelperKeyGlyph import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyboard.shortcut.data.model.shortcutInfo @@ -120,8 +118,8 @@ constructor(@Main private val resources: Resources, private val inputManager: In return shortcuts } - private fun systemControlsShortcuts(): List<KeyboardShortcutInfo> { - val shortcuts = mutableListOf( + private fun systemControlsShortcuts() = + listOf( // Access list of all apps and search (i.e. Search/Launcher): // - Meta shortcutInfo(resources.getString(R.string.group_system_access_all_apps_search)) { @@ -178,19 +176,6 @@ constructor(@Main private val resources: Resources, private val inputManager: In }, ) - if (enableVoiceAccessKeyGestures()) { - shortcuts.add( - // Toggle voice access: - // - Meta + Alt + V - shortcutInfo(resources.getString(R.string.group_system_toggle_voice_access)) { - command(META_META_ON or META_ALT_ON, KEYCODE_V) - } - ) - } - - return shortcuts - } - private fun systemAppsShortcuts() = listOf( // Pull up Notes app for quick memo: diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutCustomizationInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutCustomizationInteractor.kt index ef242678a8ac..1a62517ad01d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutCustomizationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutCustomizationInteractor.kt @@ -53,4 +53,7 @@ constructor(private val customShortcutRepository: CustomShortcutCategoriesReposi suspend fun resetAllCustomShortcuts(): ShortcutCustomizationRequestResult { return customShortcutRepository.resetAllCustomShortcuts() } + + suspend fun isSelectedKeyCombinationAvailable(): Boolean = + customShortcutRepository.isSelectedKeyCombinationAvailable() } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt index f1945e657d52..54e27a61ac78 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt @@ -43,6 +43,7 @@ import com.android.systemui.statusbar.phone.create import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch class ShortcutCustomizationDialogStarter @@ -57,20 +58,25 @@ constructor( private val viewModel = viewModelFactory.create() override suspend fun onActivated(): Nothing { - viewModel.shortcutCustomizationUiState.collect { uiState -> - when (uiState) { - is AddShortcutDialog, - is DeleteShortcutDialog, - is ResetShortcutDialog -> { - if (dialog == null) { - dialog = createDialog().also { it.show() } + coroutineScope { + launch { + viewModel.shortcutCustomizationUiState.collect { uiState -> + when (uiState) { + is AddShortcutDialog, + is DeleteShortcutDialog, + is ResetShortcutDialog -> { + if (dialog == null) { + dialog = createDialog().also { it.show() } + } + } + is ShortcutCustomizationUiState.Inactive -> { + dialog?.dismiss() + dialog = null + } } } - is ShortcutCustomizationUiState.Inactive -> { - dialog?.dismiss() - dialog = null - } } + launch { viewModel.activate() } } awaitCancellation() } @@ -101,6 +107,7 @@ constructor( onConfirmResetShortcut = { coroutineScope.launch { viewModel.resetAllCustomShortcuts() } }, + onClearSelectedKeyCombination = { viewModel.clearSelectedKeyCombination() }, ) setDialogProperties(dialog, uiState) } @@ -108,22 +115,33 @@ constructor( private fun setDialogProperties(dialog: SystemUIDialog, uiState: ShortcutCustomizationUiState) { dialog.setOnDismissListener { viewModel.onDialogDismissed() } - dialog.setTitle( - resources.getString( - when (uiState) { - is AddShortcutDialog -> - R.string.shortcut_customize_mode_add_shortcut_description - is DeleteShortcutDialog -> - R.string.shortcut_customize_mode_remove_shortcut_description - else -> R.string.shortcut_customize_mode_reset_shortcut_description - } - ) - ) + dialog.setTitle("${getDialogTitle(uiState)}. ${getDialogDescription(uiState)}") // By default, apps cannot intercept action key. The system always handles it. This // flag is needed to enable customisation dialog window to intercept action key dialog.window?.addPrivateFlags(PRIVATE_FLAG_ALLOW_ACTION_KEY_EVENTS) } + private fun getDialogTitle(uiState: ShortcutCustomizationUiState): String { + return when (uiState) { + is AddShortcutDialog -> uiState.shortcutLabel + is DeleteShortcutDialog -> + resources.getString(R.string.shortcut_customize_mode_remove_shortcut_dialog_title) + else -> + resources.getString(R.string.shortcut_customize_mode_reset_shortcut_dialog_title) + } + } + + private fun getDialogDescription(uiState: ShortcutCustomizationUiState): String { + return resources.getString( + when (uiState) { + is AddShortcutDialog -> R.string.shortcut_customize_mode_add_shortcut_description + is DeleteShortcutDialog -> + R.string.shortcut_customize_mode_remove_shortcut_description + else -> R.string.shortcut_customize_mode_reset_shortcut_description + } + ) + } + @AssistedFactory interface Factory { fun create(): ShortcutCustomizationDialogStarter diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarter.kt index fa03883e2a35..ea36a10fb01a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarter.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarter.kt @@ -24,7 +24,6 @@ import android.os.UserHandle import android.provider.Settings import androidx.annotation.VisibleForTesting import androidx.compose.foundation.layout.width -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -36,6 +35,7 @@ import com.android.systemui.keyboard.shortcut.ui.composable.ShortcutHelper import com.android.systemui.keyboard.shortcut.ui.composable.ShortcutHelperBottomSheet import com.android.systemui.keyboard.shortcut.ui.composable.getWidth import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel +import com.android.systemui.lifecycle.rememberActivated import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R import com.android.systemui.statusbar.phone.SystemUIDialogFactory @@ -51,14 +51,13 @@ class ShortcutHelperDialogStarter constructor( @Application private val applicationScope: CoroutineScope, private val shortcutHelperViewModel: ShortcutHelperViewModel, - shortcutCustomizationDialogStarterFactory: ShortcutCustomizationDialogStarter.Factory, + private val shortcutCustomizationDialogStarterFactory: + ShortcutCustomizationDialogStarter.Factory, private val dialogFactory: SystemUIDialogFactory, private val activityStarter: ActivityStarter, ) : CoreStartable { @VisibleForTesting var dialog: Dialog? = null - private val shortcutCustomizationDialogStarter = - shortcutCustomizationDialogStarterFactory.create() override fun start() { shortcutHelperViewModel.shouldShow @@ -77,7 +76,10 @@ constructor( content = { dialog -> val shortcutsUiState by shortcutHelperViewModel.shortcutsUiState.collectAsStateWithLifecycle() - LaunchedEffect(Unit) { shortcutCustomizationDialogStarter.activate() } + val shortcutCustomizationDialogStarter = + rememberActivated(traceName = "shortcutCustomizationDialogStarter") { + shortcutCustomizationDialogStarterFactory.create() + } ShortcutHelper( modifier = Modifier.width(getWidth()), shortcutsUiState = shortcutsUiState, diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt index 550438aa220f..66e45056989d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt @@ -18,14 +18,10 @@ package com.android.systemui.keyboard.shortcut.ui.composable import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn @@ -40,13 +36,15 @@ import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.ErrorOutline import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusRequester @@ -57,9 +55,15 @@ import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.key.type +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.LiveRegionMode +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.liveRegion +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.android.compose.ui.graphics.painter.rememberDrawablePainter @@ -76,6 +80,7 @@ fun ShortcutCustomizationDialog( onConfirmSetShortcut: () -> Unit, onConfirmDeleteShortcut: () -> Unit, onConfirmResetShortcut: () -> Unit, + onClearSelectedKeyCombination: () -> Unit, ) { when (uiState) { is ShortcutCustomizationUiState.AddShortcutDialog -> { @@ -85,6 +90,7 @@ fun ShortcutCustomizationDialog( onShortcutKeyCombinationSelected, onCancel, onConfirmSetShortcut, + onClearSelectedKeyCombination, ) } is ShortcutCustomizationUiState.DeleteShortcutDialog -> { @@ -106,6 +112,7 @@ private fun AddShortcutDialog( onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean, onCancel: () -> Unit, onConfirmSetShortcut: () -> Unit, + onClearSelectedKeyCombination: () -> Unit, ) { Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) { Title(uiState.shortcutLabel) @@ -121,6 +128,7 @@ private fun AddShortcutDialog( onShortcutKeyCombinationSelected = onShortcutKeyCombinationSelected, pressedKeys = uiState.pressedKeys, onConfirmSetShortcut = onConfirmSetShortcut, + onClearSelectedKeyCombination = onClearSelectedKeyCombination, ) ErrorMessageContainer(uiState.errorMessage) DialogButtons( @@ -244,7 +252,11 @@ private fun ErrorMessageContainer(errorMessage: String) { lineHeight = 20.sp, fontWeight = FontWeight.W500, color = MaterialTheme.colorScheme.error, - modifier = Modifier.padding(start = 24.dp).width(252.dp), + modifier = + Modifier.padding(start = 24.dp).width(252.dp).semantics { + contentDescription = errorMessage + liveRegion = LiveRegionMode.Polite + }, ) } } @@ -256,72 +268,82 @@ private fun SelectedKeyCombinationContainer( onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean, pressedKeys: List<ShortcutKey>, onConfirmSetShortcut: () -> Unit, + onClearSelectedKeyCombination: () -> Unit, ) { - val interactionSource = remember { MutableInteractionSource() } - val isFocused by interactionSource.collectIsFocusedAsState() - val outlineColor = - if (!isFocused) MaterialTheme.colorScheme.outline - else if (shouldShowError) MaterialTheme.colorScheme.error - else MaterialTheme.colorScheme.primary val focusRequester = remember { FocusRequester() } - + val focusManager = LocalFocusManager.current LaunchedEffect(Unit) { focusRequester.requestFocus() } - ClickableShortcutSurface( - onClick = {}, - color = Color.Transparent, - shape = RoundedCornerShape(50.dp), + OutlinedInputField( modifier = Modifier.padding(all = 16.dp) .sizeIn(minWidth = 332.dp, minHeight = 56.dp) - .border(width = 2.dp, color = outlineColor, shape = RoundedCornerShape(50.dp)) + .focusRequester(focusRequester) + .focusProperties { canFocus = true } .onPreviewKeyEvent { keyEvent -> val keyEventProcessed = onShortcutKeyCombinationSelected(keyEvent) - if ( - !keyEventProcessed && - keyEvent.key == Key.Enter && - keyEvent.type == KeyEventType.KeyUp - ) { - onConfirmSetShortcut() + if (keyEventProcessed) { true - } else keyEventProcessed - } - .focusProperties { canFocus = true } // enables keyboard focus when in touch mode - .focusRequester(focusRequester), - interactionSource = interactionSource, - ) { - Row( - modifier = Modifier.padding(start = 24.dp, top = 16.dp, end = 16.dp, bottom = 16.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - if (pressedKeys.isEmpty()) { - PressKeyPrompt() + } else { + if (keyEvent.type == KeyEventType.KeyUp) { + when (keyEvent.key) { + Key.Enter -> { + onConfirmSetShortcut() + return@onPreviewKeyEvent true + } + Key.Backspace -> { + onClearSelectedKeyCombination() + return@onPreviewKeyEvent true + } + Key.DirectionDown -> { + focusManager.moveFocus(FocusDirection.Down) + return@onPreviewKeyEvent true + } + else -> return@onPreviewKeyEvent false + } + } else false + } + }, + trailingIcon = { ErrorIcon(shouldShowError) }, + isError = shouldShowError, + placeholder = { PressKeyPrompt() }, + content = + if (pressedKeys.isNotEmpty()) { + { PressedKeysTextContainer(pressedKeys) } } else { - PressedKeysTextContainer(pressedKeys) - } - Spacer(modifier = Modifier.weight(1f)) - if (shouldShowError) { - Icon( - imageVector = Icons.Default.ErrorOutline, - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.error, - ) - } - } + null + }, + ) +} + +@Composable +private fun ErrorIcon(shouldShowError: Boolean) { + if (shouldShowError) { + Icon( + imageVector = Icons.Default.ErrorOutline, + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.error, + ) } } @Composable -private fun RowScope.PressedKeysTextContainer(pressedKeys: List<ShortcutKey>) { - pressedKeys.forEachIndexed { keyIndex, key -> - if (keyIndex > 0) { - ShortcutKeySeparator() - } - if (key is ShortcutKey.Text) { - ShortcutTextKey(key) - } else if (key is ShortcutKey.Icon) { - ShortcutIconKey(key) +private fun PressedKeysTextContainer(pressedKeys: List<ShortcutKey>) { + Row( + modifier = + Modifier.semantics(mergeDescendants = true) { liveRegion = LiveRegionMode.Polite }, + verticalAlignment = Alignment.CenterVertically, + ) { + pressedKeys.forEachIndexed { keyIndex, key -> + if (keyIndex > 0) { + ShortcutKeySeparator() + } + if (key is ShortcutKey.Text) { + ShortcutTextKey(key) + } else if (key is ShortcutKey.Icon) { + ShortcutIconKey(key) + } } } } @@ -338,7 +360,7 @@ private fun ShortcutKeySeparator() { } @Composable -private fun RowScope.ShortcutIconKey(key: ShortcutKey.Icon) { +private fun ShortcutIconKey(key: ShortcutKey.Icon) { Icon( painter = when (key) { @@ -346,7 +368,7 @@ private fun RowScope.ShortcutIconKey(key: ShortcutKey.Icon) { is ShortcutKey.Icon.DrawableIcon -> rememberDrawablePainter(drawable = key.drawable) }, contentDescription = null, - modifier = Modifier.align(Alignment.CenterVertically).height(24.dp), + modifier = Modifier.height(24.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } @@ -397,6 +419,7 @@ private fun Description(text: String) { .width(316.dp) .wrapContentSize(Alignment.Center), color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, ) } @@ -464,3 +487,31 @@ private fun PlusIconContainer() { modifier = Modifier.padding(vertical = 12.dp).size(24.dp).wrapContentSize(Alignment.Center), ) } + +@Composable +private fun OutlinedInputField( + content: @Composable (() -> Unit)?, + placeholder: @Composable () -> Unit, + trailingIcon: @Composable () -> Unit, + isError: Boolean, + modifier: Modifier = Modifier, +) { + OutlinedTextField( + value = "", + onValueChange = {}, + placeholder = if (content == null) placeholder else null, + prefix = content, + singleLine = true, + modifier = modifier, + trailingIcon = trailingIcon, + colors = + OutlinedTextFieldDefaults.colors() + .copy( + focusedIndicatorColor = MaterialTheme.colorScheme.primary, + unfocusedIndicatorColor = MaterialTheme.colorScheme.outline, + errorIndicatorColor = MaterialTheme.colorScheme.error, + ), + shape = RoundedCornerShape(50.dp), + isError = isError, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt index 915a66c43a12..f4ba99c6a394 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt @@ -28,16 +28,17 @@ import com.android.systemui.keyboard.shared.model.ShortcutCustomizationRequestRe import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutCustomizationInteractor import com.android.systemui.keyboard.shortcut.shared.model.KeyCombination import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState.AddShortcutDialog import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState.DeleteShortcutDialog import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState.ResetShortcutDialog +import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.res.R import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update class ShortcutCustomizationViewModel @@ -45,26 +46,12 @@ class ShortcutCustomizationViewModel constructor( private val context: Context, private val shortcutCustomizationInteractor: ShortcutCustomizationInteractor, -) { +) : ExclusiveActivatable() { private var keyDownEventCache: KeyEvent? = null private val _shortcutCustomizationUiState = MutableStateFlow<ShortcutCustomizationUiState>(ShortcutCustomizationUiState.Inactive) - val shortcutCustomizationUiState = - shortcutCustomizationInteractor.pressedKeys - .map { keys -> - // Note that Action Key is excluded as it's already displayed on the UI - keys.filter { - it != shortcutCustomizationInteractor.getDefaultCustomShortcutModifierKey() - } - } - .combine(_shortcutCustomizationUiState) { keys, uiState -> - if (uiState is AddShortcutDialog) { - uiState.copy(pressedKeys = keys) - } else { - uiState - } - } + val shortcutCustomizationUiState = _shortcutCustomizationUiState.asStateFlow() fun onShortcutCustomizationRequested(requestInfo: ShortcutCustomizationRequestInfo) { shortcutCustomizationInteractor.onCustomizationRequested(requestInfo) @@ -92,7 +79,7 @@ constructor( fun onDialogDismissed() { _shortcutCustomizationUiState.value = ShortcutCustomizationUiState.Inactive shortcutCustomizationInteractor.onCustomizationRequested(null) - shortcutCustomizationInteractor.updateUserSelectedKeyCombination(null) + clearSelectedKeyCombination() } fun onShortcutKeyCombinationSelected(keyEvent: KeyEvent): Boolean { @@ -112,7 +99,6 @@ constructor( suspend fun onSetShortcut() { val result = shortcutCustomizationInteractor.confirmAndSetShortcutCurrentlyBeingCustomized() - _shortcutCustomizationUiState.update { uiState -> when (result) { ShortcutCustomizationRequestResult.SUCCESS -> ShortcutCustomizationUiState.Inactive @@ -158,6 +144,10 @@ constructor( } } + fun clearSelectedKeyCombination() { + shortcutCustomizationInteractor.updateUserSelectedKeyCombination(null) + } + private fun getUiStateWithErrorMessage( uiState: ShortcutCustomizationUiState, errorMessage: String, @@ -180,11 +170,41 @@ constructor( keyDownEventCache = null } + private suspend fun isSelectedKeyCombinationAvailable() = + shortcutCustomizationInteractor.isSelectedKeyCombinationAvailable() + @AssistedFactory interface Factory { fun create(): ShortcutCustomizationViewModel } + override suspend fun onActivated(): Nothing { + shortcutCustomizationInteractor.pressedKeys.collect { + val keys = filterDefaultCustomShortcutModifierKey(it) + val errorMessage = getErrorMessageForPressedKeys(keys) + + _shortcutCustomizationUiState.update { uiState -> + if (uiState is AddShortcutDialog) { + uiState.copy(pressedKeys = keys, errorMessage = errorMessage) + } else { + uiState + } + } + } + } + + private suspend fun getErrorMessageForPressedKeys(keys: List<ShortcutKey>): String { + return if (keys.isEmpty() or isSelectedKeyCombinationAvailable()) { + "" + } + else { + context.getString(R.string.shortcut_customizer_key_combination_in_use_error_message) + } + } + + private fun filterDefaultCustomShortcutModifierKey(keys: List<ShortcutKey>) = + keys.filter { it != shortcutCustomizationInteractor.getDefaultCustomShortcutModifierKey() } + companion object { private val SUPPORTED_MODIFIERS = listOf( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index 5e0768a2fd24..f37e7685f21c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -27,6 +27,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor +import com.android.systemui.keyguard.domain.interactor.WallpaperFocalAreaInteractor import com.android.systemui.keyguard.ui.binder.KeyguardBlueprintViewBinder import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder import com.android.systemui.keyguard.ui.binder.LightRevealScrimViewBinder @@ -79,6 +80,7 @@ constructor( private val keyguardClockViewModel: KeyguardClockViewModel, private val smartspaceViewModel: KeyguardSmartspaceViewModel, private val clockInteractor: KeyguardClockInteractor, + private val wallpaperFocalAreaInteractor: WallpaperFocalAreaInteractor, private val keyguardViewMediator: KeyguardViewMediator, private val deviceEntryUnlockTrackerViewBinder: Optional<DeviceEntryUnlockTrackerViewBinder>, private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager, @@ -139,6 +141,7 @@ constructor( screenOffAnimationController, shadeInteractor, clockInteractor, + wallpaperFocalAreaInteractor, keyguardClockViewModel, interactionJankMonitor, deviceEntryHapticsInteractor, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index a39982dd31e7..11477fb6cad1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.data.repository import android.graphics.Point +import android.graphics.RectF import com.android.app.tracing.coroutines.launchTraced as launch import com.android.internal.widget.LockPatternUtils import com.android.keyguard.KeyguardUpdateMonitor @@ -258,6 +259,8 @@ interface KeyguardRepository { val notificationStackAbsoluteBottom: StateFlow<Float> + val wallpaperFocalAreaBounds: StateFlow<RectF> + /** * Returns `true` if the keyguard is showing; `false` otherwise. * @@ -329,6 +332,8 @@ interface KeyguardRepository { * this value */ fun setNotificationStackAbsoluteBottom(bottom: Float) + + fun setWallpaperFocalAreaBounds(bounds: RectF) } /** Encapsulates application state for the keyguard. */ @@ -380,7 +385,6 @@ constructor( override val onCameraLaunchDetected = MutableStateFlow(CameraLaunchSourceModel()) override val panelAlpha: MutableStateFlow<Float> = MutableStateFlow(1f) - override val topClippingBounds = MutableStateFlow<Int?>(null) override val isKeyguardShowing: MutableStateFlow<Boolean> = @@ -622,6 +626,10 @@ constructor( private val _notificationStackAbsoluteBottom = MutableStateFlow(0F) override val notificationStackAbsoluteBottom = _notificationStackAbsoluteBottom.asStateFlow() + private val _wallpaperFocalAreaBounds = MutableStateFlow(RectF(0F, 0F, 0F, 0F)) + override val wallpaperFocalAreaBounds: StateFlow<RectF> = + _wallpaperFocalAreaBounds.asStateFlow() + init { val callback = object : KeyguardStateController.Callback { @@ -700,6 +708,10 @@ constructor( _notificationStackAbsoluteBottom.value = bottom } + override fun setWallpaperFocalAreaBounds(bounds: RectF) { + _wallpaperFocalAreaBounds.value = bounds + } + private fun dozeMachineStateToModel(state: DozeMachine.State): DozeStateModel { return when (state) { DozeMachine.State.UNINITIALIZED -> DozeStateModel.UNINITIALIZED diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt index 3565b612a3c9..c5d40a0dcf30 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt @@ -48,7 +48,6 @@ import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce -import com.android.app.tracing.coroutines.launchTraced as launch import kotlinx.coroutines.withContext @OptIn(FlowPreview::class) @@ -92,6 +91,7 @@ constructor( listenForHubToAlternateBouncer() listenForHubToOccluded() listenForHubToGone() + listenForHubToDreaming() } override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator { @@ -177,6 +177,24 @@ constructor( } } + private fun listenForHubToDreaming() { + if (!communalSettingsInteractor.isV2FlagEnabled()) { + return + } + + scope.launch { + keyguardInteractor.isAbleToDream + .filterRelevantKeyguardStateAnd { isAbleToDream -> isAbleToDream } + .collect { + communalSceneInteractor.changeScene( + newScene = CommunalScenes.Blank, + loggingReason = "hub to dreaming", + keyguardState = KeyguardState.DREAMING, + ) + } + } + } + private fun listenForHubToOccluded() { if (KeyguardWmStateRefactor.isEnabled) { scope.launch { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WallpaperFocalAreaInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WallpaperFocalAreaInteractor.kt new file mode 100644 index 000000000000..934afe248a36 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WallpaperFocalAreaInteractor.kt @@ -0,0 +1,176 @@ +/* + * 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.keyguard.domain.interactor + +import android.content.Context +import android.content.res.Resources +import android.graphics.RectF +import android.util.TypedValue +import com.android.app.animation.MathUtils.max +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.data.repository.KeyguardClockRepository +import com.android.systemui.keyguard.data.repository.KeyguardRepository +import com.android.systemui.res.R +import com.android.systemui.shade.data.repository.ShadeRepository +import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor +import com.android.systemui.wallpapers.data.repository.WallpaperRepository +import javax.inject.Inject +import kotlin.math.min +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn + +@SysUISingleton +class WallpaperFocalAreaInteractor +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + context: Context, + private val keyguardRepository: KeyguardRepository, + shadeRepository: ShadeRepository, + activeNotificationsInteractor: ActiveNotificationsInteractor, + keyguardClockRepository: KeyguardClockRepository, + wallpaperRepository: WallpaperRepository, +) { + // When there's notifications in splitshade, magic portrait shape effects should be left + // aligned in foldable + private val notificationInShadeWideLayout: Flow<Boolean> = + combine( + shadeRepository.isShadeLayoutWide, + activeNotificationsInteractor.areAnyNotificationsPresent, + ) { isShadeLayoutWide, areAnyNotificationsPresent: Boolean -> + when { + !isShadeLayoutWide -> false + !areAnyNotificationsPresent -> false + else -> true + } + } + + val shouldSendFocalArea = wallpaperRepository.shouldSendFocalArea + val wallpaperFocalAreaBounds: StateFlow<RectF?> = + combine( + shadeRepository.isShadeLayoutWide, + notificationInShadeWideLayout, + keyguardRepository.notificationStackAbsoluteBottom, + keyguardRepository.shortcutAbsoluteTop, + keyguardClockRepository.notificationDefaultTop, + ) { + isShadeLayoutWide, + notificationInShadeWideLayout, + notificationStackAbsoluteBottom, + shortcutAbsoluteTop, + notificationDefaultTop -> + // Wallpaper will be zoomed in with config_wallpaperMaxScale in lockscreen + // so we need to give a bounds taking this scale in consideration + val wallpaperZoomedInScale = getSystemWallpaperMaximumScale(context) + val screenBounds = + RectF( + 0F, + 0F, + context.resources.displayMetrics.widthPixels.toFloat(), + context.resources.displayMetrics.heightPixels.toFloat(), + ) + val scaledBounds = + RectF( + screenBounds.centerX() - screenBounds.width() / 2F / wallpaperZoomedInScale, + screenBounds.centerY() - + screenBounds.height() / 2F / wallpaperZoomedInScale, + screenBounds.centerX() + screenBounds.width() / 2F / wallpaperZoomedInScale, + screenBounds.centerY() + screenBounds.height() / 2F / wallpaperZoomedInScale, + ) + val maxFocalAreaWidth = + TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + FOCAL_AREA_MAX_WIDTH_DP.toFloat(), + context.resources.displayMetrics, + ) + val (left, right) = + // tablet landscape + if (context.resources.getBoolean(R.bool.center_align_magic_portrait_shape)) { + Pair( + scaledBounds.centerX() - maxFocalAreaWidth / 2F, + scaledBounds.centerX() + maxFocalAreaWidth / 2F, + ) + // unfold foldable landscape + } else if (isShadeLayoutWide) { + if (notificationInShadeWideLayout) { + Pair(scaledBounds.left, scaledBounds.centerX()) + } else { + Pair(scaledBounds.centerX(), scaledBounds.right) + } + // handheld / portrait + } else { + val focalAreaWidth = min(scaledBounds.width(), maxFocalAreaWidth) + Pair( + scaledBounds.centerX() - focalAreaWidth / 2F, + scaledBounds.centerX() + focalAreaWidth / 2F, + ) + } + val scaledBottomMargin = + (context.resources.displayMetrics.heightPixels - shortcutAbsoluteTop) / + wallpaperZoomedInScale + val top = + // tablet landscape + if (context.resources.getBoolean(R.bool.center_align_magic_portrait_shape)) { + // no strict constraints for top, use bottom margin to make it symmetric + // vertically + scaledBounds.top + scaledBottomMargin + } + // unfold foldable landscape + else if (isShadeLayoutWide) { + // For all landscape, we should use bottom of smartspace to constrain + scaledBounds.top + notificationDefaultTop / wallpaperZoomedInScale + // handheld / portrait + } else { + scaledBounds.top + + max(notificationDefaultTop, notificationStackAbsoluteBottom) / + wallpaperZoomedInScale + } + val bottom = scaledBounds.bottom - scaledBottomMargin + RectF(left, top, right, bottom) + } + .stateIn( + applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = null, + ) + + fun setWallpaperFocalAreaBounds(bounds: RectF) { + keyguardRepository.setWallpaperFocalAreaBounds(bounds) + } + + companion object { + fun getSystemWallpaperMaximumScale(context: Context): Float { + return context.resources.getFloat( + Resources.getSystem() + .getIdentifier( + /* name= */ "config_wallpaperMaxScale", + /* defType= */ "dimen", + /* defPackage= */ "android", + ) + ) + } + + // A max width for magic portrait shape effects bounds, to avoid it going too large + // in large screen portrait mode + const val FOCAL_AREA_MAX_WIDTH_DP = 500 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt index 184f30237e8d..6a354821f628 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt @@ -24,7 +24,6 @@ import com.android.systemui.Flags.transitionRaceCondition import com.android.systemui.dagger.SysUISingleton import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository -import com.android.systemui.keyguard.shared.model.BiometricUnlockMode import com.android.systemui.keyguard.shared.model.Edge import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.KeyguardState.Companion.deviceIsAsleepInState @@ -331,17 +330,9 @@ constructor( * clock/smartspace/notif icons are visible. */ val aodVisibility: Flow<Boolean> = - combine( - keyguardInteractor.isDozing, - keyguardInteractor.isAodAvailable, - keyguardInteractor.biometricUnlockState, - ) { isDozing, isAodAvailable, biometricUnlockState -> - // AOD is visible if we're dozing, unless we are wake and unlocking (where we go - // directly from AOD to unlocked while dozing). - isDozing && - isAodAvailable && - !BiometricUnlockMode.isWakeAndUnlock(biometricUnlockState.mode) - } + transitionInteractor + .transitionValue(KeyguardState.AOD) + .map { it == 1f } .distinctUntilChanged() companion object { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt index 92b49ed6156c..21c9b0b82b2d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt @@ -23,6 +23,7 @@ import android.widget.TextView import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.systemui.Flags import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.res.R @@ -73,7 +74,6 @@ object KeyguardIndicationAreaBinder { disposables += view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { - launch("$TAG#viewModel.indicationAreaTranslationX") { viewModel.indicationAreaTranslationX.collect { translationX -> view.translationX = translationX @@ -119,6 +119,9 @@ object KeyguardIndicationAreaBinder { launch("$TAG#viewModel.configurationChange") { viewModel.configurationChange.collect { configurationBasedDimensions.value = loadFromResources(view) + if (Flags.indicationTextA11yFix()) { + indicationController.onConfigurationChanged() + } } } @@ -140,7 +143,7 @@ object KeyguardIndicationAreaBinder { view.resources.getDimensionPixelOffset(R.dimen.keyguard_indication_area_padding), indicationTextSizePx = view.resources.getDimensionPixelSize( - com.android.internal.R.dimen.text_size_small_material, + com.android.internal.R.dimen.text_size_small_material ), ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index 6d270b219c81..d8bd4452f2a6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -54,6 +54,7 @@ import com.android.systemui.customization.R as customR import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor import com.android.systemui.keyguard.KeyguardViewMediator import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor +import com.android.systemui.keyguard.domain.interactor.WallpaperFocalAreaInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters @@ -87,6 +88,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update @@ -105,6 +107,7 @@ object KeyguardRootViewBinder { screenOffAnimationController: ScreenOffAnimationController, shadeInteractor: ShadeInteractor, clockInteractor: KeyguardClockInteractor, + wallpaperFocalAreaInteractor: WallpaperFocalAreaInteractor, clockViewModel: KeyguardClockViewModel, interactionJankMonitor: InteractionJankMonitor?, deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor?, @@ -309,12 +312,15 @@ object KeyguardRootViewBinder { .setTag(clockId) jankMonitor.begin(builder) } + TransitionState.CANCELED -> jankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD) + TransitionState.FINISHED -> { keyguardViewMediator?.maybeHandlePendingLock() jankMonitor.end(CUJ_SCREEN_OFF_SHOW_AOD) } + TransitionState.RUNNING -> Unit } } @@ -378,6 +384,21 @@ object KeyguardRootViewBinder { } disposables += + view.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + if (viewModel.shouldSendFocalArea.value) { + launch { + wallpaperFocalAreaInteractor.wallpaperFocalAreaBounds + .filterNotNull() + .collect { + wallpaperFocalAreaInteractor.setWallpaperFocalAreaBounds(it) + } + } + } + } + } + + disposables += view.onLayoutChanged( OnLayoutChange( viewModel, @@ -523,6 +544,7 @@ object KeyguardRootViewBinder { View.INVISIBLE } } + else -> { if (isVisible.value) { CrossFadeHelper.fadeIn(this, animatorListener) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index a2107871a585..7605bdd3d7b5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -55,6 +55,7 @@ import com.android.systemui.customization.R as customR import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.domain.interactor.WallpaperFocalAreaInteractor import com.android.systemui.keyguard.shared.model.ClockSizeSetting import com.android.systemui.keyguard.ui.binder.KeyguardPreviewClockViewBinder import com.android.systemui.keyguard.ui.binder.KeyguardPreviewSmartspaceViewBinder @@ -117,6 +118,7 @@ constructor( private val secureSettings: SecureSettings, private val defaultShortcutsSection: DefaultShortcutsSection, private val keyguardQuickAffordanceViewBinder: KeyguardQuickAffordanceViewBinder, + private val wallpaperFocalAreaInteractor: WallpaperFocalAreaInteractor, ) { val hostToken: IBinder? = bundle.getBinder(KEY_HOST_TOKEN) private val width: Int = bundle.getInt(KEY_VIEW_WIDTH) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt index 3eb8522e0338..542fb9b46bef 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt @@ -23,10 +23,4 @@ data class BlurConfig(val minBlurRadiusPx: Float, val maxBlurRadiusPx: Float) { // No-op config that will be used by dagger of other SysUI variants which don't blur the // background surface. @Inject constructor() : this(0.0f, 0.0f) - - companion object { - // Blur the shade much lesser than the background surface so that the surface is - // distinguishable from the background. - @JvmStatic fun Float.maxBlurRadiusToNotificationPanelBlurRadius(): Float = this / 3.0f - } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/InWindowLauncherUnlockAnimationManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/InWindowLauncherUnlockAnimationManager.kt index eb005f226cf1..454ba9af5745 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/InWindowLauncherUnlockAnimationManager.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/InWindowLauncherUnlockAnimationManager.kt @@ -76,9 +76,8 @@ constructor( private var manualUnlockAmount: Float? = null /** - * Called from [OverviewProxyService] to provide us with the launcher unlock animation - * controller, which can be used to start and update the unlock animation in the launcher - * process. + * Called from Launcher to provide us with the launcher unlock animation controller, which can + * be used to start and update the unlock animation in the launcher process. */ override fun setLauncherUnlockController( activityClass: String, @@ -117,7 +116,7 @@ constructor( launcher.prepareForUnlock( false, Rect(), - 0 + 0, ) // TODO(b/293894758): Add smartspace animation support. } } @@ -134,14 +133,14 @@ constructor( Log.e( TAG, "Called prepareForUnlock(), but not playUnlockAnimation(). " + - "Failing-safe by calling setUnlockAmount(1f)" + "Failing-safe by calling setUnlockAmount(1f)", ) setUnlockAmount(1f, forceIfAnimating = true) } else if (manualUnlockSetButNotFullyVisible) { Log.e( TAG, "Unlock has ended, but manual unlock amount != 1f. " + - "Failing-safe by calling setUnlockAmount(1f)" + "Failing-safe by calling setUnlockAmount(1f)", ) setUnlockAmount(1f, forceIfAnimating = true) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt index 733d7d71061e..a6d15b96547d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt @@ -24,7 +24,6 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCE import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow import com.android.systemui.keyguard.ui.transitions.BlurConfig -import com.android.systemui.keyguard.ui.transitions.BlurConfig.Companion.maxBlurRadiusToNotificationPanelBlurRadius import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition import com.android.systemui.scene.shared.flag.SceneContainerFlag @@ -89,9 +88,7 @@ constructor( shadeDependentFlows.transitionFlow( flowWhenShadeIsNotExpanded = emptyFlow(), flowWhenShadeIsExpanded = - transitionAnimation.immediatelyTransitionTo( - blurConfig.maxBlurRadiusPx.maxBlurRadiusToNotificationPanelBlurRadius() - ), + transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx), ) } else { emptyFlow<Float>() @@ -103,7 +100,11 @@ constructor( override val windowBlurRadius: Flow<Float> = shadeDependentFlows.transitionFlow( flowWhenShadeIsExpanded = - transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx), + if (Flags.notificationShadeBlur()) { + transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx) + } else { + emptyFlow() + }, flowWhenShadeIsNotExpanded = transitionAnimation.sharedFlow( duration = FromAlternateBouncerTransitionInteractor.TO_PRIMARY_BOUNCER_DURATION, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModel.kt index e68e465ed55a..ba03c48c65e9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModel.kt @@ -16,10 +16,50 @@ package com.android.systemui.keyguard.ui.viewmodel +import androidx.compose.runtime.getValue +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.lifecycle.Hydrator import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor -import javax.inject.Inject -import kotlinx.coroutines.flow.StateFlow +import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.flowOf -class KeyguardMediaViewModel @Inject constructor(mediaCarouselInteractor: MediaCarouselInteractor) { - val isMediaVisible: StateFlow<Boolean> = mediaCarouselInteractor.hasActiveMediaOrRecommendation +class KeyguardMediaViewModel +@AssistedInject +constructor( + mediaCarouselInteractor: MediaCarouselInteractor, + keyguardInteractor: KeyguardInteractor, +) : ExclusiveActivatable() { + + private val hydrator = Hydrator("KeyguardMediaViewModel.hydrator") + /** + * Whether media carousel is visible on lockscreen. Media may be presented on lockscreen but + * still hidden on certain surfaces like AOD + */ + val isMediaVisible: Boolean by + hydrator.hydratedStateOf( + traceName = "isMediaVisible", + source = + keyguardInteractor.isDozing.flatMapLatestConflated { isDozing -> + if (isDozing) { + flowOf(false) + } else { + mediaCarouselInteractor.hasActiveMediaOrRecommendation + } + }, + initialValue = + !keyguardInteractor.isDozing.value && + mediaCarouselInteractor.hasActiveMediaOrRecommendation.value, + ) + + override suspend fun onActivated(): Nothing { + hydrator.activate() + } + + @AssistedFactory + interface Factory { + fun create(): KeyguardMediaViewModel + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt index e51e05b8ab61..aa4293a201ac 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt @@ -28,6 +28,7 @@ import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.domain.interactor.PulseExpansionInteractor +import com.android.systemui.keyguard.domain.interactor.WallpaperFocalAreaInteractor import com.android.systemui.keyguard.shared.model.Edge import com.android.systemui.keyguard.shared.model.KeyguardState.AOD import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING @@ -133,6 +134,7 @@ constructor( private val screenOffAnimationController: ScreenOffAnimationController, private val aodBurnInViewModel: AodBurnInViewModel, private val shadeInteractor: ShadeInteractor, + wallpaperFocalAreaInteractor: WallpaperFocalAreaInteractor, ) { val burnInLayerVisibility: Flow<Int> = keyguardTransitionInteractor.startedKeyguardTransitionStep @@ -362,6 +364,8 @@ constructor( initialValue = AnimatedValue.NotAnimating(false), ) + val shouldSendFocalArea = wallpaperFocalAreaInteractor.shouldSendFocalArea + fun onNotificationContainerBoundsChanged(top: Float, bottom: Float, animate: Boolean = false) { keyguardInteractor.setNotificationContainerBounds( NotificationContainerBounds(top = top, bottom = bottom, isAnimated = animate) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt index 44c4c8723dcb..e25e4e5af425 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt @@ -24,7 +24,6 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow import com.android.systemui.keyguard.ui.transitions.BlurConfig -import com.android.systemui.keyguard.ui.transitions.BlurConfig.Companion.maxBlurRadiusToNotificationPanelBlurRadius import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition import com.android.systemui.scene.shared.flag.SceneContainerFlag @@ -88,9 +87,7 @@ constructor( shadeDependentFlows.transitionFlow( flowWhenShadeIsNotExpanded = emptyFlow(), flowWhenShadeIsExpanded = - transitionAnimation.immediatelyTransitionTo( - blurConfig.maxBlurRadiusPx.maxBlurRadiusToNotificationPanelBlurRadius() - ), + transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx), ) } else { emptyFlow() @@ -110,7 +107,11 @@ constructor( override val windowBlurRadius: Flow<Float> = shadeDependentFlows.transitionFlow( flowWhenShadeIsExpanded = - transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx), + if (Flags.notificationShadeBlur()) { + transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx) + } else { + emptyFlow() + }, flowWhenShadeIsNotExpanded = transitionAnimation.sharedFlow( duration = FromLockscreenTransitionInteractor.TO_PRIMARY_BOUNCER_DURATION, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt index 4d3e27265cea..3c126aa23fef 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.ui.viewmodel +import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor import com.android.systemui.keyguard.shared.model.Edge @@ -26,12 +27,16 @@ import com.android.systemui.keyguard.ui.transitions.BlurConfig import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow @SysUISingleton class OccludedToPrimaryBouncerTransitionViewModel @Inject -constructor(blurConfig: BlurConfig, animationFlow: KeyguardTransitionAnimationFlow) : - PrimaryBouncerTransition { +constructor( + shadeDependentFlows: ShadeDependentFlows, + blurConfig: BlurConfig, + animationFlow: KeyguardTransitionAnimationFlow, +) : PrimaryBouncerTransition { private val transitionAnimation = animationFlow .setup( @@ -41,8 +46,21 @@ constructor(blurConfig: BlurConfig, animationFlow: KeyguardTransitionAnimationFl .setupWithoutSceneContainer(edge = Edge.create(OCCLUDED, PRIMARY_BOUNCER)) override val windowBlurRadius: Flow<Float> = - transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx) + shadeDependentFlows.transitionFlow( + flowWhenShadeIsExpanded = + if (Flags.notificationShadeBlur()) { + transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx) + } else { + emptyFlow() + }, + flowWhenShadeIsNotExpanded = + transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx), + ) override val notificationBlurRadius: Flow<Float> = - transitionAnimation.immediatelyTransitionTo(0.0f) + shadeDependentFlows.transitionFlow( + flowWhenShadeIsExpanded = + transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx), + flowWhenShadeIsNotExpanded = emptyFlow(), + ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt index c53a408a88e1..9b01803f1fd5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.ui.viewmodel import android.util.MathUtils import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE +import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor import com.android.systemui.keyguard.shared.model.Edge @@ -32,6 +33,7 @@ import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow /** * Breaks down PRIMARY BOUNCER->LOCKSCREEN transition into discrete steps for corresponding views to @@ -78,7 +80,11 @@ constructor( override val windowBlurRadius: Flow<Float> = shadeDependentFlows.transitionFlow( flowWhenShadeIsExpanded = - transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx), + if (Flags.notificationShadeBlur()) { + transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx) + } else { + emptyFlow() + }, flowWhenShadeIsNotExpanded = transitionAnimation.sharedFlow( duration = FromPrimaryBouncerTransitionInteractor.TO_LOCKSCREEN_DURATION, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt index fe1708efea2f..0f0e7b6faa66 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.ui.viewmodel +import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor import com.android.systemui.keyguard.shared.model.Edge @@ -26,12 +27,16 @@ import com.android.systemui.keyguard.ui.transitions.BlurConfig import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow @SysUISingleton class PrimaryBouncerToOccludedTransitionViewModel @Inject -constructor(private val blurConfig: BlurConfig, animationFlow: KeyguardTransitionAnimationFlow) : - PrimaryBouncerTransition { +constructor( + shadeDependentFlows: ShadeDependentFlows, + blurConfig: BlurConfig, + animationFlow: KeyguardTransitionAnimationFlow, +) : PrimaryBouncerTransition { private val transitionAnimation = animationFlow .setup( @@ -41,7 +46,16 @@ constructor(private val blurConfig: BlurConfig, animationFlow: KeyguardTransitio .setupWithoutSceneContainer(edge = Edge.create(PRIMARY_BOUNCER, OCCLUDED)) override val windowBlurRadius: Flow<Float> = - transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx) + shadeDependentFlows.transitionFlow( + flowWhenShadeIsExpanded = + if (Flags.notificationShadeBlur()) { + transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx) + } else { + emptyFlow() + }, + flowWhenShadeIsNotExpanded = + transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx), + ) override val notificationBlurRadius: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0.0f) diff --git a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt index 425e674ec804..fb69b793d975 100644 --- a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt @@ -21,6 +21,7 @@ import com.android.systemui.dump.DumpManager import com.android.systemui.log.LogBufferHelper.Companion.adjustMaxSize import com.android.systemui.log.LogcatEchoTracker import com.android.systemui.util.time.SystemClock +import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject @SysUISingleton @@ -31,7 +32,7 @@ constructor( private val systemClock: SystemClock, private val logcatEchoTracker: LogcatEchoTracker, ) { - private val existingBuffers = mutableMapOf<String, TableLogBuffer>() + private val existingBuffers = ConcurrentHashMap<String, TableLogBuffer>() /** * Creates a new [TableLogBuffer]. This method should only be called from static contexts, where @@ -42,17 +43,9 @@ constructor( * @param maxSize the buffer max size. See [adjustMaxSize] * @return a new [TableLogBuffer] registered with [DumpManager] */ - fun create( - name: String, - maxSize: Int, - ): TableLogBuffer { + fun create(name: String, maxSize: Int): TableLogBuffer { val tableBuffer = - TableLogBuffer( - adjustMaxSize(maxSize), - name, - systemClock, - logcatEchoTracker, - ) + TableLogBuffer(adjustMaxSize(maxSize), name, systemClock, logcatEchoTracker) dumpManager.registerTableLogBuffer(name, tableBuffer) return tableBuffer } @@ -66,13 +59,12 @@ constructor( * * @return a [TableLogBuffer] suitable for reuse */ - fun getOrCreate( - name: String, - maxSize: Int, - ): TableLogBuffer = - existingBuffers.getOrElse(name) { - val buffer = create(name, maxSize) - existingBuffers[name] = buffer - buffer + fun getOrCreate(name: String, maxSize: Int): TableLogBuffer = + synchronized(existingBuffers) { + existingBuffers.getOrElse(name) { + val buffer = create(name, maxSize) + existingBuffers[name] = buffer + buffer + } } } diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt b/packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt new file mode 100644 index 000000000000..ece97bd27df7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt @@ -0,0 +1,137 @@ +/* + * 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.lowlightclock + +import android.annotation.IntDef +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.util.Log +import com.android.systemui.Dumpable +import com.android.systemui.lowlightclock.dagger.LowLightModule.LIGHT_SENSOR +import com.android.systemui.util.sensors.AsyncSensorManager +import java.io.PrintWriter +import java.util.Optional +import javax.inject.Inject +import javax.inject.Named + +/** + * Monitors ambient light signals, applies a debouncing algorithm, and produces the current ambient + * light mode. + * + * @property algorithm the debounce algorithm which transforms light sensor events into an ambient + * light mode. + * @property sensorManager the sensor manager used to register sensor event updates. + */ +class AmbientLightModeMonitor +@Inject +constructor( + private val algorithm: Optional<DebounceAlgorithm>, + private val sensorManager: AsyncSensorManager, + @Named(LIGHT_SENSOR) private val lightSensor: Optional<Sensor>, +) : Dumpable { + companion object { + private const val TAG = "AmbientLightModeMonitor" + private val DEBUG = Log.isLoggable(TAG, Log.DEBUG) + + const val AMBIENT_LIGHT_MODE_LIGHT = 0 + const val AMBIENT_LIGHT_MODE_DARK = 1 + const val AMBIENT_LIGHT_MODE_UNDECIDED = 2 + } + + // Represents all ambient light modes. + @Retention(AnnotationRetention.SOURCE) + @IntDef(AMBIENT_LIGHT_MODE_LIGHT, AMBIENT_LIGHT_MODE_DARK, AMBIENT_LIGHT_MODE_UNDECIDED) + annotation class AmbientLightMode + + /** + * Start monitoring the current ambient light mode. + * + * @param callback callback that gets triggered when the ambient light mode changes. + */ + fun start(callback: Callback) { + if (DEBUG) Log.d(TAG, "start monitoring ambient light mode") + + if (lightSensor.isEmpty) { + if (DEBUG) Log.w(TAG, "light sensor not available") + return + } + + if (algorithm.isEmpty) { + if (DEBUG) Log.w(TAG, "debounce algorithm not available") + return + } + + algorithm.get().start(callback) + sensorManager.registerListener( + mSensorEventListener, + lightSensor.get(), + SensorManager.SENSOR_DELAY_NORMAL, + ) + } + + /** Stop monitoring the current ambient light mode. */ + fun stop() { + if (DEBUG) Log.d(TAG, "stop monitoring ambient light mode") + + if (algorithm.isPresent) { + algorithm.get().stop() + } + sensorManager.unregisterListener(mSensorEventListener) + } + + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.println() + pw.println("Ambient light mode monitor:") + pw.println(" lightSensor=$lightSensor") + pw.println() + } + + private val mSensorEventListener: SensorEventListener = + object : SensorEventListener { + override fun onSensorChanged(event: SensorEvent) { + if (event.values.isEmpty()) { + if (DEBUG) Log.w(TAG, "SensorEvent doesn't have any value") + return + } + + if (algorithm.isPresent) { + algorithm.get().onUpdateLightSensorEvent(event.values[0]) + } + } + + override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) { + // Do nothing. + } + } + + /** Interface of the ambient light mode callback, which gets triggered when the mode changes. */ + interface Callback { + fun onChange(@AmbientLightMode mode: Int) + } + + /** Interface of the algorithm that transforms light sensor events to an ambient light mode. */ + interface DebounceAlgorithm { + // Setting Callback to nullable so mockito can verify without throwing NullPointerException. + fun start(callback: Callback?) + + fun stop() + + fun onUpdateLightSensorEvent(value: Float) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/ChargingStatusProvider.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/ChargingStatusProvider.java new file mode 100644 index 000000000000..8cc399b0a22b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/ChargingStatusProvider.java @@ -0,0 +1,258 @@ +/* + * 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.lowlightclock; + +import android.content.Context; +import android.content.res.Resources; +import android.os.BatteryManager; +import android.os.RemoteException; +import android.text.format.Formatter; +import android.util.Log; + +import com.android.internal.app.IBatteryStats; +import com.android.internal.util.Preconditions; +import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.keyguard.KeyguardUpdateMonitorCallback; +import com.android.settingslib.fuelgauge.BatteryStatus; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.res.R; + +import java.text.NumberFormat; + +import javax.inject.Inject; + +/** + * Provides charging status as a string to a registered callback such that it can be displayed to + * the user (e.g. on the low-light clock). + * TODO(b/223681352): Make this code shareable with {@link KeyguardIndicationController}. + */ +public class ChargingStatusProvider { + private static final String TAG = "ChargingStatusProvider"; + + private final Resources mResources; + private final Context mContext; + private final IBatteryStats mBatteryInfo; + private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; + private final BatteryState mBatteryState = new BatteryState(); + // This callback is registered with KeyguardUpdateMonitor, which only keeps weak references to + // its callbacks. Therefore, an explicit reference needs to be kept here to avoid the + // callback being GC'd. + private ChargingStatusCallback mChargingStatusCallback; + + private Callback mCallback; + + @Inject + public ChargingStatusProvider( + Context context, + @Main Resources resources, + IBatteryStats iBatteryStats, + KeyguardUpdateMonitor keyguardUpdateMonitor) { + mContext = context; + mResources = resources; + mBatteryInfo = iBatteryStats; + mKeyguardUpdateMonitor = keyguardUpdateMonitor; + } + + /** + * Start using the {@link ChargingStatusProvider}. + * @param callback A callback to be called when the charging status changes. + */ + public void startUsing(Callback callback) { + Preconditions.checkState( + mCallback == null, "ChargingStatusProvider already started!"); + mCallback = callback; + mChargingStatusCallback = new ChargingStatusCallback(); + mKeyguardUpdateMonitor.registerCallback(mChargingStatusCallback); + reportStatusToCallback(); + } + + /** + * Stop using the {@link ChargingStatusProvider}. + */ + public void stopUsing() { + mCallback = null; + + if (mChargingStatusCallback != null) { + mKeyguardUpdateMonitor.removeCallback(mChargingStatusCallback); + mChargingStatusCallback = null; + } + } + + private String computeChargingString() { + if (!mBatteryState.isValid()) { + return null; + } + + int chargingId; + + if (mBatteryState.isBatteryDefender()) { + return mResources.getString( + R.string.keyguard_plugged_in_charging_limited, + mBatteryState.getBatteryLevelAsPercentage()); + } else if (mBatteryState.isPowerCharged()) { + return mResources.getString(R.string.keyguard_charged); + } + + final long chargingTimeRemaining = mBatteryState.getChargingTimeRemaining(mBatteryInfo); + final boolean hasChargingTime = chargingTimeRemaining > 0; + if (mBatteryState.isPowerPluggedInWired()) { + switch (mBatteryState.getChargingSpeed(mContext)) { + case BatteryStatus.CHARGING_FAST: + chargingId = hasChargingTime + ? R.string.keyguard_indication_charging_time_fast + : R.string.keyguard_plugged_in_charging_fast; + break; + case BatteryStatus.CHARGING_SLOWLY: + chargingId = hasChargingTime + ? R.string.keyguard_indication_charging_time_slowly + : R.string.keyguard_plugged_in_charging_slowly; + break; + default: + chargingId = hasChargingTime + ? R.string.keyguard_indication_charging_time + : R.string.keyguard_plugged_in; + break; + } + } else if (mBatteryState.isPowerPluggedInWireless()) { + chargingId = hasChargingTime + ? R.string.keyguard_indication_charging_time_wireless + : R.string.keyguard_plugged_in_wireless; + } else if (mBatteryState.isPowerPluggedInDocked()) { + chargingId = hasChargingTime + ? R.string.keyguard_indication_charging_time_dock + : R.string.keyguard_plugged_in_dock; + } else { + chargingId = hasChargingTime + ? R.string.keyguard_indication_charging_time + : R.string.keyguard_plugged_in; + } + + final String percentage = mBatteryState.getBatteryLevelAsPercentage(); + if (hasChargingTime) { + final String chargingTimeFormatted = + Formatter.formatShortElapsedTimeRoundingUpToMinutes( + mContext, chargingTimeRemaining); + return mResources.getString(chargingId, chargingTimeFormatted, + percentage); + } else { + return mResources.getString(chargingId, percentage); + } + } + + private void reportStatusToCallback() { + if (mCallback != null) { + final boolean shouldShowStatus = + mBatteryState.isPowerPluggedIn() || mBatteryState.isBatteryDefenderEnabled(); + mCallback.onChargingStatusChanged(shouldShowStatus, computeChargingString()); + } + } + + private class ChargingStatusCallback extends KeyguardUpdateMonitorCallback { + @Override + public void onRefreshBatteryInfo(BatteryStatus status) { + mBatteryState.setBatteryStatus(status); + reportStatusToCallback(); + } + } + + /*** + * A callback to be called when the charging status changes. + */ + public interface Callback { + /*** + * Called when the charging status changes. + * @param shouldShowStatus Whether or not to show a charging status message. + * @param statusMessage A charging status message. + */ + void onChargingStatusChanged(boolean shouldShowStatus, String statusMessage); + } + + /*** + * A wrapper around {@link BatteryStatus} for fetching various properties of the current + * battery and charging state. + */ + private static class BatteryState { + private BatteryStatus mBatteryStatus; + + public void setBatteryStatus(BatteryStatus batteryStatus) { + mBatteryStatus = batteryStatus; + } + + public boolean isValid() { + return mBatteryStatus != null; + } + + public long getChargingTimeRemaining(IBatteryStats batteryInfo) { + try { + return isPowerPluggedIn() ? batteryInfo.computeChargeTimeRemaining() : -1; + } catch (RemoteException e) { + Log.e(TAG, "Error calling IBatteryStats: ", e); + return -1; + } + } + + public boolean isBatteryDefenderEnabled() { + return isValid() && mBatteryStatus.isPluggedIn() && isBatteryDefender(); + } + + public boolean isBatteryDefender() { + return isValid() && mBatteryStatus.isBatteryDefender(); + } + + public int getBatteryLevel() { + return isValid() ? mBatteryStatus.level : 0; + } + + public int getChargingSpeed(Context context) { + return isValid() ? mBatteryStatus.getChargingSpeed(context) : 0; + } + + public boolean isPowerCharged() { + return isValid() && mBatteryStatus.isCharged(); + } + + public boolean isPowerPluggedIn() { + return isValid() && mBatteryStatus.isPluggedIn() && isChargingOrFull(); + } + + public boolean isPowerPluggedInWired() { + return isValid() + && mBatteryStatus.isPluggedInWired() + && isChargingOrFull(); + } + + public boolean isPowerPluggedInWireless() { + return isValid() + && mBatteryStatus.isPluggedInWireless() + && isChargingOrFull(); + } + + public boolean isPowerPluggedInDocked() { + return isValid() && mBatteryStatus.isPluggedInDock() && isChargingOrFull(); + } + + private boolean isChargingOrFull() { + return isValid() + && (mBatteryStatus.status == BatteryManager.BATTERY_STATUS_CHARGING + || mBatteryStatus.isCharged()); + } + + private String getBatteryLevelAsPercentage() { + return NumberFormat.getPercentInstance().format(getBatteryLevel() / 100f); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/DirectBootCondition.kt b/packages/SystemUI/src/com/android/systemui/lowlightclock/DirectBootCondition.kt new file mode 100644 index 000000000000..4c1da0198498 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/DirectBootCondition.kt @@ -0,0 +1,60 @@ +/* + * 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.lowlightclock + +import android.content.Intent +import android.content.IntentFilter +import android.os.UserManager +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.shared.condition.Condition +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.cancellable +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +class DirectBootCondition +@Inject +constructor( + broadcastDispatcher: BroadcastDispatcher, + private val userManager: UserManager, + @Application private val coroutineScope: CoroutineScope, +) : Condition(coroutineScope) { + private var job: Job? = null + private val directBootFlow = + broadcastDispatcher + .broadcastFlow(IntentFilter(Intent.ACTION_USER_UNLOCKED)) + .map { !userManager.isUserUnlocked } + .cancellable() + .distinctUntilChanged() + + override fun start() { + job = coroutineScope.launch { directBootFlow.collect { updateCondition(it) } } + updateCondition(!userManager.isUserUnlocked) + } + + override fun stop() { + job?.cancel() + } + + override fun getStartStrategy(): Int { + return START_EAGERLY + } +} diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/ForceLowLightCondition.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/ForceLowLightCondition.java new file mode 100644 index 000000000000..7f21d0707f63 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/ForceLowLightCondition.java @@ -0,0 +1,137 @@ +/* + * 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.lowlightclock; + +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.android.systemui.dagger.qualifiers.Application; +import com.android.systemui.shared.condition.Condition; +import com.android.systemui.statusbar.commandline.Command; +import com.android.systemui.statusbar.commandline.CommandRegistry; + +import kotlinx.coroutines.CoroutineScope; + +import java.io.PrintWriter; +import java.util.List; + +import javax.inject.Inject; + +/** + * This condition registers for and fulfills cmd shell commands to force a device into or out of + * low-light conditions. + */ +public class ForceLowLightCondition extends Condition { + /** + * Command root + */ + public static final String COMMAND_ROOT = "low-light"; + /** + * Command for forcing device into low light. + */ + public static final String COMMAND_ENABLE_LOW_LIGHT = "enable"; + + /** + * Command for preventing a device from entering low light. + */ + public static final String COMMAND_DISABLE_LOW_LIGHT = "disable"; + + /** + * Command for clearing previously forced low-light conditions. + */ + public static final String COMMAND_CLEAR_LOW_LIGHT = "clear"; + + private static final String TAG = "ForceLowLightCondition"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + /** + * Default Constructor. + * + * @param commandRegistry command registry to register commands with. + */ + @Inject + public ForceLowLightCondition( + @Application CoroutineScope scope, + CommandRegistry commandRegistry + ) { + super(scope, null, true); + + if (DEBUG) { + Log.d(TAG, "registering commands"); + } + commandRegistry.registerCommand(COMMAND_ROOT, () -> new Command() { + @Override + public void execute(@NonNull PrintWriter pw, @NonNull List<String> args) { + if (args.size() != 1) { + pw.println("no command specified"); + help(pw); + return; + } + + final String cmd = args.get(0); + + if (TextUtils.equals(cmd, COMMAND_ENABLE_LOW_LIGHT)) { + logAndPrint(pw, "forcing low light"); + updateCondition(true); + } else if (TextUtils.equals(cmd, COMMAND_DISABLE_LOW_LIGHT)) { + logAndPrint(pw, "forcing to not enter low light"); + updateCondition(false); + } else if (TextUtils.equals(cmd, COMMAND_CLEAR_LOW_LIGHT)) { + logAndPrint(pw, "clearing any forced low light"); + clearCondition(); + } else { + pw.println("invalid command"); + help(pw); + } + } + + @Override + public void help(@NonNull PrintWriter pw) { + pw.println("Usage: adb shell cmd statusbar low-light <cmd>"); + pw.println("Supported commands:"); + pw.println(" - enable"); + pw.println(" forces device into low-light"); + pw.println(" - disable"); + pw.println(" forces device to not enter low-light"); + pw.println(" - clear"); + pw.println(" clears any previously forced state"); + } + + private void logAndPrint(PrintWriter pw, String message) { + pw.println(message); + if (DEBUG) { + Log.d(TAG, message); + } + } + }); + } + + @Override + protected void start() { + } + + @Override + protected void stop() { + } + + @Override + protected int getStartStrategy() { + return START_EAGERLY; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightClockAnimationProvider.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightClockAnimationProvider.java new file mode 100644 index 000000000000..6de599803a57 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightClockAnimationProvider.java @@ -0,0 +1,127 @@ +/* + * 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.lowlightclock; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.view.View; +import android.view.animation.Interpolator; + +import com.android.app.animation.Interpolators; +import com.android.dream.lowlight.util.TruncatedInterpolator; +import com.android.systemui.lowlightclock.dagger.LowLightModule; +import com.android.systemui.statusbar.CrossFadeHelper; + +import javax.inject.Inject; +import javax.inject.Named; + +/*** + * A class that provides the animations used by the low-light clock. + * + * The entry and exit animations are opposites, with the only difference being a delay before the + * text fades in on entry. + */ +public class LowLightClockAnimationProvider { + private final int mYTranslationAnimationInStartOffset; + private final long mYTranslationAnimationInDurationMillis; + private final long mAlphaAnimationInStartDelayMillis; + private final long mAlphaAnimationDurationMillis; + + /** + * Custom interpolator used for the translate out animation, which uses an emphasized easing + * like the translate in animation, but is scaled to match the length of the alpha animation. + */ + private final Interpolator mTranslationOutInterpolator; + + @Inject + public LowLightClockAnimationProvider( + @Named(LowLightModule.Y_TRANSLATION_ANIMATION_OFFSET) + int yTranslationAnimationInStartOffset, + @Named(LowLightModule.Y_TRANSLATION_ANIMATION_DURATION_MILLIS) + long yTranslationAnimationInDurationMillis, + @Named(LowLightModule.ALPHA_ANIMATION_IN_START_DELAY_MILLIS) + long alphaAnimationInStartDelayMillis, + @Named(LowLightModule.ALPHA_ANIMATION_DURATION_MILLIS) + long alphaAnimationDurationMillis) { + mYTranslationAnimationInStartOffset = yTranslationAnimationInStartOffset; + mYTranslationAnimationInDurationMillis = yTranslationAnimationInDurationMillis; + mAlphaAnimationInStartDelayMillis = alphaAnimationInStartDelayMillis; + mAlphaAnimationDurationMillis = alphaAnimationDurationMillis; + + mTranslationOutInterpolator = new TruncatedInterpolator(Interpolators.EMPHASIZED, + /*originalDuration=*/ mYTranslationAnimationInDurationMillis, + /*newDuration=*/ mAlphaAnimationDurationMillis); + } + + /*** + * Provides an animation for when the given views become visible. + * @param views Any number of views to animate in together. + */ + public Animator provideAnimationIn(View... views) { + final AnimatorSet animatorSet = new AnimatorSet(); + + for (View view : views) { + if (view == null) continue; + // Set the alpha to 0 to start because the alpha animation has a start delay. + CrossFadeHelper.fadeOut(view, 0f, false); + + final Animator alphaAnimator = + ObjectAnimator.ofFloat(view, View.ALPHA, 1f); + alphaAnimator.setStartDelay(mAlphaAnimationInStartDelayMillis); + alphaAnimator.setDuration(mAlphaAnimationDurationMillis); + alphaAnimator.setInterpolator(Interpolators.LINEAR); + + final Animator positionAnimator = ObjectAnimator + .ofFloat(view, View.TRANSLATION_Y, mYTranslationAnimationInStartOffset, 0f); + positionAnimator.setDuration(mYTranslationAnimationInDurationMillis); + positionAnimator.setInterpolator(Interpolators.EMPHASIZED); + + // The position animator must be started first since the alpha animator has a start + // delay. + animatorSet.playTogether(positionAnimator, alphaAnimator); + } + + return animatorSet; + } + + /*** + * Provides an animation for when the given views are going out of view. + * @param views Any number of views to animate out. + */ + public Animator provideAnimationOut(View... views) { + final AnimatorSet animatorSet = new AnimatorSet(); + + for (View view : views) { + if (view == null) continue; + final Animator alphaAnimator = + ObjectAnimator.ofFloat(view, View.ALPHA, 0f); + alphaAnimator.setDuration(mAlphaAnimationDurationMillis); + alphaAnimator.setInterpolator(Interpolators.LINEAR); + + final Animator positionAnimator = ObjectAnimator + .ofFloat(view, View.TRANSLATION_Y, mYTranslationAnimationInStartOffset); + // Use the same duration as the alpha animation plus our custom interpolator. + positionAnimator.setDuration(mAlphaAnimationDurationMillis); + positionAnimator.setInterpolator(mTranslationOutInterpolator); + animatorSet.playTogether(alphaAnimator, positionAnimator); + } + + return animatorSet; + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightCondition.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightCondition.java new file mode 100644 index 000000000000..e91be5028777 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightCondition.java @@ -0,0 +1,77 @@ +/* + * 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.lowlightclock; + +import com.android.internal.logging.UiEventLogger; +import com.android.systemui.dagger.qualifiers.Application; +import com.android.systemui.shared.condition.Condition; + +import kotlinx.coroutines.CoroutineScope; + +import javax.inject.Inject; + +/** + * Condition for monitoring when the device enters and exits lowlight mode. + */ +public class LowLightCondition extends Condition { + private final AmbientLightModeMonitor mAmbientLightModeMonitor; + private final UiEventLogger mUiEventLogger; + + @Inject + public LowLightCondition(@Application CoroutineScope scope, + AmbientLightModeMonitor ambientLightModeMonitor, + UiEventLogger uiEventLogger) { + super(scope); + mAmbientLightModeMonitor = ambientLightModeMonitor; + mUiEventLogger = uiEventLogger; + } + + @Override + protected void start() { + mAmbientLightModeMonitor.start(this::onLowLightChanged); + } + + @Override + protected void stop() { + mAmbientLightModeMonitor.stop(); + + // Reset condition met to false. + updateCondition(false); + } + + @Override + protected int getStartStrategy() { + // As this condition keeps the lowlight sensor active, it should only run when needed. + return START_WHEN_NEEDED; + } + + private void onLowLightChanged(int lowLightMode) { + if (lowLightMode == AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_UNDECIDED) { + // Ignore undecided mode changes. + return; + } + + final boolean isLowLight = lowLightMode == AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK; + if (isLowLight == isConditionMet()) { + // No change in condition, don't do anything. + return; + } + mUiEventLogger.log(isLowLight ? LowLightDockEvent.AMBIENT_LIGHT_TO_DARK + : LowLightDockEvent.AMBIENT_LIGHT_TO_LIGHT); + updateCondition(isLowLight); + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeUserActionsViewModelKosmos.kt b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightDisplayController.kt index 6345c4076412..9a9d813b18c5 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeUserActionsViewModelKosmos.kt +++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightDisplayController.kt @@ -14,11 +14,10 @@ * limitations under the License. */ -package com.android.systemui.shade.ui.viewmodel +package com.android.systemui.lowlightclock -import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.Kosmos.Fixture -import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeUserActionsViewModel +interface LowLightDisplayController { + fun isDisplayBrightnessModeSupported(): Boolean -val Kosmos.notificationsShadeUserActionsViewModel: - NotificationsShadeUserActionsViewModel by Fixture { NotificationsShadeUserActionsViewModel() } + fun setDisplayBrightnessModeEnabled(enabled: Boolean) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightDockEvent.kt index d6845b1ff7e3..b99aeb6eeacc 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt +++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightDockEvent.kt @@ -14,20 +14,18 @@ * limitations under the License. */ -package com.android.systemui.volume.dialog.sliders.ui +package com.android.systemui.lowlightclock -import com.android.systemui.haptics.msdl.msdlPlayer -import com.android.systemui.haptics.vibratorHelper -import com.android.systemui.kosmos.Kosmos -import com.android.systemui.util.time.systemClock -import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSliderInputEventsViewModel +import com.android.internal.logging.UiEvent +import com.android.internal.logging.UiEventLogger -val Kosmos.volumeDialogSliderHapticsViewBinder by - Kosmos.Fixture { - VolumeDialogSliderHapticsViewBinder( - volumeDialogSliderInputEventsViewModel, - vibratorHelper, - msdlPlayer, - systemClock, - ) +enum class LowLightDockEvent(private val id: Int) : UiEventLogger.UiEventEnum { + @UiEvent(doc = "Ambient light changed from light to dark") AMBIENT_LIGHT_TO_DARK(999), + @UiEvent(doc = "The low light mode has started") LOW_LIGHT_STARTED(1000), + @UiEvent(doc = "Ambient light changed from dark to light") AMBIENT_LIGHT_TO_LIGHT(1001), + @UiEvent(doc = "The low light mode has stopped") LOW_LIGHT_STOPPED(1002); + + override fun getId(): Int { + return id } +} diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightLogger.kt b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightLogger.kt new file mode 100644 index 000000000000..11d75215edf5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightLogger.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.lowlightclock + +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.LogLevel +import com.android.systemui.lowlightclock.dagger.LowLightLog +import javax.inject.Inject + +/** Logs to a {@link LogBuffer} anything related to low-light features. */ +class LowLightLogger @Inject constructor(@LowLightLog private val buffer: LogBuffer) { + /** Logs a debug message to the buffer. */ + fun d(tag: String, message: String) = buffer.log(tag, LogLevel.DEBUG, message) +} diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java new file mode 100644 index 000000000000..912ace7675d5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java @@ -0,0 +1,133 @@ +/* + * 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.lowlightclock; + +import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT; +import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR; +import static com.android.systemui.dreams.dagger.DreamModule.LOW_LIGHT_DREAM_SERVICE; +import static com.android.systemui.keyguard.ScreenLifecycle.SCREEN_ON; +import static com.android.systemui.lowlightclock.dagger.LowLightModule.LOW_LIGHT_PRECONDITIONS; + +import android.content.ComponentName; +import android.content.pm.PackageManager; + +import androidx.annotation.Nullable; + +import com.android.dream.lowlight.LowLightDreamManager; +import com.android.systemui.dagger.qualifiers.SystemUser; +import com.android.systemui.keyguard.ScreenLifecycle; +import com.android.systemui.shared.condition.Condition; +import com.android.systemui.shared.condition.Monitor; +import com.android.systemui.util.condition.ConditionalCoreStartable; + +import dagger.Lazy; + +import java.util.Set; + +import javax.inject.Inject; +import javax.inject.Named; + +/** + * Tracks environment (low-light or not) in order to correctly show or hide a low-light clock while + * dreaming. + */ +public class LowLightMonitor extends ConditionalCoreStartable implements Monitor.Callback, + ScreenLifecycle.Observer { + private static final String TAG = "LowLightMonitor"; + + private final Lazy<LowLightDreamManager> mLowLightDreamManager; + private final Monitor mConditionsMonitor; + private final Lazy<Set<Condition>> mLowLightConditions; + private Monitor.Subscription.Token mSubscriptionToken; + private ScreenLifecycle mScreenLifecycle; + private final LowLightLogger mLogger; + + private final ComponentName mLowLightDreamService; + + private final PackageManager mPackageManager; + + @Inject + public LowLightMonitor(Lazy<LowLightDreamManager> lowLightDreamManager, + @SystemUser Monitor conditionsMonitor, + @Named(LOW_LIGHT_PRECONDITIONS) Lazy<Set<Condition>> lowLightConditions, + ScreenLifecycle screenLifecycle, + LowLightLogger lowLightLogger, + @Nullable @Named(LOW_LIGHT_DREAM_SERVICE) ComponentName lowLightDreamService, + PackageManager packageManager) { + super(conditionsMonitor); + mLowLightDreamManager = lowLightDreamManager; + mConditionsMonitor = conditionsMonitor; + mLowLightConditions = lowLightConditions; + mScreenLifecycle = screenLifecycle; + mLogger = lowLightLogger; + mLowLightDreamService = lowLightDreamService; + mPackageManager = packageManager; + } + + @Override + public void onConditionsChanged(boolean allConditionsMet) { + mLogger.d(TAG, "Low light enabled: " + allConditionsMet); + + mLowLightDreamManager.get().setAmbientLightMode(allConditionsMet + ? AMBIENT_LIGHT_MODE_LOW_LIGHT : AMBIENT_LIGHT_MODE_REGULAR); + } + + @Override + public void onScreenTurnedOn() { + if (mSubscriptionToken == null) { + mLogger.d(TAG, "Screen turned on. Subscribing to low light conditions."); + + mSubscriptionToken = mConditionsMonitor.addSubscription( + new Monitor.Subscription.Builder(this) + .addConditions(mLowLightConditions.get()) + .build()); + } + } + + + @Override + public void onScreenTurnedOff() { + if (mSubscriptionToken != null) { + mLogger.d(TAG, "Screen turned off. Removing subscription to low light conditions."); + + mConditionsMonitor.removeSubscription(mSubscriptionToken); + mSubscriptionToken = null; + } + } + + @Override + protected void onStart() { + if (mLowLightDreamService != null) { + // Note that the dream service is disabled by default. This prevents the dream from + // appearing in settings on devices that don't have it explicitly excluded (done in + // the settings overlay). Therefore, the component is enabled if it is to be used + // here. + mPackageManager.setComponentEnabledSetting( + mLowLightDreamService, + PackageManager.COMPONENT_ENABLED_STATE_ENABLED, + PackageManager.DONT_KILL_APP + ); + } else { + // If there is no low light dream service, do not observe conditions. + return; + } + + mScreenLifecycle.addObserver(this); + if (mScreenLifecycle.getScreenState() == SCREEN_ON) { + onScreenTurnedOn(); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/ScreenSaverEnabledCondition.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/ScreenSaverEnabledCondition.java new file mode 100644 index 000000000000..fd6ce1762a28 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/ScreenSaverEnabledCondition.java @@ -0,0 +1,87 @@ +/* + * 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.lowlightclock; + +import android.content.res.Resources; +import android.database.ContentObserver; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.Log; + +import com.android.systemui.dagger.qualifiers.Application; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.shared.condition.Condition; +import com.android.systemui.util.settings.SecureSettings; + +import kotlinx.coroutines.CoroutineScope; + +import javax.inject.Inject; + +/** + * Condition for monitoring if the screensaver setting is enabled. + */ +public class ScreenSaverEnabledCondition extends Condition { + private static final String TAG = ScreenSaverEnabledCondition.class.getSimpleName(); + + private final boolean mScreenSaverEnabledByDefaultConfig; + private final SecureSettings mSecureSettings; + + private final ContentObserver mScreenSaverSettingObserver = new ContentObserver(null) { + @Override + public void onChange(boolean selfChange) { + updateScreenSaverEnabledSetting(); + } + }; + + @Inject + public ScreenSaverEnabledCondition(@Application CoroutineScope scope, @Main Resources resources, + SecureSettings secureSettings) { + super(scope); + mScreenSaverEnabledByDefaultConfig = resources.getBoolean( + com.android.internal.R.bool.config_dreamsEnabledByDefault); + mSecureSettings = secureSettings; + } + + @Override + protected void start() { + mSecureSettings.registerContentObserverForUserSync( + Settings.Secure.SCREENSAVER_ENABLED, + mScreenSaverSettingObserver, UserHandle.USER_CURRENT); + updateScreenSaverEnabledSetting(); + } + + @Override + protected void stop() { + mSecureSettings.unregisterContentObserverSync(mScreenSaverSettingObserver); + } + + @Override + protected int getStartStrategy() { + return START_EAGERLY; + } + + private void updateScreenSaverEnabledSetting() { + final boolean enabled = mSecureSettings.getIntForUser( + Settings.Secure.SCREENSAVER_ENABLED, + mScreenSaverEnabledByDefaultConfig ? 1 : 0, + UserHandle.USER_CURRENT) != 0; + if (!enabled) { + Log.i(TAG, "Disabling low-light clock because screen saver has been disabled"); + } + updateCondition(enabled); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightLog.kt b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightLog.kt new file mode 100644 index 000000000000..0819664c921c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightLog.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.lowlightclock.dagger + +import javax.inject.Qualifier + +/** A [com.android.systemui.log.LogBuffer] for logging related to low light features. */ +@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class LowLightLog diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java new file mode 100644 index 000000000000..c08be51c0699 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java @@ -0,0 +1,150 @@ +/* + * 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.lowlightclock.dagger; + +import android.content.res.Resources; +import android.hardware.Sensor; + +import com.android.dream.lowlight.dagger.LowLightDreamModule; +import com.android.systemui.CoreStartable; +import com.android.systemui.communal.DeviceInactiveCondition; +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.log.LogBuffer; +import com.android.systemui.log.LogBufferFactory; +import com.android.systemui.lowlightclock.AmbientLightModeMonitor; +import com.android.systemui.lowlightclock.DirectBootCondition; +import com.android.systemui.lowlightclock.ForceLowLightCondition; +import com.android.systemui.lowlightclock.LowLightCondition; +import com.android.systemui.lowlightclock.LowLightDisplayController; +import com.android.systemui.lowlightclock.LowLightMonitor; +import com.android.systemui.lowlightclock.ScreenSaverEnabledCondition; +import com.android.systemui.res.R; +import com.android.systemui.shared.condition.Condition; + +import dagger.Binds; +import dagger.BindsOptionalOf; +import dagger.Module; +import dagger.Provides; +import dagger.multibindings.ClassKey; +import dagger.multibindings.IntoMap; +import dagger.multibindings.IntoSet; + +import javax.inject.Named; + +@Module(includes = LowLightDreamModule.class) +public abstract class LowLightModule { + public static final String Y_TRANSLATION_ANIMATION_OFFSET = + "y_translation_animation_offset"; + public static final String Y_TRANSLATION_ANIMATION_DURATION_MILLIS = + "y_translation_animation_duration_millis"; + public static final String ALPHA_ANIMATION_IN_START_DELAY_MILLIS = + "alpha_animation_in_start_delay_millis"; + public static final String ALPHA_ANIMATION_DURATION_MILLIS = + "alpha_animation_duration_millis"; + public static final String LOW_LIGHT_PRECONDITIONS = "low_light_preconditions"; + public static final String LIGHT_SENSOR = "low_light_monitor_light_sensor"; + + + /** + * Provides a {@link LogBuffer} for logs related to low-light features. + */ + @Provides + @SysUISingleton + @LowLightLog + public static LogBuffer provideLowLightLogBuffer(LogBufferFactory factory) { + return factory.create("LowLightLog", 250); + } + + @Binds + @IntoSet + @Named(LOW_LIGHT_PRECONDITIONS) + abstract Condition bindScreenSaverEnabledCondition(ScreenSaverEnabledCondition condition); + + @Provides + @IntoSet + @Named(com.android.systemui.lowlightclock.dagger.LowLightModule.LOW_LIGHT_PRECONDITIONS) + static Condition provideLowLightCondition(LowLightCondition lowLightCondition, + DirectBootCondition directBootCondition) { + // Start lowlight if we are either in lowlight or in direct boot. The ordering of the + // conditions matters here since we don't want to start the lowlight condition if + // we are in direct boot mode. + return directBootCondition.or(lowLightCondition); + } + + @Binds + @IntoSet + @Named(LOW_LIGHT_PRECONDITIONS) + abstract Condition bindForceLowLightCondition(ForceLowLightCondition condition); + + @Binds + @IntoSet + @Named(LOW_LIGHT_PRECONDITIONS) + abstract Condition bindDeviceInactiveCondition(DeviceInactiveCondition condition); + + @BindsOptionalOf + abstract LowLightDisplayController bindsLowLightDisplayController(); + + @BindsOptionalOf + @Named(LIGHT_SENSOR) + abstract Sensor bindsLightSensor(); + + @BindsOptionalOf + abstract AmbientLightModeMonitor.DebounceAlgorithm bindsDebounceAlgorithm(); + + /** + * + */ + @Provides + @Named(Y_TRANSLATION_ANIMATION_OFFSET) + static int providesAnimationInOffset(@Main Resources resources) { + return resources.getDimensionPixelOffset( + R.dimen.low_light_clock_translate_animation_offset); + } + + /** + * + */ + @Provides + @Named(Y_TRANSLATION_ANIMATION_DURATION_MILLIS) + static long providesAnimationDurationMillis(@Main Resources resources) { + return resources.getInteger(R.integer.low_light_clock_translate_animation_duration_ms); + } + + /** + * + */ + @Provides + @Named(ALPHA_ANIMATION_IN_START_DELAY_MILLIS) + static long providesAlphaAnimationInStartDelayMillis(@Main Resources resources) { + return resources.getInteger(R.integer.low_light_clock_alpha_animation_in_start_delay_ms); + } + + /** + * + */ + @Provides + @Named(ALPHA_ANIMATION_DURATION_MILLIS) + static long providesAlphaAnimationDurationMillis(@Main Resources resources) { + return resources.getInteger(R.integer.low_light_clock_alpha_animation_duration_ms); + } + /** Inject into LowLightMonitor. */ + @Binds + @IntoMap + @ClassKey(LowLightMonitor.class) + abstract CoreStartable bindLowLightMonitor(LowLightMonitor lowLightMonitor); +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt index f1f299aac2b4..52749c54b9ba 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt @@ -1268,13 +1268,21 @@ class LegacyMediaDataManagerImpl( } private fun getResumeMediaAction(action: Runnable): MediaAction { + val iconId = + if (Flags.mediaControlsUiUpdate()) { + R.drawable.ic_media_play_button + } else { + R.drawable.ic_media_play + } return MediaAction( - Icon.createWithResource(context, R.drawable.ic_media_play) - .setTint(themeText) - .loadDrawable(context), + Icon.createWithResource(context, iconId).setTint(themeText).loadDrawable(context), action, context.getString(R.string.controls_media_resume), - context.getDrawable(R.drawable.ic_media_play_container), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_play_button_container) + } else { + context.getDrawable(R.drawable.ic_media_play_container) + }, ) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt index a176e0c1c2a6..8bb7303a8386 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt @@ -43,7 +43,9 @@ import android.support.v4.media.MediaMetadataCompat import android.text.TextUtils import android.util.Log import androidx.media.utils.MediaConstants +import com.android.app.tracing.coroutines.asyncTraced as async import com.android.app.tracing.coroutines.traceCoroutine +import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background @@ -67,7 +69,6 @@ import kotlin.coroutines.coroutineContext import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job -import com.android.app.tracing.coroutines.asyncTraced as async import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.ensureActive @@ -511,13 +512,21 @@ constructor( sbn.notification.extras.containsKey(Notification.EXTRA_MEDIA_REMOTE_DEVICE) private fun getResumeMediaAction(action: Runnable): MediaAction { + val iconId = + if (Flags.mediaControlsUiUpdate()) { + R.drawable.ic_media_play_button + } else { + R.drawable.ic_media_play + } return MediaAction( - Icon.createWithResource(context, R.drawable.ic_media_play) - .setTint(themeText) - .loadDrawable(context), + Icon.createWithResource(context, iconId).setTint(themeText).loadDrawable(context), action, context.getString(R.string.controls_media_resume), - context.getDrawable(R.drawable.ic_media_play_container), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_play_button_container) + } else { + context.getDrawable(R.drawable.ic_media_play_container) + }, ) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt index a524db4437a5..587a678c6ac0 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt @@ -1197,13 +1197,21 @@ class MediaDataProcessor( } private fun getResumeMediaAction(action: Runnable): MediaAction { + val iconId = + if (Flags.mediaControlsUiUpdate()) { + R.drawable.ic_media_play_button + } else { + R.drawable.ic_media_play + } return MediaAction( - Icon.createWithResource(context, R.drawable.ic_media_play) - .setTint(themeText) - .loadDrawable(context), + Icon.createWithResource(context, iconId).setTint(themeText).loadDrawable(context), action, context.getString(R.string.controls_media_resume), - context.getDrawable(R.drawable.ic_media_play_container), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_play_button_container) + } else { + context.getDrawable(R.drawable.ic_media_play_container) + }, ) } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java index 1b0aeb47e36a..53f3b3a7a59d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java @@ -28,7 +28,6 @@ import android.graphics.drawable.Drawable; import android.util.Log; import android.view.View; import android.view.ViewGroup; -import android.view.accessibility.AccessibilityNodeInfo; import android.widget.CheckBox; import android.widget.TextView; @@ -485,13 +484,6 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { == MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE ? R.string.accessibility_bluetooth_name : R.string.accessibility_cast_name, device.getName())); - view.setAccessibilityDelegate(new View.AccessibilityDelegate() { - public void onInitializeAccessibilityNodeInfo(View host, - AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(host, info); - host.setOnClickListener(null); - } - }); } } diff --git a/packages/SystemUI/src/com/android/systemui/model/SysUiState.java b/packages/SystemUI/src/com/android/systemui/model/SysUiState.java index 67fe0e981b09..1a5e605c96f8 100644 --- a/packages/SystemUI/src/com/android/systemui/model/SysUiState.java +++ b/packages/SystemUI/src/com/android/systemui/model/SysUiState.java @@ -79,7 +79,8 @@ public class SysUiState implements Dumpable { /** Methods to this call can be chained together before calling {@link #commitUpdate(int)}. */ public SysUiState setFlag(@SystemUiStateFlags long flag, boolean enabled) { - final Boolean overrideOrNull = mSceneContainerPlugin.flagValueOverride(flag); + final Boolean overrideOrNull = mSceneContainerPlugin != null + ? mSceneContainerPlugin.flagValueOverride(flag) : null; if (overrideOrNull != null && enabled != overrideOrNull) { if (DEBUG) { Log.d(TAG, "setFlag for flag " + flag + " and value " + enabled + " overridden to " diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java index 173a964cc5d3..1807847e3f3c 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java @@ -77,7 +77,7 @@ import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler; -import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.recents.LauncherProxyService; import com.android.systemui.settings.DisplayTracker; import com.android.systemui.settings.UserTracker; import com.android.systemui.shared.Flags; @@ -115,7 +115,7 @@ public final class NavBarHelper implements AccessibilityButtonModeObserver.ModeChangedListener, AccessibilityButtonTargetsObserver.TargetsChangedListener, AccessibilityGestureTargetsObserver.TargetsChangedListener, - OverviewProxyService.OverviewProxyListener, NavigationModeController.ModeChangedListener, + LauncherProxyService.LauncherProxyListener, NavigationModeController.ModeChangedListener, Dumpable, CommandQueue.Callbacks, ConfigurationController.ConfigurationListener { private static final String TAG = NavBarHelper.class.getSimpleName(); @@ -199,7 +199,7 @@ public final class NavBarHelper implements AccessibilityButtonTargetsObserver accessibilityButtonTargetsObserver, AccessibilityGestureTargetsObserver accessibilityGestureTargetsObserver, SystemActions systemActions, - OverviewProxyService overviewProxyService, + LauncherProxyService launcherProxyService, Lazy<AssistManager> assistManagerLazy, Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy, KeyguardStateController keyguardStateController, @@ -240,7 +240,7 @@ public final class NavBarHelper implements mNavBarMode = navigationModeController.addListener(this); mCommandQueue.addCallback(this); configurationController.addCallback(this); - overviewProxyService.addCallback(this); + launcherProxyService.addCallback(this); dumpManager.registerDumpable(this); } @@ -536,10 +536,12 @@ public final class NavBarHelper implements } /** - * @return Whether the IME is shown on top of the screen given the {@code vis} flag of - * {@link InputMethodService} and the keyguard states. + * Checks whether the IME is visible on top of the screen, based on the given IME window + * visibility flags, and the current keyguard state. + * + * @param vis the IME window visibility. */ - public boolean isImeShown(@ImeWindowVisibility int vis) { + public boolean isImeVisible(@ImeWindowVisibility int vis) { View shadeWindowView = mNotificationShadeWindowController.getWindowRootView(); boolean isKeyguardShowing = mKeyguardStateController.isShowing(); boolean imeVisibleOnShade = shadeWindowView != null && shadeWindowView.isAttachedToWindow() diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java index 645bd0b4b441..ebda3765cf90 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java @@ -52,7 +52,7 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.model.SysUiState; import com.android.systemui.navigationbar.views.NavigationBar; import com.android.systemui.navigationbar.views.NavigationBarView; -import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.recents.LauncherProxyService; import com.android.systemui.settings.DisplayTracker; import com.android.systemui.shared.statusbar.phone.BarTransitions.TransitionMode; import com.android.systemui.shared.system.TaskStackChangeListeners; @@ -115,7 +115,7 @@ public class NavigationBarControllerImpl implements @Inject public NavigationBarControllerImpl(Context context, - OverviewProxyService overviewProxyService, + LauncherProxyService launcherProxyService, NavigationModeController navigationModeController, SysUiState sysUiFlagsContainer, CommandQueue commandQueue, @@ -145,7 +145,7 @@ public class NavigationBarControllerImpl implements mNavMode = navigationModeController.addListener(this); mNavBarHelper = navBarHelper; mTaskbarDelegate = taskbarDelegate; - mTaskbarDelegate.setDependencies(commandQueue, overviewProxyService, + mTaskbarDelegate.setDependencies(commandQueue, launcherProxyService, navBarHelper, navigationModeController, sysUiFlagsContainer, dumpManager, autoHideControllerStore.forDisplay(mContext.getDisplayId()), lightBarController, pipOptional, backAnimation.orElse(null), diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java index 05d8bff2ceb6..9d8943052b38 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java @@ -17,8 +17,9 @@ package com.android.systemui.navigationbar; import static android.app.ActivityManager.LOCK_TASK_MODE_PINNED; -import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN; -import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN; +import static android.app.StatusBarManager.NAVBAR_BACK_DISMISS_IME; +import static android.app.StatusBarManager.NAVBAR_IME_SWITCHER_BUTTON_VISIBLE; +import static android.app.StatusBarManager.NAVBAR_IME_VISIBLE; import static android.app.StatusBarManager.WINDOW_STATE_SHOWING; import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; @@ -29,14 +30,16 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BACK_DISABLED; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BACK_DISMISS_IME; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED; -import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING; -import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_VISIBLE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING; import android.app.StatusBarManager; +import android.app.StatusBarManager.NavbarFlags; import android.app.StatusBarManager.WindowVisibleState; import android.content.Context; import android.graphics.Rect; @@ -67,7 +70,7 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.model.SysUiState; import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler; import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.recents.LauncherProxyService; import com.android.systemui.settings.DisplayTracker; import com.android.systemui.shared.recents.utilities.Utilities; import com.android.systemui.shared.statusbar.phone.BarTransitions; @@ -93,7 +96,7 @@ import javax.inject.Inject; /** */ @SysUISingleton public class TaskbarDelegate implements CommandQueue.Callbacks, - OverviewProxyService.OverviewProxyListener, NavigationModeController.ModeChangedListener, + LauncherProxyService.LauncherProxyListener, NavigationModeController.ModeChangedListener, Dumpable { private static final String TAG = TaskbarDelegate.class.getSimpleName(); @@ -101,7 +104,7 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, private final LightBarTransitionsController.Factory mLightBarTransitionsControllerFactory; private boolean mInitialized; private CommandQueue mCommandQueue; - private OverviewProxyService mOverviewProxyService; + private LauncherProxyService mLauncherProxyService; private NavBarHelper mNavBarHelper; private NavigationModeController mNavigationModeController; private SysUiState mSysUiState; @@ -111,7 +114,8 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, private TaskStackChangeListeners mTaskStackChangeListeners; private Optional<Pip> mPipOptional; private int mDefaultDisplayId; - private int mNavigationIconHints; + @NavbarFlags + private int mNavbarFlags; private final NavBarHelper.NavbarTaskbarStateUpdater mNavbarTaskbarStateUpdater = new NavBarHelper.NavbarTaskbarStateUpdater() { @Override @@ -206,7 +210,7 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, } public void setDependencies(CommandQueue commandQueue, - OverviewProxyService overviewProxyService, + LauncherProxyService launcherProxyService, NavBarHelper navBarHelper, NavigationModeController navigationModeController, SysUiState sysUiState, DumpManager dumpManager, @@ -218,7 +222,7 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, DisplayTracker displayTracker) { // TODO: adding this in the ctor results in a dagger dependency cycle :( mCommandQueue = commandQueue; - mOverviewProxyService = overviewProxyService; + mLauncherProxyService = launcherProxyService; mNavBarHelper = navBarHelper; mNavigationModeController = navigationModeController; mSysUiState = sysUiState; @@ -236,12 +240,12 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, @Override public void onDisplayReady(int displayId) { CommandQueue.Callbacks.super.onDisplayReady(displayId); - if (mOverviewProxyService.getProxy() == null) { + if (mLauncherProxyService.getProxy() == null) { return; } try { - mOverviewProxyService.getProxy().onDisplayReady(displayId); + mLauncherProxyService.getProxy().onDisplayReady(displayId); } catch (RemoteException e) { Log.e(TAG, "onDisplayReady() failed", e); } @@ -250,17 +254,31 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, @Override public void onDisplayRemoved(int displayId) { CommandQueue.Callbacks.super.onDisplayRemoved(displayId); - if (mOverviewProxyService.getProxy() == null) { + if (mLauncherProxyService.getProxy() == null) { return; } try { - mOverviewProxyService.getProxy().onDisplayRemoved(displayId); + mLauncherProxyService.getProxy().onDisplayRemoved(displayId); } catch (RemoteException e) { Log.e(TAG, "onDisplayRemoved() failed", e); } } + @Override + public void onDisplayRemoveSystemDecorations(int displayId) { + CommandQueue.Callbacks.super.onDisplayRemoveSystemDecorations(displayId); + if (mLauncherProxyService.getProxy() == null) { + return; + } + + try { + mLauncherProxyService.getProxy().onDisplayRemoveSystemDecorations(displayId); + } catch (RemoteException e) { + Log.e(TAG, "onDisplaySystemDecorationsRemoved() failed", e); + } + } + // Separated into a method to keep setDependencies() clean/readable. private LightBarTransitionsController createLightBarTransitionsController() { @@ -269,7 +287,7 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, @Override public void applyDarkIntensity(float darkIntensity) { mBgHandler.post(() -> { - mOverviewProxyService.onNavButtonsDarkIntensityChanged(darkIntensity); + mLauncherProxyService.onNavButtonsDarkIntensityChanged(darkIntensity); }); } @@ -291,7 +309,7 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, mDefaultDisplayId = displayId; parseCurrentSysuiState(); mCommandQueue.addCallback(this); - mOverviewProxyService.addCallback(this); + mLauncherProxyService.addCallback(this); onNavigationModeChanged(mNavigationModeController.addListener(this)); mNavBarHelper.registerNavTaskStateUpdater(mNavbarTaskbarStateUpdater); // Initialize component callback @@ -316,7 +334,7 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, return; } mCommandQueue.removeCallback(this); - mOverviewProxyService.removeCallback(this); + mLauncherProxyService.removeCallback(this); mNavigationModeController.removeListener(this); mNavBarHelper.removeNavTaskStateUpdater(mNavbarTaskbarStateUpdater); mScreenPinningNotify = null; @@ -360,10 +378,12 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, mSysUiState.setFlag(SYSUI_STATE_A11Y_BUTTON_CLICKABLE, clickable) .setFlag(SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE, longClickable) - .setFlag(SYSUI_STATE_IME_SHOWING, - (mNavigationIconHints & NAVIGATION_HINT_IME_SHOWN) != 0) - .setFlag(SYSUI_STATE_IME_SWITCHER_SHOWING, - (mNavigationIconHints & NAVIGATION_HINT_IME_SWITCHER_SHOWN) != 0) + .setFlag(SYSUI_STATE_IME_VISIBLE, + (mNavbarFlags & NAVBAR_IME_VISIBLE) != 0) + .setFlag(SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE, + (mNavbarFlags & NAVBAR_IME_SWITCHER_BUTTON_VISIBLE) != 0) + .setFlag(SYSUI_STATE_BACK_DISMISS_IME, + (mNavbarFlags & NAVBAR_BACK_DISMISS_IME) != 0) .setFlag(SYSUI_STATE_OVERVIEW_DISABLED, (mDisabledFlags & View.STATUS_BAR_DISABLE_RECENT) != 0) .setFlag(SYSUI_STATE_HOME_DISABLED, @@ -381,43 +401,43 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, } void onTransitionModeUpdated(int barMode, boolean checkBarModes) { - if (mOverviewProxyService.getProxy() == null) { + if (mLauncherProxyService.getProxy() == null) { return; } try { - mOverviewProxyService.getProxy().onTransitionModeUpdated(barMode, checkBarModes); + mLauncherProxyService.getProxy().onTransitionModeUpdated(barMode, checkBarModes); } catch (RemoteException e) { Log.e(TAG, "onTransitionModeUpdated() failed, barMode: " + barMode, e); } } void checkNavBarModes(int displayId) { - if (mOverviewProxyService.getProxy() == null) { + if (mLauncherProxyService.getProxy() == null) { return; } try { - mOverviewProxyService.getProxy().checkNavBarModes(displayId); + mLauncherProxyService.getProxy().checkNavBarModes(displayId); } catch (RemoteException e) { Log.e(TAG, "checkNavBarModes() failed", e); } } void finishBarAnimations(int displayId) { - if (mOverviewProxyService.getProxy() == null) { + if (mLauncherProxyService.getProxy() == null) { return; } try { - mOverviewProxyService.getProxy().finishBarAnimations(displayId); + mLauncherProxyService.getProxy().finishBarAnimations(displayId); } catch (RemoteException e) { Log.e(TAG, "finishBarAnimations() failed", e); } } void touchAutoDim(int displayId) { - if (mOverviewProxyService.getProxy() == null) { + if (mLauncherProxyService.getProxy() == null) { return; } @@ -425,31 +445,31 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, int state = mStatusBarStateController.getState(); boolean shouldReset = state != StatusBarState.KEYGUARD && state != StatusBarState.SHADE_LOCKED; - mOverviewProxyService.getProxy().touchAutoDim(displayId, shouldReset); + mLauncherProxyService.getProxy().touchAutoDim(displayId, shouldReset); } catch (RemoteException e) { Log.e(TAG, "touchAutoDim() failed", e); } } void transitionTo(int displayId, @BarTransitions.TransitionMode int barMode, boolean animate) { - if (mOverviewProxyService.getProxy() == null) { + if (mLauncherProxyService.getProxy() == null) { return; } try { - mOverviewProxyService.getProxy().transitionTo(displayId, barMode, animate); + mLauncherProxyService.getProxy().transitionTo(displayId, barMode, animate); } catch (RemoteException e) { Log.e(TAG, "transitionTo() failed, barMode: " + barMode, e); } } private void updateAssistantAvailability(boolean assistantAvailable, boolean longPressHomeEnabled) { - if (mOverviewProxyService.getProxy() == null) { + if (mLauncherProxyService.getProxy() == null) { return; } try { - mOverviewProxyService.getProxy().onAssistantAvailable(assistantAvailable, + mLauncherProxyService.getProxy().onAssistantAvailable(assistantAvailable, longPressHomeEnabled); } catch (RemoteException e) { Log.e(TAG, "onAssistantAvailable() failed, available: " + assistantAvailable, e); @@ -457,24 +477,24 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, } private void updateWallpaperVisible(int displayId, boolean visible) { - if (mOverviewProxyService.getProxy() == null) { + if (mLauncherProxyService.getProxy() == null) { return; } try { - mOverviewProxyService.getProxy().updateWallpaperVisibility(displayId, visible); + mLauncherProxyService.getProxy().updateWallpaperVisibility(displayId, visible); } catch (RemoteException e) { Log.e(TAG, "updateWallpaperVisibility() failed, visible: " + visible, e); } } private void appTransitionPending(boolean pending) { - if (mOverviewProxyService.getProxy() == null) { + if (mLauncherProxyService.getProxy() == null) { return; } try { - mOverviewProxyService.getProxy().appTransitionPending(pending); + mLauncherProxyService.getProxy().appTransitionPending(pending); } catch (RemoteException e) { Log.e(TAG, "appTransitionPending() failed, pending: " + pending, e); } @@ -483,18 +503,17 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, @Override public void setImeWindowStatus(int displayId, @ImeWindowVisibility int vis, @BackDispositionMode int backDisposition, boolean showImeSwitcher) { - boolean imeShown = mNavBarHelper.isImeShown(vis); - if (!imeShown) { - // Count imperceptible changes as visible so we transition taskbar out quickly. - imeShown = (vis & InputMethodService.IME_VISIBLE_IMPERCEPTIBLE) != 0; - } - showImeSwitcher = imeShown && showImeSwitcher; - int hints = Utilities.calculateBackDispositionHints(mNavigationIconHints, backDisposition, - imeShown, showImeSwitcher); - if (hints != mNavigationIconHints) { - mNavigationIconHints = hints; - updateSysuiFlags(); + // Count imperceptible changes as visible so we transition taskbar out quickly. + final boolean isImeVisible = mNavBarHelper.isImeVisible(vis) + || (vis & InputMethodService.IME_VISIBLE_IMPERCEPTIBLE) != 0; + final int flags = Utilities.updateNavbarFlagsFromIme(mNavbarFlags, backDisposition, + isImeVisible, showImeSwitcher); + if (flags == mNavbarFlags) { + return; } + + mNavbarFlags = flags; + updateSysuiFlags(); } @Override @@ -509,14 +528,14 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, @Override public void onRotationProposal(int rotation, boolean isValid) { - mOverviewProxyService.onRotationProposal(rotation, isValid); + mLauncherProxyService.onRotationProposal(rotation, isValid); } @Override public void disable(int displayId, int state1, int state2, boolean animate) { mDisabledFlags = state1; updateSysuiFlags(); - mOverviewProxyService.disable(displayId, state1, state2, animate); + mLauncherProxyService.disable(displayId, state1, state2, animate); } @Override @@ -524,7 +543,7 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme, int behavior, @InsetsType int requestedVisibleTypes, String packageName, LetterboxDetails[] letterboxDetails) { - mOverviewProxyService.onSystemBarAttributesChanged(displayId, behavior); + mLauncherProxyService.onSystemBarAttributesChanged(displayId, behavior); boolean nbModeChanged = false; if (mAppearance != appearance) { mAppearance = appearance; @@ -577,12 +596,12 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, @Override public void toggleTaskbar() { - if (mOverviewProxyService.getProxy() == null) { + if (mLauncherProxyService.getProxy() == null) { return; } try { - mOverviewProxyService.getProxy().onTaskbarToggled(); + mLauncherProxyService.getProxy().onTaskbarToggled(); } catch (RemoteException e) { Log.e(TAG, "onTaskbarToggled() failed", e); } @@ -654,7 +673,7 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, @Override public void setNavigationBarLumaSamplingEnabled(int displayId, boolean enable) { - mOverviewProxyService.onNavigationBarLumaSamplingEnabled(displayId, enable); + mLauncherProxyService.onNavigationBarLumaSamplingEnabled(displayId, enable); } @Override @@ -688,7 +707,7 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, @Override public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { pw.println("TaskbarDelegate (mDefaultDisplayId=" + mDefaultDisplayId + "):"); - pw.println(" mNavigationIconHints=" + mNavigationIconHints); + pw.println(" mNavbarFlags=" + mNavbarFlags); pw.println(" mNavigationMode=" + mNavigationMode); pw.println(" mDisabledFlags=" + mDisabledFlags); pw.println(" mTaskBarWindowState=" + mTaskBarWindowState); diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index 1c94f56f0942..f44c2c01951c 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -85,7 +85,7 @@ import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.NavigationEdgeBackPlugin; import com.android.systemui.plugins.PluginListener; import com.android.systemui.plugins.PluginManager; -import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.recents.LauncherProxyService; import com.android.systemui.res.R; import com.android.systemui.settings.UserTracker; import com.android.systemui.shared.system.ActivityManagerWrapper; @@ -155,8 +155,8 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack } }; - private OverviewProxyService.OverviewProxyListener mQuickSwitchListener = - new OverviewProxyService.OverviewProxyListener() { + private LauncherProxyService.LauncherProxyListener mQuickSwitchListener = + new LauncherProxyService.LauncherProxyListener() { @Override public void onPrioritizedRotation(@Surface.Rotation int rotation) { mStartingQuickstepRotation = rotation; @@ -197,7 +197,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack private final Context mContext; private final UserTracker mUserTracker; - private final OverviewProxyService mOverviewProxyService; + private final LauncherProxyService mLauncherProxyService; private final SysUiState mSysUiState; private Runnable mStateChangeCallback; private Consumer<Boolean> mButtonForcedVisibleCallback; @@ -332,7 +332,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack : SysUiStatsLog.BACK_GESTURE__TYPE__COMPLETED); if (!mInRejectedExclusion) { // Log successful back gesture to contextual edu stats - mOverviewProxyService.updateContextualEduStats(mIsTrackpadThreeFingerSwipe, + mLauncherProxyService.updateContextualEduStats(mIsTrackpadThreeFingerSwipe, GestureType.BACK); } } @@ -441,7 +441,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack @AssistedInject EdgeBackGestureHandler( @Assisted Context context, - OverviewProxyService overviewProxyService, + LauncherProxyService launcherProxyService, SysUiState sysUiState, PluginManager pluginManager, @BackPanelUiThread UiThreadContext uiThreadContext, @@ -468,7 +468,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mBackgroundExecutor = backgroundExecutor; mBgHandler = bgHandler; mUserTracker = userTracker; - mOverviewProxyService = overviewProxyService; + mLauncherProxyService = launcherProxyService; mSysUiState = sysUiState; mPluginManager = pluginManager; mNavigationModeController = navigationModeController; @@ -620,7 +620,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack */ public void onNavBarAttached() { mIsAttached = true; - mOverviewProxyService.addCallback(mQuickSwitchListener); + mLauncherProxyService.addCallback(mQuickSwitchListener); mSysUiState.addCallback(mSysUiStateCallback); mInputManager.registerInputDeviceListener(mInputDeviceListener, mBgHandler); int[] inputDevices = mInputManager.getInputDeviceIds(); @@ -636,7 +636,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack */ public void onNavBarDetached() { mIsAttached = false; - mOverviewProxyService.removeCallback(mQuickSwitchListener); + mLauncherProxyService.removeCallback(mQuickSwitchListener); mSysUiState.removeCallback(mSysUiStateCallback); mInputManager.unregisterInputDeviceListener(mInputDeviceListener); mTrackpadsConnected.clear(); diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java index c78750718cf0..f95f45906b23 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java @@ -17,12 +17,14 @@ package com.android.systemui.navigationbar.views; import static android.app.ActivityManager.LOCK_TASK_MODE_PINNED; -import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN; -import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN; +import static android.app.StatusBarManager.NAVBAR_BACK_DISMISS_IME; +import static android.app.StatusBarManager.NAVBAR_IME_SWITCHER_BUTTON_VISIBLE; +import static android.app.StatusBarManager.NAVBAR_IME_VISIBLE; import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN; import static android.app.StatusBarManager.WINDOW_STATE_SHOWING; import static android.app.StatusBarManager.WindowType; import static android.app.StatusBarManager.WindowVisibleState; +import static android.app.StatusBarManager.navbarFlagsToString; import static android.app.StatusBarManager.windowStateToString; import static android.app.WindowConfiguration.ROTATION_UNDEFINED; import static android.view.InsetsSource.FLAG_SUPPRESS_SCRIM; @@ -34,7 +36,7 @@ import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.HOME_BUTTON_LONG_PRESS_DURATION_MS; import static com.android.systemui.navigationbar.NavBarHelper.transitionMode; -import static com.android.systemui.recents.OverviewProxyService.OverviewProxyListener; +import static com.android.systemui.recents.LauncherProxyService.LauncherProxyListener; import static com.android.systemui.shared.recents.utilities.Utilities.isLargeScreen; import static com.android.systemui.shared.rotation.RotationButtonController.DEBUG_ROTATION; import static com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_OPAQUE; @@ -42,8 +44,9 @@ import static com.android.systemui.shared.statusbar.phone.BarTransitions.Transit import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY; -import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING; -import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BACK_DISMISS_IME; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_VISIBLE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING; import static com.android.systemui.shared.system.QuickStepContract.isGesturalMode; @@ -56,6 +59,7 @@ import android.annotation.NonNull; import android.app.ActivityTaskManager; import android.app.IActivityTaskManager; import android.app.StatusBarManager; +import android.app.StatusBarManager.NavbarFlags; import android.content.Context; import android.content.res.Configuration; import android.graphics.Insets; @@ -127,7 +131,7 @@ import com.android.systemui.navigationbar.views.buttons.KeyButtonView; import com.android.systemui.navigationbar.views.buttons.NavBarButtonClickLogger; import com.android.systemui.navigationbar.views.buttons.NavbarOrientationTrackingLogger; import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.recents.LauncherProxyService; import com.android.systemui.recents.Recents; import com.android.systemui.res.R; import com.android.systemui.settings.DisplayTracker; @@ -208,7 +212,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements private final ShadeViewController mShadeViewController; private final PanelExpansionInteractor mPanelExpansionInteractor; private final NotificationRemoteInputManager mNotificationRemoteInputManager; - private final OverviewProxyService mOverviewProxyService; + private final LauncherProxyService mLauncherProxyService; private final NavigationModeController mNavigationModeController; private final UserTracker mUserTracker; private final CommandQueue mCommandQueue; @@ -233,7 +237,8 @@ public class NavigationBar extends ViewController<NavigationBarView> implements private @WindowVisibleState int mNavigationBarWindowState = WINDOW_STATE_SHOWING; - private int mNavigationIconHints = 0; + @NavbarFlags + private int mNavbarFlags; private @TransitionMode int mTransitionMode; private boolean mLongPressHomeEnabled; @@ -278,7 +283,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements * gesture to indicate to them that they can continue in that orientation without having to * rotate the phone * The secondary handle will show when we get - * {@link OverviewProxyListener#notifyPrioritizedRotation(int)} callback with the + * {@link LauncherProxyListener#notifyPrioritizedRotation(int)} callback with the * original handle hidden and we'll flip the visibilities once the * {@link #mTasksFrozenListener} fires */ @@ -382,12 +387,12 @@ public class NavigationBar extends ViewController<NavigationBarView> implements } }; - private final OverviewProxyListener mOverviewProxyListener = new OverviewProxyListener() { + private final LauncherProxyListener mLauncherProxyListener = new LauncherProxyListener() { @Override public void onConnectionChanged(boolean isConnected) { - mView.onOverviewProxyConnectionChange( - mOverviewProxyService.isEnabled()); - mView.setShouldShowSwipeUpUi(mOverviewProxyService.shouldShowSwipeUpUI()); + mView.onLauncherProxyConnectionChange( + mLauncherProxyService.isEnabled()); + mView.setShouldShowSwipeUpUi(mLauncherProxyService.shouldShowSwipeUpUI()); updateScreenPinningGestures(); } @@ -560,7 +565,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements AccessibilityManager accessibilityManager, DeviceProvisionedController deviceProvisionedController, MetricsLogger metricsLogger, - OverviewProxyService overviewProxyService, + LauncherProxyService launcherProxyService, NavigationModeController navigationModeController, StatusBarStateController statusBarStateController, StatusBarKeyguardViewManager statusBarKeyguardViewManager, @@ -613,7 +618,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mShadeViewController = shadeViewController; mPanelExpansionInteractor = panelExpansionInteractor; mNotificationRemoteInputManager = notificationRemoteInputManager; - mOverviewProxyService = overviewProxyService; + mLauncherProxyService = launcherProxyService; mNavigationModeController = navigationModeController; mUserTracker = userTracker; mCommandQueue = commandQueue; @@ -649,18 +654,18 @@ public class NavigationBar extends ViewController<NavigationBarView> implements if (!mEdgeBackGestureHandler.isHandlingGestures()) { // We're in 2/3 button mode OR back button force-shown in SUW if (!mImeVisible) { - // IME not showing, take all touches + // IME is not visible, take all touches info.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_FRAME); return; } if (!mView.isImeRenderingNavButtons()) { - // IME showing but not drawing any buttons, take all touches + // IME is visible but not drawing any buttons, take all touches info.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_FRAME); return; } } - // When in gestural and the IME is showing, don't use the nearest region since it will + // When in gestural and the IME is visible, don't use the nearest region since it will // take gesture space away from the IME info.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_REGION); info.touchableRegion.set( @@ -817,13 +822,12 @@ public class NavigationBar extends ViewController<NavigationBarView> implements if (mSavedState != null) { getBarTransitions().getLightTransitionsController().restoreState(mSavedState); } - setNavigationIconHints(mNavigationIconHints); setWindowVisible(isNavBarWindowVisible()); mView.setBehavior(mBehavior); setNavBarMode(mNavBarMode); repositionNavigationBar(mCurrentRotation); mView.setUpdateActiveTouchRegionsCallback( - () -> mOverviewProxyService.onActiveNavBarRegionChanges( + () -> mLauncherProxyService.onActiveNavBarRegionChanges( getButtonLocations(true /* inScreen */, true /* useNearestRegion */))); mView.getViewTreeObserver().addOnComputeInternalInsetsListener( @@ -839,7 +843,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mWakefulnessLifecycle.addObserver(mWakefulnessObserver); notifyNavigationBarScreenOn(); - mOverviewProxyService.addCallback(mOverviewProxyListener); + mLauncherProxyService.addCallback(mLauncherProxyListener); updateSystemUiStateFlags(); // Currently there is no accelerometer sensor on non-default display. @@ -877,7 +881,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements public void onViewDetached() { mView.setUpdateActiveTouchRegionsCallback(null); getBarTransitions().destroy(); - mOverviewProxyService.removeCallback(mOverviewProxyListener); + mLauncherProxyService.removeCallback(mLauncherProxyListener); mUserTracker.removeCallback(mUserChangedCallback); mWakefulnessLifecycle.removeObserver(mWakefulnessObserver); if (mOrientationHandle != null) { @@ -1111,6 +1115,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements pw.println(" mLongPressHomeEnabled=" + mLongPressHomeEnabled); pw.println(" mNavigationBarWindowState=" + windowStateToString(mNavigationBarWindowState)); + pw.println(" mNavbarFlags=" + navbarFlagsToString(mNavbarFlags)); pw.println(" mTransitionMode=" + BarTransitions.modeToString(mTransitionMode)); pw.println(" mTransientShown=" + mTransientShown); @@ -1135,13 +1140,14 @@ public class NavigationBar extends ViewController<NavigationBarView> implements if (displayId != mDisplayId) { return; } - boolean imeShown = mNavBarHelper.isImeShown(vis); - showImeSwitcher = imeShown && showImeSwitcher; - int hints = Utilities.calculateBackDispositionHints(mNavigationIconHints, backDisposition, - imeShown, showImeSwitcher); - if (hints == mNavigationIconHints) return; + final boolean isImeVisible = mNavBarHelper.isImeVisible(vis); + final int flags = Utilities.updateNavbarFlagsFromIme(mNavbarFlags, backDisposition, + isImeVisible, showImeSwitcher); + if (flags == mNavbarFlags) { + return; + } - setNavigationIconHints(hints); + setNavbarFlags(flags); checkBarModes(); updateSystemUiStateFlags(); } @@ -1680,10 +1686,12 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mSysUiFlagsContainer.setFlag(SYSUI_STATE_A11Y_BUTTON_CLICKABLE, clickable) .setFlag(SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE, longClickable) .setFlag(SYSUI_STATE_NAV_BAR_HIDDEN, !isNavBarWindowVisible()) - .setFlag(SYSUI_STATE_IME_SHOWING, - (mNavigationIconHints & NAVIGATION_HINT_IME_SHOWN) != 0) - .setFlag(SYSUI_STATE_IME_SWITCHER_SHOWING, - (mNavigationIconHints & NAVIGATION_HINT_IME_SWITCHER_SHOWN) != 0) + .setFlag(SYSUI_STATE_IME_VISIBLE, + (mNavbarFlags & NAVBAR_IME_VISIBLE) != 0) + .setFlag(SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE, + (mNavbarFlags & NAVBAR_IME_SWITCHER_BUTTON_VISIBLE) != 0) + .setFlag(SYSUI_STATE_BACK_DISMISS_IME, + (mNavbarFlags & NAVBAR_BACK_DISMISS_IME) != 0) .setFlag(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY, allowSystemGestureIgnoringBarVisibility()) .commitUpdate(mDisplayId); @@ -1691,9 +1699,9 @@ public class NavigationBar extends ViewController<NavigationBarView> implements private void updateAssistantEntrypoints(boolean assistantAvailable, boolean longPressHomeEnabled) { - if (mOverviewProxyService.getProxy() != null) { + if (mLauncherProxyService.getProxy() != null) { try { - mOverviewProxyService.getProxy().onAssistantAvailable(assistantAvailable, + mLauncherProxyService.getProxy().onAssistantAvailable(assistantAvailable, longPressHomeEnabled); } catch (RemoteException e) { Log.w(TAG, "Unable to send assistant availability data to launcher"); @@ -1926,30 +1934,37 @@ public class NavigationBar extends ViewController<NavigationBarView> implements }; @VisibleForTesting - int getNavigationIconHints() { - return mNavigationIconHints; + @NavbarFlags + int getNavbarFlags() { + return mNavbarFlags; } - private void setNavigationIconHints(int hints) { - if (hints == mNavigationIconHints) return; + /** + * Sets the navigation bar state flags. + * + * @param flags the navigation bar state flags. + */ + private void setNavbarFlags(@NavbarFlags int flags) { + if (flags == mNavbarFlags) { + return; + } if (!isLargeScreen(mContext)) { // All IME functions handled by launcher via Sysui flags for large screen - final boolean newBackAlt = (hints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0; - final boolean oldBackAlt = - (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0; - if (newBackAlt != oldBackAlt) { - mView.onBackAltChanged(newBackAlt); + final boolean backDismissIme = (flags & StatusBarManager.NAVBAR_BACK_DISMISS_IME) != 0; + final boolean oldBackDismissIme = + (mNavbarFlags & StatusBarManager.NAVBAR_BACK_DISMISS_IME) != 0; + if (backDismissIme != oldBackDismissIme) { + mView.onBackDismissImeChanged(backDismissIme); } - mImeVisible = (hints & NAVIGATION_HINT_IME_SHOWN) != 0; + mImeVisible = (flags & NAVBAR_IME_VISIBLE) != 0; - mView.setNavigationIconHints(hints); + mView.setNavbarFlags(flags); } if (DEBUG) { - android.widget.Toast.makeText(mContext, - "Navigation icon hints = " + hints, - 500).show(); + android.widget.Toast.makeText(mContext, "Navbar flags = " + flags, 500) + .show(); } - mNavigationIconHints = hints; + mNavbarFlags = flags; } /** @@ -2093,7 +2108,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements if (!canShowSecondaryHandle()) { resetSecondaryHandle(); } - mView.setShouldShowSwipeUpUi(mOverviewProxyService.shouldShowSwipeUpUI()); + mView.setShouldShowSwipeUpUi(mLauncherProxyService.shouldShowSwipeUpUI()); } }; diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarInflaterView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarInflaterView.java index 96b730c08397..2c5a9c84645b 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarInflaterView.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarInflaterView.java @@ -41,7 +41,7 @@ import com.android.systemui.navigationbar.views.buttons.ButtonDispatcher; import com.android.systemui.navigationbar.views.buttons.KeyButtonView; import com.android.systemui.navigationbar.views.buttons.ReverseLinearLayout; import com.android.systemui.navigationbar.views.buttons.ReverseLinearLayout.ReverseRelativeLayout; -import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.recents.LauncherProxyService; import com.android.systemui.res.R; import com.android.systemui.shared.system.QuickStepContract; @@ -117,13 +117,13 @@ public class NavigationBarInflaterView extends FrameLayout { private boolean mIsVertical; private boolean mAlternativeOrder; - private OverviewProxyService mOverviewProxyService; + private LauncherProxyService mLauncherProxyService; private int mNavBarMode = NAV_BAR_MODE_3BUTTON; public NavigationBarInflaterView(Context context, AttributeSet attrs) { super(context, attrs); createInflaters(); - mOverviewProxyService = Dependency.get(OverviewProxyService.class); + mLauncherProxyService = Dependency.get(LauncherProxyService.class); mListener = new Listener(this); mNavBarMode = Dependency.get(NavigationModeController.class).addListener(mListener); } @@ -159,7 +159,7 @@ public class NavigationBarInflaterView extends FrameLayout { protected String getDefaultLayout() { final int defaultResource = QuickStepContract.isGesturalMode(mNavBarMode) ? R.string.config_navBarLayoutHandle - : mOverviewProxyService.shouldShowSwipeUpUI() + : mLauncherProxyService.shouldShowSwipeUpUI() ? R.string.config_navBarLayoutQuickstep : R.string.config_navBarLayout; return getContext().getString(defaultResource); diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java index d5ae72165c4a..36cb8fa374b0 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java @@ -16,6 +16,9 @@ package com.android.systemui.navigationbar.views; +import static android.app.StatusBarManager.NAVBAR_BACK_DISMISS_IME; +import static android.app.StatusBarManager.NAVBAR_IME_SWITCHER_BUTTON_VISIBLE; +import static android.app.StatusBarManager.NAVBAR_IME_VISIBLE; import static android.inputmethodservice.InputMethodService.canImeRenderGesturalNavButtons; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; @@ -31,7 +34,7 @@ import android.animation.PropertyValuesHolder; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.annotation.DrawableRes; -import android.app.StatusBarManager; +import android.app.StatusBarManager.NavbarFlags; import android.content.Context; import android.content.res.Configuration; import android.graphics.Canvas; @@ -113,7 +116,8 @@ public class NavigationBarView extends FrameLayout { boolean mLongClickableAccessibilityButton; int mDisabledFlags = 0; - int mNavigationIconHints = 0; + @NavbarFlags + private int mNavbarFlags; private int mNavBarMode; private boolean mImeDrawsImeNavBar; @@ -176,7 +180,7 @@ public class NavigationBarView extends FrameLayout { */ private final boolean mImeCanRenderGesturalNavButtons = canImeRenderGesturalNavButtons(); private Gefingerpoken mTouchHandler; - private boolean mOverviewProxyEnabled; + private boolean mLauncherProxyEnabled; private boolean mShowSwipeUpUi; private UpdateActiveTouchRegionsCallback mUpdateActiveTouchRegionsCallback; @@ -210,7 +214,11 @@ public class NavigationBarView extends FrameLayout { } } - public void onBackAltCleared() { + /** + * Called when the back button is no longer visually adjusted to indicate that it will + * dismiss the IME when pressed. + */ + public void onBackDismissImeCleared() { ButtonDispatcher backButton = getBackButton(); // When dismissing ime during unlock, force the back button to run the same appearance @@ -499,10 +507,9 @@ public class NavigationBarView extends FrameLayout { } private void orientBackButton(KeyButtonDrawable drawable) { - final boolean useAltBack = - (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0; + final boolean isBackDismissIme = (mNavbarFlags & NAVBAR_BACK_DISMISS_IME) != 0; final boolean isRtl = mConfiguration.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; - float degrees = useAltBack ? (isRtl ? 90 : -90) : 0; + float degrees = isBackDismissIme ? (isRtl ? 90 : -90) : 0; if (drawable.getRotation() == degrees) { return; } @@ -514,7 +521,7 @@ public class NavigationBarView extends FrameLayout { // Animate the back button's rotation to the new degrees and only in portrait move up the // back button to line up with the other buttons - float targetY = !mShowSwipeUpUi && !mIsVertical && useAltBack + float targetY = !mShowSwipeUpUi && !mIsVertical && isBackDismissIme ? - getResources().getDimension(R.dimen.navbar_back_button_ime_offset) : 0; ObjectAnimator navBarAnimator = ObjectAnimator.ofPropertyValuesHolder(drawable, @@ -555,22 +562,25 @@ public class NavigationBarView extends FrameLayout { super.setLayoutDirection(layoutDirection); } - void setNavigationIconHints(int hints) { - if (hints == mNavigationIconHints) return; - mNavigationIconHints = hints; + void setNavbarFlags(@NavbarFlags int flags) { + if (flags == mNavbarFlags) { + return; + } + mNavbarFlags = flags; updateNavButtonIcons(); } /** - * Called when the boolean value of whether to adjust the back button for the IME changed. + * Called when the state of the back button being visually adjusted to indicate that it will + * dismiss the IME when pressed has changed. * - * @param useBackAlt whether to adjust the back button for the IME. + * @param isBackDismissIme whether the back button is adjusted for IME dismissal. * * @see android.inputmethodservice.InputMethodService.BackDispositionMode */ - void onBackAltChanged(boolean useBackAlt) { - if (!useBackAlt) { - mTransitionListener.onBackAltCleared(); + void onBackDismissImeChanged(boolean isBackDismissIme) { + if (!isBackDismissIme) { + mTransitionListener.onBackDismissImeCleared(); } } @@ -594,8 +604,7 @@ public class NavigationBarView extends FrameLayout { // We have to replace or restore the back and home button icons when exiting or entering // carmode, respectively. Recents are not available in CarMode in nav bar so change // to recent icon is not required. - final boolean useAltBack = - (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0; + final boolean isBackDismissIme = (mNavbarFlags & NAVBAR_BACK_DISMISS_IME) != 0; KeyButtonDrawable backIcon = mBackIcon; orientBackButton(backIcon); KeyButtonDrawable homeIcon = mHomeDefaultIcon; @@ -607,11 +616,12 @@ public class NavigationBarView extends FrameLayout { updateRecentsIcon(); - // Update IME button visibility, a11y and rotate button always overrides the appearance - boolean disableImeSwitcher = - (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN) == 0 - || isImeRenderingNavButtons(); - mContextualButtonGroup.setButtonVisibility(R.id.ime_switcher, !disableImeSwitcher); + // Update IME switcher button visibility, a11y and rotate button always overrides + // the appearance. + final boolean isImeSwitcherButtonVisible = + (mNavbarFlags & NAVBAR_IME_SWITCHER_BUTTON_VISIBLE) != 0 + && !isImeRenderingNavButtons(); + mContextualButtonGroup.setButtonVisibility(R.id.ime_switcher, isImeSwitcherButtonVisible); mBarTransitions.reapplyDarkIntensity(); @@ -625,14 +635,14 @@ public class NavigationBarView extends FrameLayout { boolean disableHomeHandle = disableRecent && ((mDisabledFlags & View.STATUS_BAR_DISABLE_HOME) != 0); - boolean disableBack = !useAltBack && (mEdgeBackGestureHandler.isHandlingGestures() + boolean disableBack = !isBackDismissIme && (mEdgeBackGestureHandler.isHandlingGestures() || ((mDisabledFlags & View.STATUS_BAR_DISABLE_BACK) != 0)) || isImeRenderingNavButtons(); // When screen pinning, don't hide back and home when connected service or back and // recents buttons when disconnected from launcher service in screen pinning mode, // as they are used for exiting. - if (mOverviewProxyEnabled) { + if (mLauncherProxyEnabled) { // Force disable recents when not in legacy mode disableRecent |= !QuickStepContract.isLegacyMode(mNavBarMode); if (mScreenPinningActive && !QuickStepContract.isGesturalMode(mNavBarMode)) { @@ -663,9 +673,8 @@ public class NavigationBarView extends FrameLayout { * Returns whether the IME is currently visible and drawing the nav buttons. */ boolean isImeRenderingNavButtons() { - return mImeDrawsImeNavBar - && mImeCanRenderGesturalNavButtons - && (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_IME_SHOWN) != 0; + return mImeDrawsImeNavBar && mImeCanRenderGesturalNavButtons + && (mNavbarFlags & NAVBAR_IME_VISIBLE) != 0; } @VisibleForTesting @@ -755,8 +764,8 @@ public class NavigationBarView extends FrameLayout { } } - void onOverviewProxyConnectionChange(boolean enabled) { - mOverviewProxyEnabled = enabled; + void onLauncherProxyConnectionChange(boolean enabled) { + mLauncherProxyEnabled = enabled; } void setShouldShowSwipeUpUi(boolean showSwipeUpUi) { diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/buttons/KeyButtonView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/buttons/KeyButtonView.java index 111a2d43f881..32a03e5b10e9 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/buttons/KeyButtonView.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/buttons/KeyButtonView.java @@ -61,7 +61,7 @@ import com.android.internal.logging.UiEventLoggerImpl; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.systemui.Dependency; import com.android.systemui.assist.AssistManager; -import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.recents.LauncherProxyService; import com.android.systemui.res.R; import com.android.systemui.shared.navigationbar.KeyButtonRipple; import com.android.systemui.shared.system.QuickStepContract; @@ -82,7 +82,7 @@ public class KeyButtonView extends ImageView implements ButtonInterface { @VisibleForTesting boolean mLongClicked; private OnClickListener mOnClickListener; private final KeyButtonRipple mRipple; - private final OverviewProxyService mOverviewProxyService; + private final LauncherProxyService mLauncherProxyService; private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class); private final InputManagerGlobal mInputManagerGlobal; private final Paint mOvalBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); @@ -181,7 +181,7 @@ public class KeyButtonView extends ImageView implements ButtonInterface { mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); mRipple = new KeyButtonRipple(context, this, R.dimen.key_button_ripple_max_width); - mOverviewProxyService = Dependency.get(OverviewProxyService.class); + mLauncherProxyService = Dependency.get(LauncherProxyService.class); mInputManagerGlobal = manager; setBackground(mRipple); setWillNotDraw(false); @@ -282,7 +282,7 @@ public class KeyButtonView extends ImageView implements ButtonInterface { @Override public boolean onTouchEvent(MotionEvent ev) { - final boolean showSwipeUI = mOverviewProxyService.shouldShowSwipeUpUI(); + final boolean showSwipeUI = mLauncherProxyService.shouldShowSwipeUpUI(); final int action = ev.getAction(); int x, y; if (action == MotionEvent.ACTION_DOWN) { diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt index 195b0cebe2eb..1b9251061f3d 100644 --- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt @@ -21,7 +21,8 @@ import com.android.compose.animation.scene.Swipe import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.compose.animation.scene.UserActionResult.HideOverlay -import com.android.compose.animation.scene.UserActionResult.ReplaceByOverlay +import com.android.compose.animation.scene.UserActionResult.ShowOverlay +import com.android.compose.animation.scene.UserActionResult.ShowOverlay.HideCurrentOverlays import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel @@ -38,7 +39,10 @@ class NotificationsShadeOverlayActionsViewModel @AssistedInject constructor() : Swipe.Up to HideOverlay(Overlays.NotificationsShade), Back to HideOverlay(Overlays.NotificationsShade), Swipe.Down(fromSource = SceneContainerEdge.TopRight) to - ReplaceByOverlay(Overlays.QuickSettingsShade), + ShowOverlay( + Overlays.QuickSettingsShade, + hideCurrentOverlays = HideCurrentOverlays.Some(Overlays.NotificationsShade), + ), ) ) } diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt deleted file mode 100644 index 398ace4b67f4..000000000000 --- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.notifications.ui.viewmodel - -import com.android.compose.animation.scene.Back -import com.android.compose.animation.scene.Swipe -import com.android.compose.animation.scene.UserAction -import com.android.compose.animation.scene.UserActionResult -import com.android.compose.animation.scene.UserActionResult.ReplaceByOverlay -import com.android.systemui.scene.shared.model.Overlays -import com.android.systemui.scene.shared.model.SceneFamilies -import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge -import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -/** - * Models the UI state for the user actions that the user can perform to navigate to other scenes. - */ -class NotificationsShadeUserActionsViewModel @AssistedInject constructor() : - UserActionsViewModel() { - - override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) { - setActions( - mapOf( - Back to SceneFamilies.Home, - Swipe.Up to SceneFamilies.Home, - Swipe.Down(fromSource = SceneContainerEdge.TopRight) to - ReplaceByOverlay(Overlays.QuickSettingsShade), - ) - ) - } - - @AssistedFactory - interface Factory { - fun create(): NotificationsShadeUserActionsViewModel - } -} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java index afb852ae824c..c8f7be6d80b2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java @@ -17,7 +17,6 @@ package com.android.systemui.qs; import static com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import static com.android.systemui.Flags.quickSettingsVisualHapticsLongpress; import android.annotation.NonNull; import android.annotation.Nullable; @@ -364,12 +363,7 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr } private void addTile(final QSTile tile, boolean collapsedView) { - QSLongPressEffect longPressEffect; - if (quickSettingsVisualHapticsLongpress()) { - longPressEffect = mLongPressEffectProvider.get(); - } else { - longPressEffect = null; - } + QSLongPressEffect longPressEffect = mLongPressEffectProvider.get(); final QSTileViewImpl tileView = new QSTileViewImpl( getContext(), collapsedView, longPressEffect); final TileRecord r = new TileRecord(tile, tileView); diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/BounceableInfo.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/BounceableInfo.kt index c9d767e6d152..302242ca11dd 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/BounceableInfo.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/BounceableInfo.kt @@ -16,7 +16,7 @@ package com.android.systemui.qs.panels.ui.compose -import android.processor.immutability.Immutable +import androidx.compose.runtime.Stable import com.android.compose.animation.Bounceable import com.android.systemui.qs.panels.shared.model.SizedTile import com.android.systemui.qs.panels.ui.model.GridCell @@ -24,7 +24,7 @@ import com.android.systemui.qs.panels.ui.model.TileGridCell import com.android.systemui.qs.panels.ui.viewmodel.BounceableTileViewModel import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel -@Immutable +@Stable data class BounceableInfo( val bounceable: BounceableTileViewModel, val previousTile: Bounceable?, diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt index 5cb30b999e13..b084f79a5bba 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt @@ -19,8 +19,8 @@ package com.android.systemui.qs.panels.ui.compose import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.key import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier @@ -49,6 +49,8 @@ fun ContentScope.QuickQuickSettings( val squishiness by viewModel.squishinessViewModel.squishiness.collectAsStateWithLifecycle() val scope = rememberCoroutineScope() + val spans by remember(sizedTiles) { derivedStateOf { sizedTiles.fastMap { it.width } } } + DisposableEffect(tiles) { val token = Any() tiles.forEach { it.startListening(token) } @@ -62,26 +64,24 @@ fun ContentScope.QuickQuickSettings( columns = columns, columnSpacing = dimensionResource(R.dimen.qs_tile_margin_horizontal), rowSpacing = dimensionResource(R.dimen.qs_tile_margin_vertical), - spans = sizedTiles.fastMap { it.width }, + spans = spans, modifier = Modifier.sysuiResTag("qqs_tile_layout"), + keys = { sizedTiles[it].tile.spec }, ) { spanIndex -> val it = sizedTiles[spanIndex] val column = cellIndex % columns cellIndex += it.width - key(it.tile.spec) { - Tile( - tile = it.tile, - iconOnly = it.isIcon, - modifier = Modifier.element(it.tile.spec.toElementKey(spanIndex)), - squishiness = { squishiness }, - coroutineScope = scope, - bounceableInfo = bounceables.bounceableInfo(it, spanIndex, column, columns), - tileHapticsViewModelFactoryProvider = - viewModel.tileHapticsViewModelFactoryProvider, - // There should be no QuickQuickSettings when the details view is enabled. - detailsViewModel = null, - ) - } + Tile( + tile = it.tile, + iconOnly = it.isIcon, + modifier = Modifier.element(it.tile.spec.toElementKey(spanIndex)), + squishiness = { squishiness }, + coroutineScope = scope, + bounceableInfo = bounceables.bounceableInfo(it, spanIndex, column, columns), + tileHapticsViewModelFactoryProvider = viewModel.tileHapticsViewModelFactoryProvider, + // There should be no QuickQuickSettings when the details view is enabled. + detailsViewModel = null, + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt index d2ee126ace91..2cccaddc02a8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt @@ -498,11 +498,9 @@ fun gridHeight(rows: Int, tileHeight: Dp, tilePadding: Dp, gridPadding: Dp): Dp return ((tileHeight + tilePadding) * rows) + gridPadding * 2 } -private fun GridCell.key(index: Int, dragAndDropState: DragAndDropState): Any { +private fun GridCell.key(index: Int): Any { return when (this) { - is TileGridCell -> { - if (dragAndDropState.isMoving(tile.tileSpec)) index else key - } + is TileGridCell -> key is SpacerGridCell -> index } } @@ -510,10 +508,13 @@ private fun GridCell.key(index: Int, dragAndDropState: DragAndDropState): Any { /** * Adds a list of [GridCell] to the lazy grid * - * @param cells the pairs of [GridCell] to [AnimatableTileViewModel] + * @param cells the pairs of [GridCell] to [BounceableTileViewModel] + * @param columns the number of columns of this tile grid * @param dragAndDropState the [DragAndDropState] for this grid * @param selectionState the [MutableSelectionState] for this grid - * @param onToggleSize the callback when a tile's size is toggled + * @param coroutineScope the [CoroutineScope] to be used for the tiles + * @param largeTilesSpan the width used for large tiles + * @param onResize the callback when a tile has a new [ResizeOperation] */ fun LazyGridScope.EditTiles( cells: List<Pair<GridCell, BounceableTileViewModel>>, @@ -526,7 +527,7 @@ fun LazyGridScope.EditTiles( ) { items( count = cells.size, - key = { cells[it].first.key(it, dragAndDropState) }, + key = { cells[it].first.key(it) }, span = { cells[it].first.span }, contentType = { TileType }, ) { index -> @@ -536,13 +537,12 @@ fun LazyGridScope.EditTiles( // If the tile is being moved, replace it with a visible spacer SpacerGridCell( Modifier.background( - color = - MaterialTheme.colorScheme.secondary.copy( - alpha = EditModeTileDefaults.PLACEHOLDER_ALPHA - ), - shape = RoundedCornerShape(InactiveCornerRadius), - ) - .animateItem() + color = + MaterialTheme.colorScheme.secondary.copy( + alpha = EditModeTileDefaults.PLACEHOLDER_ALPHA + ), + shape = RoundedCornerShape(InactiveCornerRadius), + ) ) } else { TileGridCell( diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt index 4432d336237f..cc4c3af1dc63 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt @@ -18,8 +18,8 @@ package com.android.systemui.qs.panels.ui.compose.infinitegrid import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.key import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier @@ -86,27 +86,28 @@ constructor( val scope = rememberCoroutineScope() var cellIndex = 0 + val spans by remember(sizedTiles) { derivedStateOf { sizedTiles.fastMap { it.width } } } + VerticalSpannedGrid( columns = columns, columnSpacing = dimensionResource(R.dimen.qs_tile_margin_horizontal), rowSpacing = dimensionResource(R.dimen.qs_tile_margin_vertical), - spans = sizedTiles.fastMap { it.width }, + spans = spans, + keys = { sizedTiles[it].tile.spec }, ) { spanIndex -> val it = sizedTiles[spanIndex] val column = cellIndex % columns cellIndex += it.width - key(it.tile.spec) { - Tile( - tile = it.tile, - iconOnly = iconTilesViewModel.isIconTile(it.tile.spec), - modifier = Modifier.element(it.tile.spec.toElementKey(spanIndex)), - squishiness = { squishiness }, - tileHapticsViewModelFactoryProvider = tileHapticsViewModelFactoryProvider, - coroutineScope = scope, - bounceableInfo = bounceables.bounceableInfo(it, spanIndex, column, columns), - detailsViewModel = detailsViewModel, - ) - } + Tile( + tile = it.tile, + iconOnly = iconTilesViewModel.isIconTile(it.tile.spec), + modifier = Modifier.element(it.tile.spec.toElementKey(spanIndex)), + squishiness = { squishiness }, + tileHapticsViewModelFactoryProvider = tileHapticsViewModelFactoryProvider, + coroutineScope = scope, + bounceableInfo = bounceables.bounceableInfo(it, spanIndex, column, columns), + detailsViewModel = detailsViewModel, + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt index 16c27223a471..8a627c452081 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt @@ -18,6 +18,7 @@ package com.android.systemui.qs.pipeline.shared import android.content.ComponentName import android.text.TextUtils +import androidx.compose.runtime.Stable import com.android.systemui.qs.external.CustomTile /** @@ -34,6 +35,7 @@ sealed class TileSpec private constructor(open val spec: String) { data object Invalid : TileSpec("") /** Container for the spec of a tile provided by SystemUI. */ + @Stable data class PlatformTileSpec internal constructor(override val spec: String) : TileSpec(spec) { override fun toString(): String { return "P($spec)" @@ -45,6 +47,7 @@ sealed class TileSpec private constructor(open val spec: String) { * * [componentName] indicates the associated `TileService`. */ + @Stable data class CustomTileSpec internal constructor(override val spec: String, val componentName: ComponentName) : TileSpec(spec) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt index 6d3e5d07c251..b1f99cccff70 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt @@ -61,6 +61,7 @@ constructor( private val internetDialogManager: InternetDialogManager, private val wifiStateWorker: WifiStateWorker, private val accessPointController: AccessPointController, + private val internetDetailsViewModelFactory: InternetDetailsViewModel.Factory, ) : QSTileImpl<QSTile.BooleanState>( host, @@ -107,7 +108,7 @@ constructor( } override fun getDetailsViewModel(): TileDetailsViewModel { - return InternetDetailsViewModel { longClick(null) } + return internetDetailsViewModelFactory.create { longClick(null) } } override fun handleSecondaryClick(expandable: Expandable?) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt index 0051bf5de7f2..ad5dd27f07c2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt @@ -35,12 +35,14 @@ import com.android.systemui.modes.shared.ModesUiIcons import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.qs.QSTile +import com.android.systemui.plugins.qs.TileDetailsViewModel import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.qs.QSHost import com.android.systemui.qs.QsEventLogger import com.android.systemui.qs.asQSTileIcon import com.android.systemui.qs.logging.QSLogger import com.android.systemui.qs.tileimpl.QSTileImpl +import com.android.systemui.qs.tiles.dialog.ModesDetailsViewModel import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesTileDataInteractor import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesTileUserActionInteractor import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel @@ -48,6 +50,7 @@ import com.android.systemui.qs.tiles.impl.modes.ui.ModesTileMapper import com.android.systemui.qs.tiles.viewmodel.QSTileConfigProvider import com.android.systemui.qs.tiles.viewmodel.QSTileState import com.android.systemui.res.R +import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel import javax.inject.Inject import kotlinx.coroutines.runBlocking @@ -67,6 +70,7 @@ constructor( private val dataInteractor: ModesTileDataInteractor, private val tileMapper: ModesTileMapper, private val userActionInteractor: ModesTileUserActionInteractor, + private val modesDialogViewModel: ModesDialogViewModel, ) : QSTileImpl<QSTile.State>( host, @@ -114,6 +118,13 @@ constructor( userActionInteractor.handleToggleClick(model) } + override fun getDetailsViewModel(): TileDetailsViewModel { + return ModesDetailsViewModel( + onSettingsClick = { userActionInteractor.handleLongClick(null) }, + viewModel = modesDialogViewModel, + ) + } + override fun getLongClickIntent(): Intent = userActionInteractor.longClickIntent @VisibleForTesting diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt index c64532a2c4ba..733159e285e8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt @@ -57,10 +57,8 @@ import com.android.settingslib.satellite.SatelliteDialogUtils.mayStartSatelliteW import com.android.settingslib.wifi.WifiEnterpriseRestrictionUtils import com.android.systemui.Prefs import com.android.systemui.accessibility.floatingmenu.AnnotationLinkSpan -import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.qs.flags.QsDetailedView import com.android.systemui.res.R import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.policy.KeyguardStateController @@ -75,11 +73,7 @@ import kotlinx.coroutines.Job /** * View content for the Internet tile details that handles all UI interactions and state management. - * - * @param internetDialog non-null if the details should be shown as part of a dialog and null - * otherwise. */ -// TODO: b/377388104 Make this content for details view only. class InternetDetailsContentManager @AssistedInject constructor( @@ -88,9 +82,7 @@ constructor( @Assisted(CAN_CONFIG_WIFI) private val canConfigWifi: Boolean, @Assisted private val coroutineScope: CoroutineScope, @Assisted private var context: Context, - @Assisted private var internetDialog: SystemUIDialog?, private val uiEventLogger: UiEventLogger, - private val dialogTransitionAnimator: DialogTransitionAnimator, @Main private val handler: Handler, @Background private val backgroundExecutor: Executor, private val keyguard: KeyguardStateController, @@ -104,8 +96,6 @@ constructor( // UI Components private lateinit var contentView: View - private lateinit var internetDialogTitleView: TextView - private lateinit var internetDialogSubTitleView: TextView private lateinit var divider: View private lateinit var progressBar: ProgressBar private lateinit var ethernetLayout: LinearLayout @@ -132,7 +122,6 @@ constructor( private lateinit var shareWifiButton: Button private lateinit var airplaneModeButton: Button private var alertDialog: AlertDialog? = null - private lateinit var doneButton: Button private val canChangeWifiState = WifiEnterpriseRestrictionUtils.isChangeWifiStateAllowed(context) @@ -153,7 +142,6 @@ constructor( @Assisted(CAN_CONFIG_WIFI) canConfigWifi: Boolean, coroutineScope: CoroutineScope, context: Context, - internetDialog: SystemUIDialog?, ): InternetDetailsContentManager } @@ -209,8 +197,6 @@ constructor( } // Network layouts - internetDialogTitleView = contentView.requireViewById(R.id.internet_dialog_title) - internetDialogSubTitleView = contentView.requireViewById(R.id.internet_dialog_subtitle) divider = contentView.requireViewById(R.id.divider) progressBar = contentView.requireViewById(R.id.wifi_searching_progress) @@ -219,15 +205,6 @@ constructor( setMobileLayout() ethernetLayout = contentView.requireViewById(R.id.ethernet_layout) - // Done button is only visible for the dialog view - doneButton = contentView.requireViewById(R.id.done_button) - if (internetDialog == null) { - doneButton.visibility = View.GONE - } else { - // Set done button if qs details view is not enabled. - doneButton.setOnClickListener { internetDialog!!.dismiss() } - } - // Share WiFi shareWifiButton = contentView.requireViewById(R.id.share_wifi_button) shareWifiButton.setOnClickListener { view -> @@ -251,6 +228,17 @@ constructor( // Background drawables backgroundOn = context.getDrawable(R.drawable.settingslib_switch_bar_bg_on) backgroundOff = context.getDrawable(R.drawable.internet_dialog_selected_effect) + + // Done button is only visible for the dialog view + contentView.findViewById<Button>(R.id.done_button).apply { visibility = View.GONE } + + // Title and subtitle will be added in the `TileDetails` + contentView.findViewById<TextView>(R.id.internet_dialog_title).apply { + visibility = View.GONE + } + contentView.findViewById<TextView>(R.id.internet_dialog_subtitle).apply { + visibility = View.GONE + } } private fun setWifiLayout() { @@ -336,21 +324,19 @@ constructor( } } - private fun getDialogTitleText(): CharSequence { - return internetDetailsContentController.getDialogTitleText() + fun getTitleText(): String { + return internetDetailsContentController.getDialogTitleText().toString() + } + + fun getSubtitleText(): String { + return internetDetailsContentController.getSubtitleText(isProgressBarVisible).toString() } private fun updateDetailsUI(internetContent: InternetContent) { if (DEBUG) { Log.d(TAG, "updateDetailsUI ") } - if (QsDetailedView.isEnabled) { - internetDialogTitleView.visibility = View.GONE - internetDialogSubTitleView.visibility = View.GONE - } else { - internetDialogTitleView.text = internetContent.internetDialogTitleString - internetDialogSubTitleView.text = internetContent.internetDialogSubTitle - } + airplaneModeButton.visibility = if (internetContent.isAirplaneModeEnabled) View.VISIBLE else View.GONE @@ -361,17 +347,11 @@ constructor( private fun getStartingInternetContent(): InternetContent { return InternetContent( - internetDialogTitleString = getDialogTitleText(), - internetDialogSubTitle = getSubtitleText(), isWifiEnabled = internetDetailsContentController.isWifiEnabled, isDeviceLocked = internetDetailsContentController.isDeviceLocked, ) } - private fun getSubtitleText(): String { - return internetDetailsContentController.getSubtitleText(isProgressBarVisible).toString() - } - @VisibleForTesting internal fun hideWifiViews() { setProgressBarVisible(false) @@ -393,7 +373,6 @@ constructor( progressBar.visibility = if (visible) View.VISIBLE else View.GONE progressBar.isIndeterminate = visible divider.visibility = if (visible) View.GONE else View.VISIBLE - internetDialogSubTitleView.text = getSubtitleText() } private fun showTurnOffAutoDataSwitchDialog(subId: Int) { @@ -418,12 +397,7 @@ constructor( SystemUIDialog.setShowForAllUsers(alertDialog, true) SystemUIDialog.registerDismissListener(alertDialog) SystemUIDialog.setWindowOnTop(alertDialog, keyguard.isShowing()) - if (QsDetailedView.isEnabled) { - alertDialog!!.show() - } else { - dialogTransitionAnimator.showFromDialog(alertDialog!!, internetDialog!!, null, false) - Log.e(TAG, "Internet dialog is shown with the refactor code") - } + alertDialog!!.show() } private fun shouldShowMobileDialog(): Boolean { @@ -466,11 +440,8 @@ constructor( SystemUIDialog.setShowForAllUsers(alertDialog, true) SystemUIDialog.registerDismissListener(alertDialog) SystemUIDialog.setWindowOnTop(alertDialog, keyguard.isShowing()) - if (QsDetailedView.isEnabled) { - alertDialog!!.show() - } else { - dialogTransitionAnimator.showFromDialog(alertDialog!!, internetDialog!!, null, false) - } + + alertDialog!!.show() } private fun onClickConnectedWifi(view: View?) { @@ -803,7 +774,6 @@ constructor( secondaryMobileNetworkLayout?.setOnClickListener(null) seeAllLayout.setOnClickListener(null) wifiToggle.setOnCheckedChangeListener(null) - doneButton.setOnClickListener(null) shareWifiButton.setOnClickListener(null) airplaneModeButton.setOnClickListener(null) internetDetailsContentController.onStop() @@ -825,8 +795,6 @@ constructor( private fun getInternetContent(shouldUpdateMobileNetwork: Boolean): InternetContent { return InternetContent( shouldUpdateMobileNetwork = shouldUpdateMobileNetwork, - internetDialogTitleString = getDialogTitleText(), - internetDialogSubTitle = getSubtitleText(), activeNetworkIsCellular = if (shouldUpdateMobileNetwork) internetDetailsContentController.activeNetworkIsCellular() @@ -924,10 +892,7 @@ constructor( if (DEBUG) { Log.d(TAG, "dismissDialog") } - if (internetDialog != null) { - internetDialog!!.dismiss() - internetDialog = null - } + // TODO: b/377388104 Close details view } override fun onAccessPointsChanged( @@ -967,8 +932,6 @@ constructor( @VisibleForTesting data class InternetContent( - val internetDialogTitleString: CharSequence, - val internetDialogSubTitle: CharSequence, val isAirplaneModeEnabled: Boolean = false, val hasEthernet: Boolean = false, val shouldUpdateMobileNetwork: Boolean = false, diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt index f239a179d79a..df4dddbca9e6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt @@ -16,44 +16,93 @@ package com.android.systemui.qs.tiles.dialog +import android.util.Log import android.view.LayoutInflater import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.viewinterop.AndroidView import com.android.systemui.plugins.qs.TileDetailsViewModel import com.android.systemui.res.R +import com.android.systemui.statusbar.connectivity.AccessPointController +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject -class InternetDetailsViewModel( - onLongClick: () -> Unit, +class InternetDetailsViewModel +@AssistedInject +constructor( + private val accessPointController: AccessPointController, + private val contentManagerFactory: InternetDetailsContentManager.Factory, + @Assisted private val onLongClick: () -> Unit, ) : TileDetailsViewModel() { - private val _onLongClick = onLongClick + private lateinit var internetDetailsContentManager: InternetDetailsContentManager @Composable override fun GetContentView() { + val coroutineScope = rememberCoroutineScope() + val context = LocalContext.current + + internetDetailsContentManager = remember { + contentManagerFactory.create( + canConfigMobileData = accessPointController.canConfigMobileData(), + canConfigWifi = accessPointController.canConfigWifi(), + coroutineScope = coroutineScope, + context = context, + ) + } AndroidView( modifier = Modifier.fillMaxWidth().fillMaxHeight(), factory = { context -> - // Inflate with the existing dialog xml layout - LayoutInflater.from(context) - .inflate(R.layout.internet_connectivity_dialog, null) - // TODO: b/377388104 - Implement the internet details view + // Inflate with the existing dialog xml layout and bind it with the manager + val view = + LayoutInflater.from(context) + .inflate(R.layout.internet_connectivity_dialog, null) + internetDetailsContentManager.bind(view) + + view + // TODO: b/377388104 - Polish the internet details view UI + }, + onRelease = { + internetDetailsContentManager.unBind() + if (DEBUG) { + Log.d(TAG, "onRelease") + } }, ) } override fun clickOnSettingsButton() { - _onLongClick() + onLongClick() } override fun getTitle(): String { + // TODO: b/377388104 make title and sub title mutable states of string + // by internetDetailsContentManager.getTitleText() + // TODO: test title change between airplane mode and not airplane mode // TODO: b/377388104 Update the placeholder text return "Internet" } override fun getSubTitle(): String { + // TODO: b/377388104 make title and sub title mutable states of string + // by internetDetailsContentManager.getSubtitleText() + // TODO: test subtitle change between airplane mode and not airplane mode // TODO: b/377388104 Update the placeholder text return "Tab a network to connect" } + + @AssistedFactory + interface Factory { + fun create(onLongClick: () -> Unit): InternetDetailsViewModel + } + + companion object { + private const val TAG = "InternetDetailsVModel" + private val DEBUG: Boolean = Log.isLoggable(TAG, Log.DEBUG) + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt new file mode 100644 index 000000000000..511597d05d37 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.dialog + +import androidx.compose.runtime.Composable +import com.android.systemui.plugins.qs.TileDetailsViewModel +import com.android.systemui.statusbar.policy.ui.dialog.composable.ModeTileGrid +import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel + +/** The view model used for the modes details view in the Quick Settings */ +class ModesDetailsViewModel( + private val onSettingsClick: () -> Unit, + private val viewModel: ModesDialogViewModel, +) : TileDetailsViewModel() { + @Composable + override fun GetContentView() { + // TODO(b/378513940): Finish implementing this function. + ModeTileGrid(viewModel = viewModel) + } + + override fun clickOnSettingsButton() { + onSettingsClick() + } + + override fun getTitle(): String { + // TODO(b/388321032): Replace this string with a string in a translatable xml file. + return "Modes" + } + + override fun getSubTitle(): String { + // TODO(b/388321032): Replace this string with a string in a translatable xml file. + return "Silences interruptions from people and apps in different circumstances" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt index c4f9515b819f..6e2c437b9c16 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt @@ -43,6 +43,7 @@ constructor( private val wifiStateWorker: WifiStateWorker, private val accessPointController: AccessPointController, private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler, + private val internetDetailsViewModelFactory: InternetDetailsViewModel.Factory, ) : QSTileUserActionInteractor<InternetTileModel> { override suspend fun handleInput(input: QSTileInput<InternetTileModel>): Unit = @@ -70,7 +71,7 @@ constructor( } override val detailsViewModel: TileDetailsViewModel = - InternetDetailsViewModel { handleLongClick(null) } + internetDetailsViewModelFactory.create { handleLongClick(null) } private fun handleLongClick(expandable:Expandable?){ qsTileIntentUserActionHandler.handle( diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt index 5ce7f0d039c8..b5da044b886a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt @@ -54,7 +54,7 @@ constructor( handleToggleClick(input.data) } is QSTileUserAction.LongClick -> { - qsTileIntentUserInputHandler.handle(action.expandable, longClickIntent) + handleLongClick(action.expandable) } } } @@ -95,6 +95,10 @@ constructor( } } + fun handleLongClick(expandable: Expandable?) { + qsTileIntentUserInputHandler.handle(expandable, longClickIntent) + } + companion object { const val TAG = "ModesTileUserActionInteractor" } diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt index 000f7f8a7d31..5bc26f50f70f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt @@ -21,7 +21,8 @@ import com.android.compose.animation.scene.Swipe import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.compose.animation.scene.UserActionResult.HideOverlay -import com.android.compose.animation.scene.UserActionResult.ReplaceByOverlay +import com.android.compose.animation.scene.UserActionResult.ShowOverlay +import com.android.compose.animation.scene.UserActionResult.ShowOverlay.HideCurrentOverlays import com.android.systemui.qs.panels.ui.viewmodel.EditModeViewModel import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge @@ -47,7 +48,11 @@ constructor(private val editModeViewModel: EditModeViewModel) : UserActionsViewM } put( Swipe.Down(fromSource = SceneContainerEdge.TopLeft), - ReplaceByOverlay(Overlays.NotificationsShade), + ShowOverlay( + Overlays.NotificationsShade, + hideCurrentOverlays = + HideCurrentOverlays.Some(Overlays.QuickSettingsShade), + ), ) } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/LauncherProxyService.java index 60c2cca1ae8b..9af4630bf492 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/LauncherProxyService.java @@ -100,14 +100,14 @@ import com.android.systemui.navigationbar.views.NavigationBar; import com.android.systemui.navigationbar.views.NavigationBarView; import com.android.systemui.navigationbar.views.buttons.KeyButtonView; import com.android.systemui.process.ProcessWrapper; -import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener; +import com.android.systemui.recents.LauncherProxyService.LauncherProxyListener; import com.android.systemui.scene.domain.interactor.SceneInteractor; import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.settings.DisplayTracker; import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.ShadeViewController; import com.android.systemui.shade.domain.interactor.ShadeInteractor; -import com.android.systemui.shared.recents.IOverviewProxy; +import com.android.systemui.shared.recents.ILauncherProxy; import com.android.systemui.shared.recents.ISystemUiProxy; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags; @@ -135,16 +135,16 @@ import javax.inject.Inject; import javax.inject.Provider; /** - * Class to send information from overview to launcher with a binder. + * Class to send information from SysUI to Launcher with a binder. */ @SysUISingleton -public class OverviewProxyService implements CallbackController<OverviewProxyListener>, +public class LauncherProxyService implements CallbackController<LauncherProxyListener>, NavigationModeController.ModeChangedListener, Dumpable { @VisibleForTesting static final String ACTION_QUICKSTEP = "android.intent.action.QUICKSTEP_SERVICE"; - public static final String TAG_OPS = "OverviewProxyService"; + public static final String TAG_OPS = "LauncherProxyService"; private static final long BACKOFF_MILLIS = 1000; private static final long DEFERRED_CALLBACK_MILLIS = 5000; // Max backoff caps at 5 mins @@ -165,7 +165,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis private final Runnable mConnectionRunnable = () -> internalConnectToCurrentUser("runnable: startConnectionToCurrentUser"); private final ComponentName mRecentsComponentName; - private final List<OverviewProxyListener> mConnectionCallbacks = new ArrayList<>(); + private final List<LauncherProxyListener> mConnectionCallbacks = new ArrayList<>(); private final Intent mQuickStepIntent; private final ScreenshotHelper mScreenshotHelper; private final CommandQueue mCommandQueue; @@ -179,12 +179,12 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis private final BroadcastDispatcher mBroadcastDispatcher; private final BackAnimation mBackAnimation; - private IOverviewProxy mOverviewProxy; + private ILauncherProxy mLauncherProxy; private int mConnectionBackoffAttempts; private boolean mBound; private boolean mIsEnabled; - // This is set to false when the overview service is requested to be bound until it is notified - // that the previous service has been cleaned up in IOverviewProxy#onUnbind(). It is also set to + // This is set to false when the launcher service is requested to be bound until it is notified + // that the previous service has been cleaned up in ILauncherProxy#onUnbind(). It is also set to // true after a 1000ms timeout by mDeferredBindAfterTimedOutCleanup. private boolean mIsPrevServiceCleanedUp = true; @@ -341,7 +341,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis @Override public void updateContextualEduStats(boolean isTrackpadGesture, String gestureType) { verifyCallerAndClearCallingIdentityPostMain("updateContextualEduStats", - () -> mHandler.post(() -> OverviewProxyService.this.updateContextualEduStats( + () -> mHandler.post(() -> LauncherProxyService.this.updateContextualEduStats( isTrackpadGesture, GestureType.valueOf(gestureType)))); } @@ -504,7 +504,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis public void onReceive(Context context, Intent intent) { if (Objects.equals(intent.getAction(), Intent.ACTION_USER_UNLOCKED)) { if (keyguardPrivateNotifications()) { - // Start the overview connection to the launcher service + // Start the launcher connection to the launcher service // Connect if user hasn't connected yet if (getProxy() == null) { startConnectionToCurrentUser(); @@ -546,14 +546,14 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis } }; - private final ServiceConnection mOverviewServiceConnection = new ServiceConnection() { + private final ServiceConnection mLauncherServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { - Log.d(TAG_OPS, "Overview proxy service connected"); + Log.d(TAG_OPS, "Launcher proxy service connected"); mConnectionBackoffAttempts = 0; mHandler.removeCallbacks(mDeferredConnectionCallback); try { - service.linkToDeath(mOverviewServiceDeathRcpt, 0); + service.linkToDeath(mLauncherServiceDeathRcpt, 0); } catch (RemoteException e) { // Failed to link to death (process may have died between binding and connecting), // just unbind the service for now and retry again @@ -564,7 +564,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis } mCurrentBoundedUserId = mUserTracker.getUserId(); - mOverviewProxy = IOverviewProxy.Stub.asInterface(service); + mLauncherProxy = ILauncherProxy.Stub.asInterface(service); Bundle params = new Bundle(); addInterface(mSysUiProxy, params); @@ -574,8 +574,8 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis mShellInterface.createExternalInterfaces(params); try { - Log.d(TAG_OPS, "OverviewProxyService connected, initializing overview proxy"); - mOverviewProxy.onInitialize(params); + Log.d(TAG_OPS, "LauncherProxyService connected, initializing launcher proxy"); + mLauncherProxy.onInitialize(params); } catch (RemoteException e) { mCurrentBoundedUserId = -1; Log.e(TAG_OPS, "Failed to call onInitialize()", e); @@ -614,7 +614,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis private final StatusBarWindowCallback mStatusBarWindowCallback = this::onStatusBarStateChanged; // This is the death handler for the binder from the launcher service - private final IBinder.DeathRecipient mOverviewServiceDeathRcpt + private final IBinder.DeathRecipient mLauncherServiceDeathRcpt = this::cleanupAfterDeath; private final IVoiceInteractionSessionListener mVoiceInteractionSessionListener = @@ -632,7 +632,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis @Override public void onVoiceSessionWindowVisibilityChanged(boolean visible) { mContext.getMainExecutor().execute(() -> - OverviewProxyService.this.onVoiceSessionWindowVisibilityChanged(visible)); + LauncherProxyService.this.onVoiceSessionWindowVisibilityChanged(visible)); } @Override @@ -652,7 +652,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis @SuppressWarnings("OptionalUsedAsFieldOrParameterType") @Inject - public OverviewProxyService(Context context, + public LauncherProxyService(Context context, @Main Executor mainExecutor, CommandQueue commandQueue, ShellInterface shellInterface, @@ -755,14 +755,14 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis @Override public void moveFocusedTaskToStageSplit(int displayId, boolean leftOrTop) { - if (mOverviewProxy != null) { + if (mLauncherProxy != null) { try { if (DesktopModeStatus.canEnterDesktopMode(mContext) && (sysUiState.getFlags() & SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE) != 0) { return; } - mOverviewProxy.enterStageSplitFromRunningApp(leftOrTop); + mLauncherProxy.enterStageSplitFromRunningApp(leftOrTop); } catch (RemoteException e) { Log.w(TAG_OPS, "Unable to enter stage split from the current running app"); } @@ -817,12 +817,12 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis private void notifySystemUiStateFlags(@SystemUiStateFlags long flags) { if (SysUiState.DEBUG) { - Log.d(TAG_OPS, "Notifying sysui state change to overview service: proxy=" - + mOverviewProxy + " flags=" + flags); + Log.d(TAG_OPS, "Notifying sysui state change to launcher service: proxy=" + + mLauncherProxy + " flags=" + flags); } try { - if (mOverviewProxy != null) { - mOverviewProxy.onSystemUiStateChanged(flags); + if (mLauncherProxy != null) { + mLauncherProxy.onSystemUiStateChanged(flags); } } catch (RemoteException e) { Log.e(TAG_OPS, "Failed to notify sysui state change", e); @@ -854,9 +854,9 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis } private void dispatchNavButtonBounds() { - if (mOverviewProxy != null && mActiveNavBarRegion != null) { + if (mLauncherProxy != null && mActiveNavBarRegion != null) { try { - mOverviewProxy.onActiveNavBarRegionChanges(mActiveNavBarRegion); + mLauncherProxy.onActiveNavBarRegionChanges(mActiveNavBarRegion); } catch (RemoteException e) { Log.e(TAG_OPS, "Failed to call onActiveNavBarRegionChanges()", e); } @@ -888,7 +888,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis // This should not happen, but if any per-user SysUI component has a dependency on OPS, // then this could get triggered Log.w(TAG_OPS, - "Skipping connection to overview service due to non-system foreground user " + "Skipping connection to launcher service due to non-system foreground user " + "caller"); return; } @@ -925,7 +925,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis } try { mBound = mContext.bindServiceAsUser(mQuickStepIntent, - mOverviewServiceConnection, + mLauncherServiceConnection, Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE, currentUser); } catch (SecurityException e) { @@ -954,15 +954,15 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis } @Override - public void addCallback(@NonNull OverviewProxyListener listener) { + public void addCallback(@NonNull LauncherProxyListener listener) { if (!mConnectionCallbacks.contains(listener)) { mConnectionCallbacks.add(listener); } - listener.onConnectionChanged(mOverviewProxy != null); + listener.onConnectionChanged(mLauncherProxy != null); } @Override - public void removeCallback(@NonNull OverviewProxyListener listener) { + public void removeCallback(@NonNull LauncherProxyListener listener) { mConnectionCallbacks.remove(listener); } @@ -974,21 +974,21 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis return mIsEnabled; } - public IOverviewProxy getProxy() { - return mOverviewProxy; + public ILauncherProxy getProxy() { + return mLauncherProxy; } private void disconnectFromLauncherService(String disconnectReason) { Log.d(TAG_OPS, "disconnectFromLauncherService bound?: " + mBound + - " currentProxy: " + mOverviewProxy + " disconnectReason: " + disconnectReason, + " currentProxy: " + mLauncherProxy + " disconnectReason: " + disconnectReason, new Throwable()); if (mBound) { // Always unbind the service (ie. if called through onNullBinding or onBindingDied) - mContext.unbindService(mOverviewServiceConnection); + mContext.unbindService(mLauncherServiceConnection); mBound = false; - if (mOverviewProxy != null) { + if (mLauncherProxy != null) { try { - mOverviewProxy.onUnbind(new IRemoteCallback.Stub() { + mLauncherProxy.onUnbind(new IRemoteCallback.Stub() { @Override public void sendResult(Bundle data) throws RemoteException { // Received Launcher reply, try to bind anew. @@ -1006,9 +1006,9 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis } } - if (mOverviewProxy != null) { - mOverviewProxy.asBinder().unlinkToDeath(mOverviewServiceDeathRcpt, 0); - mOverviewProxy = null; + if (mLauncherProxy != null) { + mLauncherProxy.asBinder().unlinkToDeath(mLauncherServiceDeathRcpt, 0); + mLauncherProxy = null; notifyConnectionChanged(); } } @@ -1044,7 +1044,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis private void notifyConnectionChanged() { for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { - mConnectionCallbacks.get(i).onConnectionChanged(mOverviewProxy != null); + mConnectionCallbacks.get(i).onConnectionChanged(mLauncherProxy != null); } } @@ -1095,10 +1095,10 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis public void notifyAssistantVisibilityChanged(float visibility) { try { - if (mOverviewProxy != null) { - mOverviewProxy.onAssistantVisibilityChanged(visibility); + if (mLauncherProxy != null) { + mLauncherProxy.onAssistantVisibilityChanged(visibility); } else { - Log.e(TAG_OPS, "Failed to get overview proxy for assistant visibility."); + Log.e(TAG_OPS, "Failed to get launcher proxy for assistant visibility."); } } catch (RemoteException e) { Log.e(TAG_OPS, "Failed to call notifyAssistantVisibilityChanged()", e); @@ -1148,10 +1148,10 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis public void disable(int displayId, int state1, int state2, boolean animate) { try { - if (mOverviewProxy != null) { - mOverviewProxy.disable(displayId, state1, state2, animate); + if (mLauncherProxy != null) { + mLauncherProxy.disable(displayId, state1, state2, animate); } else { - Log.e(TAG_OPS, "Failed to get overview proxy for disable flags."); + Log.e(TAG_OPS, "Failed to get launcher proxy for disable flags."); } } catch (RemoteException e) { Log.e(TAG_OPS, "Failed to call disable()", e); @@ -1160,10 +1160,10 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis public void onRotationProposal(int rotation, boolean isValid) { try { - if (mOverviewProxy != null) { - mOverviewProxy.onRotationProposal(rotation, isValid); + if (mLauncherProxy != null) { + mLauncherProxy.onRotationProposal(rotation, isValid); } else { - Log.e(TAG_OPS, "Failed to get overview proxy for proposing rotation."); + Log.e(TAG_OPS, "Failed to get launcher proxy for proposing rotation."); } } catch (RemoteException e) { Log.e(TAG_OPS, "Failed to call onRotationProposal()", e); @@ -1172,10 +1172,10 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis public void onSystemBarAttributesChanged(int displayId, int behavior) { try { - if (mOverviewProxy != null) { - mOverviewProxy.onSystemBarAttributesChanged(displayId, behavior); + if (mLauncherProxy != null) { + mLauncherProxy.onSystemBarAttributesChanged(displayId, behavior); } else { - Log.e(TAG_OPS, "Failed to get overview proxy for system bar attr change."); + Log.e(TAG_OPS, "Failed to get launcher proxy for system bar attr change."); } } catch (RemoteException e) { Log.e(TAG_OPS, "Failed to call onSystemBarAttributesChanged()", e); @@ -1184,10 +1184,10 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis public void onNavButtonsDarkIntensityChanged(float darkIntensity) { try { - if (mOverviewProxy != null) { - mOverviewProxy.onNavButtonsDarkIntensityChanged(darkIntensity); + if (mLauncherProxy != null) { + mLauncherProxy.onNavButtonsDarkIntensityChanged(darkIntensity); } else { - Log.e(TAG_OPS, "Failed to get overview proxy to update nav buttons dark intensity"); + Log.e(TAG_OPS, "Failed to get launcher proxy to update nav buttons dark intensity"); } } catch (RemoteException e) { Log.e(TAG_OPS, "Failed to call onNavButtonsDarkIntensityChanged()", e); @@ -1196,10 +1196,10 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis public void onNavigationBarLumaSamplingEnabled(int displayId, boolean enable) { try { - if (mOverviewProxy != null) { - mOverviewProxy.onNavigationBarLumaSamplingEnabled(displayId, enable); + if (mLauncherProxy != null) { + mLauncherProxy.onNavigationBarLumaSamplingEnabled(displayId, enable); } else { - Log.e(TAG_OPS, "Failed to get overview proxy to enable/disable nav bar luma" + Log.e(TAG_OPS, "Failed to get launcher proxy to enable/disable nav bar luma" + "sampling"); } } catch (RemoteException e) { @@ -1221,7 +1221,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis @Override public void dump(PrintWriter pw, String[] args) { pw.println(TAG_OPS + " state:"); - pw.print(" isConnected="); pw.println(mOverviewProxy != null); + pw.print(" isConnected="); pw.println(mLauncherProxy != null); pw.print(" mIsEnabled="); pw.println(isEnabled()); pw.print(" mRecentsComponentName="); pw.println(mRecentsComponentName); pw.print(" mQuickStepIntent="); pw.println(mQuickStepIntent); @@ -1237,7 +1237,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis mSysUiState.dump(pw, args); } - public interface OverviewProxyListener { + public interface LauncherProxyListener { default void onConnectionChanged(boolean isConnected) {} default void onPrioritizedRotation(@Surface.Rotation int rotation) {} default void onOverviewShown(boolean fromHome) {} diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyRecentsImpl.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyRecentsImpl.java index 21c5ae8aec40..e51b73dd96c6 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyRecentsImpl.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyRecentsImpl.java @@ -23,30 +23,30 @@ import android.util.Log; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.shared.recents.IOverviewProxy; +import com.android.systemui.shared.recents.ILauncherProxy; import com.android.systemui.statusbar.policy.KeyguardStateController; import javax.inject.Inject; /** - * An implementation of the Recents interface which proxies to the OverviewProxyService. + * An implementation of the Recents interface which proxies to the LauncherProxyService. */ @SysUISingleton public class OverviewProxyRecentsImpl implements RecentsImplementation { private final static String TAG = "OverviewProxyRecentsImpl"; private Handler mHandler; - private final OverviewProxyService mOverviewProxyService; + private final LauncherProxyService mLauncherProxyService; private final ActivityStarter mActivityStarter; private final KeyguardStateController mKeyguardStateController; @SuppressWarnings("OptionalUsedAsFieldOrParameterType") @Inject public OverviewProxyRecentsImpl( - OverviewProxyService overviewProxyService, + LauncherProxyService launcherProxyService, ActivityStarter activityStarter, KeyguardStateController keyguardStateController) { - mOverviewProxyService = overviewProxyService; + mLauncherProxyService = launcherProxyService; mActivityStarter = activityStarter; mKeyguardStateController = keyguardStateController; } @@ -58,10 +58,10 @@ public class OverviewProxyRecentsImpl implements RecentsImplementation { @Override public void showRecentApps(boolean triggeredFromAltTab) { - IOverviewProxy overviewProxy = mOverviewProxyService.getProxy(); - if (overviewProxy != null) { + ILauncherProxy launcherProxy = mLauncherProxyService.getProxy(); + if (launcherProxy != null) { try { - overviewProxy.onOverviewShown(triggeredFromAltTab); + launcherProxy.onOverviewShown(triggeredFromAltTab); } catch (RemoteException e) { Log.e(TAG, "Failed to send overview show event to launcher.", e); } @@ -70,10 +70,10 @@ public class OverviewProxyRecentsImpl implements RecentsImplementation { @Override public void hideRecentApps(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) { - IOverviewProxy overviewProxy = mOverviewProxyService.getProxy(); - if (overviewProxy != null) { + ILauncherProxy launcherProxy = mLauncherProxyService.getProxy(); + if (launcherProxy != null) { try { - overviewProxy.onOverviewHidden(triggeredFromAltTab, triggeredFromHomeKey); + launcherProxy.onOverviewHidden(triggeredFromAltTab, triggeredFromHomeKey); } catch (RemoteException e) { Log.e(TAG, "Failed to send overview hide event to launcher.", e); } @@ -83,13 +83,13 @@ public class OverviewProxyRecentsImpl implements RecentsImplementation { @Override public void toggleRecentApps() { // If connected to launcher service, let it handle the toggle logic - IOverviewProxy overviewProxy = mOverviewProxyService.getProxy(); - if (overviewProxy != null) { + ILauncherProxy launcherProxy = mLauncherProxyService.getProxy(); + if (launcherProxy != null) { final Runnable toggleRecents = () -> { try { - if (mOverviewProxyService.getProxy() != null) { - mOverviewProxyService.getProxy().onOverviewToggle(); - mOverviewProxyService.notifyToggleRecentApps(); + if (mLauncherProxyService.getProxy() != null) { + mLauncherProxyService.getProxy().onOverviewToggle(); + mLauncherProxyService.notifyToggleRecentApps(); } } catch (RemoteException e) { Log.e(TAG, "Cannot send toggle recents through proxy service.", e); diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index ecd002705c60..63c10c9b971a 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -945,10 +945,6 @@ constructor( override fun onTransitionAnimationEnd() { sceneInteractor.onTransitionAnimationEnd() } - - override fun onTransitionAnimationCancelled() { - sceneInteractor.onTransitionAnimationCancelled() - } } ) } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt index 08214c456897..f5c605211520 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt @@ -35,7 +35,6 @@ import android.util.Log import android.view.Display import android.view.ScrollCaptureResponse import android.view.ViewRootImpl.ActivityConfigCallback -import android.view.WindowManager import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE import android.widget.Toast import android.window.WindowContext @@ -218,9 +217,7 @@ internal constructor( window.setFocusable(true) viewProxy.requestFocus() - if (screenshot.type != WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE) { - enqueueScrollCaptureRequest(requestId, screenshot.userHandle) - } + enqueueScrollCaptureRequest(requestId, screenshot.userHandle) window.attachWindow() diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotCrossProfileService.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotCrossProfileService.kt index 2e6c7567259f..d82a8bd0ddcd 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotCrossProfileService.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotCrossProfileService.kt @@ -31,7 +31,7 @@ class ScreenshotCrossProfileService : Service() { private val mBinder: IBinder = object : ICrossProfileService.Stub() { - override fun launchIntent(intent: Intent, bundle: Bundle) { + override fun launchIntent(intent: Intent, bundle: Bundle?) { startActivity(intent, bundle) } } diff --git a/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java b/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java index 49f3cfc4ceaf..917869a66ca4 100644 --- a/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java +++ b/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java @@ -27,6 +27,8 @@ import android.graphics.PorterDuff; import android.graphics.PorterDuff.Mode; import android.graphics.PorterDuffColorFilter; import android.graphics.Rect; +import android.graphics.RenderEffect; +import android.graphics.Shader; import android.graphics.drawable.Drawable; import android.os.Looper; import android.util.AttributeSet; @@ -373,4 +375,18 @@ public class ScrimView extends View { ((ScrimDrawable) mDrawable).setRoundedCorners(radius); } } + + /** + * Blur the view with the specific blur radius or clear any blurs if the radius is 0 + */ + public void setBlurRadius(float blurRadius) { + if (blurRadius > 0) { + setRenderEffect(RenderEffect.createBlurEffect( + blurRadius, + blurRadius, + Shader.TileMode.CLAMP)); + } else { + setRenderEffect(null); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 19bf4c0bab81..28f5694c3332 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -109,7 +109,6 @@ import com.android.systemui.keyguard.shared.model.Edge; import com.android.systemui.keyguard.shared.model.TransitionState; import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.keyguard.ui.binder.KeyguardLongPressViewBinder; -import com.android.systemui.keyguard.ui.transitions.BlurConfig; import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel; import com.android.systemui.keyguard.ui.viewmodel.KeyguardTouchHandlingViewModel; import com.android.systemui.media.controls.domain.pipeline.MediaDataManager; @@ -915,8 +914,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump if (!com.android.systemui.Flags.bouncerUiRevamp()) return; if (isBouncerShowing && isExpanded()) { - float shadeBlurEffect = BlurConfig.maxBlurRadiusToNotificationPanelBlurRadius( - mDepthController.getMaxBlurRadiusPx()); + float shadeBlurEffect = mDepthController.getMaxBlurRadiusPx(); mView.setRenderEffect(RenderEffect.createBlurEffect( shadeBlurEffect, shadeBlurEffect, @@ -2253,7 +2251,8 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } private boolean isPanelVisibleBecauseOfHeadsUp() { - boolean headsUpVisible = mHeadsUpManager.hasPinnedHeadsUp() || mHeadsUpAnimatingAway; + boolean headsUpVisible = (mHeadsUpManager != null && mHeadsUpManager.hasPinnedHeadsUp()) + || mHeadsUpAnimatingAway; return headsUpVisible && mBarState == StatusBarState.SHADE; } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java index 48bbb0407ee3..48e374746bf5 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java @@ -17,6 +17,7 @@ package com.android.systemui.shade; import static android.os.Trace.TRACE_TAG_APP; +import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED; import static com.android.systemui.Flags.enableViewCaptureTracing; import static com.android.systemui.statusbar.phone.CentralSurfaces.DEBUG; @@ -49,10 +50,12 @@ import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowInsetsController; +import android.view.accessibility.AccessibilityEvent; import com.android.app.viewcapture.ViewCaptureFactory; import com.android.internal.view.FloatingActionMode; import com.android.internal.widget.floatingtoolbar.FloatingToolbar; +import com.android.systemui.Flags; import com.android.systemui.scene.ui.view.WindowRootView; import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround; import com.android.systemui.statusbar.phone.ConfigurationForwarder; @@ -77,6 +80,8 @@ public class NotificationShadeWindowView extends WindowRootView { private SafeCloseable mViewCaptureCloseable; + private boolean mAnimatingContentLaunch = false; + public NotificationShadeWindowView(Context context, AttributeSet attrs) { super(context, attrs); setMotionEventSplittingEnabled(false); @@ -188,6 +193,22 @@ public class NotificationShadeWindowView extends WindowRootView { } } + @Override + public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event) { + if (Flags.shadeLaunchAccessibility() && mAnimatingContentLaunch + && event.getEventType() == TYPE_VIEW_ACCESSIBILITY_FOCUSED) { + // Block accessibility focus events during launch animations to avoid stray TalkBack + // announcements. + return false; + } + + return super.requestSendAccessibilityEvent(child, event); + } + + public void setAnimatingContentLaunch(boolean animating) { + mAnimatingContentLaunch = animating; + } + public void setConfigurationForwarder(ConfigurationForwarder configurationForwarder) { ShadeWindowGoesAround.isUnexpectedlyInLegacyMode(); mConfigurationForwarder = configurationForwarder; diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index e5dcd2338b9d..ffec8f284c48 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -16,10 +16,12 @@ package com.android.systemui.shade; +import static com.android.systemui.Flags.shadeLaunchAccessibility; import static com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING; import static com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; +import static com.android.systemui.util.kotlin.JavaAdapterKt.combineFlows; import android.app.StatusBarManager; import android.util.Log; @@ -59,6 +61,7 @@ import com.android.systemui.res.R; import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.settings.brightness.domain.interactor.BrightnessMirrorShowingInteractor; import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor; +import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractor; import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround; import com.android.systemui.shared.animation.DisableSubpixelTextTransitionListener; import com.android.systemui.statusbar.BlurUtils; @@ -85,6 +88,7 @@ import com.android.systemui.window.ui.WindowRootViewBinder; import com.android.systemui.window.ui.viewmodel.WindowRootViewModel; import kotlinx.coroutines.ExperimentalCoroutinesApi; +import kotlinx.coroutines.flow.Flow; import java.io.PrintWriter; import java.util.Optional; @@ -174,6 +178,7 @@ public class NotificationShadeWindowViewController implements Dumpable { NotificationShadeDepthController depthController, NotificationShadeWindowView notificationShadeWindowView, ShadeViewController shadeViewController, + ShadeAnimationInteractor shadeAnimationInteractor, PanelExpansionInteractor panelExpansionInteractor, ShadeExpansionStateManager shadeExpansionStateManager, NotificationStackScrollLayoutController notificationStackScrollLayoutController, @@ -238,9 +243,17 @@ public class NotificationShadeWindowViewController implements Dumpable { collectFlow(mView, keyguardTransitionInteractor.transition( Edge.create(LOCKSCREEN, DREAMING)), mLockscreenToDreamingTransition); + Flow<Boolean> isLaunchAnimationRunning = + shadeLaunchAccessibility() + ? combineFlows( + notificationLaunchAnimationInteractor.isLaunchAnimationRunning(), + shadeAnimationInteractor.isLaunchingActivity(), + (notificationLaunching, shadeLaunching) -> + notificationLaunching || shadeLaunching) + : notificationLaunchAnimationInteractor.isLaunchAnimationRunning(); collectFlow( mView, - notificationLaunchAnimationInteractor.isLaunchAnimationRunning(), + isLaunchAnimationRunning, this::setExpandAnimationRunning); if (QSComposeFragment.isEnabled()) { collectFlow(mView, @@ -726,9 +739,17 @@ public class NotificationShadeWindowViewController implements Dumpable { if (ActivityTransitionAnimator.DEBUG_TRANSITION_ANIMATION) { Log.d(TAG, "Setting mExpandAnimationRunning=" + running); } + if (running) { mLaunchAnimationTimeout = mClock.uptimeMillis() + 5000; } + + if (shadeLaunchAccessibility()) { + // The view needs to know when an animation is ongoing so it can intercept + // unnecessary accessibility events. + mView.setAnimatingContentLaunch(running); + } + mExpandAnimationRunning = running; mNotificationShadeWindowController.setLaunchingActivity(mExpandAnimationRunning); } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt index 7299f092640f..cf310dd32613 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt @@ -34,8 +34,8 @@ import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.navigationbar.NavigationModeController import com.android.systemui.plugins.qs.QS import com.android.systemui.plugins.qs.QSContainerController -import com.android.systemui.recents.OverviewProxyService -import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener +import com.android.systemui.recents.LauncherProxyService +import com.android.systemui.recents.LauncherProxyService.LauncherProxyListener import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shared.system.QuickStepContract @@ -57,7 +57,7 @@ class NotificationsQSContainerController constructor( view: NotificationsQuickSettingsContainer, private val navigationModeController: NavigationModeController, - private val overviewProxyService: OverviewProxyService, + private val launcherProxyService: LauncherProxyService, private val shadeHeaderController: ShadeHeaderController, private val shadeInteractor: ShadeInteractor, private val fragmentService: FragmentService, @@ -85,8 +85,8 @@ constructor( private var isGestureNavigation = true private var taskbarVisible = false - private val taskbarVisibilityListener: OverviewProxyListener = - object : OverviewProxyListener { + private val taskbarVisibilityListener: LauncherProxyListener = + object : LauncherProxyListener { override fun onTaskbarStatusUpdated(visible: Boolean, stashed: Boolean) { taskbarVisible = visible } @@ -134,7 +134,7 @@ constructor( public override fun onViewAttached() { updateResources() - overviewProxyService.addCallback(taskbarVisibilityListener) + launcherProxyService.addCallback(taskbarVisibilityListener) mView.setInsetsChangedListener(delayedInsetSetter) mView.setQSFragmentAttachedListener { qs: QS -> qs.setContainerController(this) } mView.setConfigurationChangedListener { updateResources() } @@ -142,7 +142,7 @@ constructor( } override fun onViewDetached() { - overviewProxyService.removeCallback(taskbarVisibilityListener) + launcherProxyService.removeCallback(taskbarVisibilityListener) mView.removeOnInsetsChangedListener() mView.removeQSFragmentAttachedListener() mView.setConfigurationChangedListener(null) diff --git a/packages/SystemUI/src/com/android/systemui/shade/QsBatteryModeController.kt b/packages/SystemUI/src/com/android/systemui/shade/QsBatteryModeController.kt index 7a70966c2b12..b15615a83698 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QsBatteryModeController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/QsBatteryModeController.kt @@ -5,6 +5,7 @@ import android.view.DisplayCutout import com.android.systemui.battery.BatteryMeterView import com.android.systemui.res.R import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore +import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider import javax.inject.Inject /** @@ -15,11 +16,9 @@ class QsBatteryModeController @Inject constructor( @ShadeDisplayAware private val context: Context, - insetsProviderStore: StatusBarContentInsetsProviderStore, + private val insetsProviderStore: StatusBarContentInsetsProviderStore, ) { - private val insetsProvider = insetsProviderStore.defaultDisplay - private companion object { // MotionLayout frames are in [0, 100]. Where 0 and 100 are reserved for start and end // frames. @@ -43,17 +42,19 @@ constructor( * animation. */ @BatteryMeterView.BatteryPercentMode - fun getBatteryMode(cutout: DisplayCutout?, qsExpandedFraction: Float): Int? = - when { + fun getBatteryMode(cutout: DisplayCutout?, qsExpandedFraction: Float): Int? { + val insetsProvider = insetsProviderStore.forDisplay(context.displayId) + return when { qsExpandedFraction > fadeInStartFraction -> BatteryMeterView.MODE_ESTIMATE - qsExpandedFraction < fadeOutCompleteFraction -> - if (hasCenterCutout(cutout)) { + insetsProvider != null && qsExpandedFraction < fadeOutCompleteFraction -> + if (hasCenterCutout(cutout, insetsProvider)) { BatteryMeterView.MODE_ON } else { BatteryMeterView.MODE_ESTIMATE } else -> null } + } fun updateResources() { fadeInStartFraction = @@ -64,7 +65,10 @@ constructor( MOTION_LAYOUT_MAX_FRAME.toFloat() } - private fun hasCenterCutout(cutout: DisplayCutout?): Boolean = + private fun hasCenterCutout( + cutout: DisplayCutout?, + insetsProvider: StatusBarContentInsetsProvider, + ): Boolean = cutout?.let { !insetsProvider.currentRotationHasCornerCutout() && !it.boundingRectTop.isEmpty } ?: false diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt index 747642097327..f926d39760fe 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt @@ -36,7 +36,6 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.res.R import com.android.systemui.scene.ui.view.WindowRootView -import com.android.systemui.shade.data.repository.MutableShadeDisplaysRepository import com.android.systemui.shade.data.repository.ShadeDisplaysRepository import com.android.systemui.shade.data.repository.ShadeDisplaysRepositoryImpl import com.android.systemui.shade.display.ShadeDisplayPolicyModule @@ -211,15 +210,6 @@ object ShadeDisplayAwareModule { return impl } - @SysUISingleton - @Provides - fun provideMutableShadePositionRepository( - impl: ShadeDisplaysRepositoryImpl - ): MutableShadeDisplaysRepository { - ShadeWindowGoesAround.isUnexpectedlyInLegacyMode() - return impl - } - @Provides @SysUISingleton fun provideShadeDialogContextInteractor( diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt index e8a792c30aa2..fa40aa2bad24 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt @@ -57,6 +57,7 @@ import com.android.systemui.shade.ShadeHeaderController.Companion.QS_HEADER_CONS import com.android.systemui.shade.ShadeViewProviderModule.Companion.SHADE_HEADER import com.android.systemui.shade.carrier.ShadeCarrierGroup import com.android.systemui.shade.carrier.ShadeCarrierGroupController +import com.android.systemui.shade.data.repository.ShadeDisplaysRepository import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.phone.StatusIconContainer @@ -90,8 +91,9 @@ constructor( private val statusBarIconController: StatusBarIconController, private val tintedIconManagerFactory: TintedIconManager.Factory, private val privacyIconsController: HeaderPrivacyIconsController, - private val insetsProviderStore: StatusBarContentInsetsProviderStore, + private val statusBarContentInsetsProviderStore: StatusBarContentInsetsProviderStore, @ShadeDisplayAware private val configurationController: ConfigurationController, + private val shadeDisplaysRepository: ShadeDisplaysRepository, private val variableDateViewControllerFactory: VariableDateViewController.Factory, @Named(SHADE_HEADER) private val batteryMeterViewController: BatteryMeterViewController, private val dumpManager: DumpManager, @@ -104,7 +106,9 @@ constructor( private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory, ) : ViewController<View>(header), Dumpable { - private val insetsProvider = insetsProviderStore.defaultDisplay + private val statusBarContentInsetsProvider + get() = + statusBarContentInsetsProviderStore.forDisplay(shadeDisplaysRepository.displayId.value) companion object { /** IDs for transitions and constraints for the [MotionLayout]. */ @@ -222,10 +226,14 @@ constructor( private val insetListener = View.OnApplyWindowInsetsListener { view, insets -> - updateConstraintsForInsets(view as MotionLayout, insets) - lastInsets = WindowInsets(insets) - - view.onApplyWindowInsets(insets) + val windowInsets = WindowInsets(insets) + if (windowInsets != lastInsets) { + updateConstraintsForInsets(view as MotionLayout, insets) + lastInsets = windowInsets + view.onApplyWindowInsets(insets) + } else { + insets + } } private var singleCarrier = false @@ -285,7 +293,7 @@ constructor( override fun onDensityOrFontScaleChanged() { clock.setTextAppearance(R.style.TextAppearance_QS_Status) date.setTextAppearance(R.style.TextAppearance_QS_Status) - mShadeCarrierGroup.updateTextAppearance(R.style.TextAppearance_QS_Status_Carriers) + mShadeCarrierGroup.updateTextAppearance(R.style.TextAppearance_QS_Status) loadConstraints() header.minHeight = resources.getDimensionPixelSize(R.dimen.large_screen_shade_header_min_height) @@ -414,6 +422,7 @@ constructor( } private fun updateConstraintsForInsets(view: MotionLayout, insets: WindowInsets) { + val insetsProvider = statusBarContentInsetsProvider ?: return val cutout = insets.displayCutout.also { this.cutout = it } val sbInsets: Insets = insetsProvider.getStatusBarContentInsetsForCurrentRotation() @@ -508,6 +517,9 @@ constructor( systemIconsHoverContainer.setOnClickListener(null) systemIconsHoverContainer.isClickable = false } + + lastInsets?.let { updateConstraintsForInsets(header, it) } + header.jumpToState(header.startState) updatePosition() updateScrollY() diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt index 7bfe40c3d811..173da336c62f 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt @@ -20,7 +20,7 @@ import android.provider.Settings.Global.DEVELOPMENT_SHADE_DISPLAY_AWARENESS import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.display.data.repository.DisplayRepository -import com.android.systemui.shade.data.repository.MutableShadeDisplaysRepository +import com.android.systemui.shade.data.repository.ShadeDisplaysRepository import com.android.systemui.shade.display.ShadeDisplayPolicy import com.android.systemui.statusbar.commandline.Command import com.android.systemui.statusbar.commandline.CommandRegistry @@ -35,7 +35,7 @@ constructor( private val globalSettings: GlobalSettings, private val commandRegistry: CommandRegistry, private val displaysRepository: DisplayRepository, - private val positionRepository: MutableShadeDisplaysRepository, + private val positionRepository: ShadeDisplaysRepository, private val policies: Set<@JvmSuppressWildcards ShadeDisplayPolicy>, private val defaultPolicy: ShadeDisplayPolicy, ) : Command, CoreStartable { @@ -103,7 +103,7 @@ constructor( } private fun printPolicies() { - val currentPolicyName = positionRepository.policy.value.name + val currentPolicyName = positionRepository.currentPolicy.name pw.println("Available policies: ") policies.forEach { pw.print(" - ${it.name}") diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/FakeShadeDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/FakeShadeDisplayRepository.kt index 732d4d1500e7..3513334f2a5c 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/FakeShadeDisplayRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/FakeShadeDisplayRepository.kt @@ -17,6 +17,8 @@ package com.android.systemui.shade.data.repository import android.view.Display +import com.android.systemui.shade.display.FakeShadeDisplayPolicy +import com.android.systemui.shade.display.ShadeDisplayPolicy import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -30,7 +32,6 @@ class FakeShadeDisplayRepository : ShadeDisplaysRepository { override val displayId: StateFlow<Int> get() = _displayId - fun resetDisplayId() { - _displayId.value = Display.DEFAULT_DISPLAY - } + override val currentPolicy: ShadeDisplayPolicy + get() = FakeShadeDisplayPolicy } diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt index af48231e0a99..f959f7fe0c31 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt @@ -20,14 +20,18 @@ import android.provider.Settings.Global.DEVELOPMENT_SHADE_DISPLAY_AWARENESS import android.view.Display import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.keyguard.data.repository.KeyguardRepository +import com.android.systemui.shade.ShadeOnDefaultDisplayWhenLocked import com.android.systemui.shade.display.ShadeDisplayPolicy import com.android.systemui.util.settings.GlobalSettings import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map @@ -38,12 +42,8 @@ import kotlinx.coroutines.flow.stateIn interface ShadeDisplaysRepository { /** ID of the display which currently hosts the shade */ val displayId: StateFlow<Int> -} - -/** Allows to change the policy that determines in which display the Shade window is visible. */ -interface MutableShadeDisplaysRepository : ShadeDisplaysRepository { - /** Updates the policy to select where the shade is visible. */ - val policy: StateFlow<ShadeDisplayPolicy> + /** The current policy set. */ + val currentPolicy: ShadeDisplayPolicy } /** Keeps the policy and propagates the display id for the shade from it. */ @@ -56,9 +56,11 @@ constructor( defaultPolicy: ShadeDisplayPolicy, @Background bgScope: CoroutineScope, policies: Set<@JvmSuppressWildcards ShadeDisplayPolicy>, -) : MutableShadeDisplaysRepository { + @ShadeOnDefaultDisplayWhenLocked private val shadeOnDefaultDisplayWhenLocked: Boolean, + keyguardRepository: KeyguardRepository, +) : ShadeDisplaysRepository { - override val policy: StateFlow<ShadeDisplayPolicy> = + private val policy: StateFlow<ShadeDisplayPolicy> = globalSettings .observerFlow(DEVELOPMENT_SHADE_DISPLAY_AWARENESS) .onStart { emit(Unit) } @@ -71,10 +73,32 @@ constructor( return@map defaultPolicy } .distinctUntilChanged() - .stateIn(bgScope, SharingStarted.WhileSubscribed(), defaultPolicy) + .stateIn(bgScope, SharingStarted.Eagerly, defaultPolicy) + + private val displayIdFromPolicy: Flow<Int> = policy.flatMapLatest { it.displayId } + + private val keyguardAwareDisplayPolicy: Flow<Int> = + if (!shadeOnDefaultDisplayWhenLocked) { + displayIdFromPolicy + } else { + keyguardRepository.isKeyguardShowing.combine(displayIdFromPolicy) { + isKeyguardShowing, + currentDisplayId -> + if (isKeyguardShowing) { + Display.DEFAULT_DISPLAY + } else { + currentDisplayId + } + } + } + + override val currentPolicy: ShadeDisplayPolicy + get() = policy.value override val displayId: StateFlow<Int> = - policy - .flatMapLatest { it.displayId } - .stateIn(bgScope, SharingStarted.WhileSubscribed(), Display.DEFAULT_DISPLAY) + keyguardAwareDisplayPolicy.stateIn( + bgScope, + SharingStarted.WhileSubscribed(), + Display.DEFAULT_DISPLAY, + ) } diff --git a/packages/SystemUI/src/com/android/systemui/shade/display/FakeShadeDisplayPolicy.kt b/packages/SystemUI/src/com/android/systemui/shade/display/FakeShadeDisplayPolicy.kt new file mode 100644 index 000000000000..e010bd6f9880 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/display/FakeShadeDisplayPolicy.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade.display + +import android.view.Display +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +/** Used only for testing. */ +object FakeShadeDisplayPolicy : ShadeDisplayPolicy { + override val name: String + get() = "fake_shade_policy" + + override val displayId: StateFlow<Int> + get() = _displayId + + private val _displayId = MutableStateFlow(Display.DEFAULT_DISPLAY) + + fun setDisplayId(displayId: Int) { + _displayId.value = displayId + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/display/FocusShadeDisplayPolicy.kt b/packages/SystemUI/src/com/android/systemui/shade/display/FocusShadeDisplayPolicy.kt new file mode 100644 index 000000000000..7d8f7c59ad66 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/display/FocusShadeDisplayPolicy.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade.display + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.display.data.repository.FocusedDisplayRepository +import javax.inject.Inject +import kotlinx.coroutines.flow.StateFlow + +/** Policy that just emits the [FocusedDisplayRepository] display id. */ +@SysUISingleton +class FocusShadeDisplayPolicy +@Inject +constructor(private val focusedDisplayRepository: FocusedDisplayRepository) : ShadeDisplayPolicy { + override val name: String + get() = "focused_display" + + override val displayId: StateFlow<Int> + get() = focusedDisplayRepository.focusedDisplayId +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/display/ShadeDisplayPolicy.kt b/packages/SystemUI/src/com/android/systemui/shade/display/ShadeDisplayPolicy.kt index bf5deff5cff5..677e41a47afe 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/display/ShadeDisplayPolicy.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/display/ShadeDisplayPolicy.kt @@ -19,7 +19,8 @@ package com.android.systemui.shade.display import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractor.ShadeElement import dagger.Binds import dagger.Module -import dagger.multibindings.IntoSet +import dagger.Provides +import dagger.multibindings.ElementsIntoSet import kotlinx.coroutines.flow.StateFlow /** Describes the display the shade should be shown in. */ @@ -53,27 +54,25 @@ interface ShadeExpansionIntent { fun consumeExpansionIntent(): ShadeElement? } -@Module +@Module(includes = [AllShadeDisplayPoliciesModule::class]) interface ShadeDisplayPolicyModule { @Binds fun provideDefaultPolicy(impl: DefaultDisplayShadePolicy): ShadeDisplayPolicy @Binds fun provideShadeExpansionIntent(impl: StatusBarTouchShadeDisplayPolicy): ShadeExpansionIntent +} - @IntoSet - @Binds - fun provideDefaultDisplayPolicyToSet(impl: DefaultDisplayShadePolicy): ShadeDisplayPolicy - - @IntoSet - @Binds - fun provideAnyExternalShadeDisplayPolicyToSet( - impl: AnyExternalShadeDisplayPolicy - ): ShadeDisplayPolicy - - @Binds - @IntoSet - fun provideStatusBarTouchShadeDisplayPolicy( - impl: StatusBarTouchShadeDisplayPolicy - ): ShadeDisplayPolicy +@Module +internal object AllShadeDisplayPoliciesModule { + @Provides + @ElementsIntoSet + fun provideShadeDisplayPolicies( + defaultPolicy: DefaultDisplayShadePolicy, + externalPolicy: AnyExternalShadeDisplayPolicy, + statusBarPolicy: StatusBarTouchShadeDisplayPolicy, + focusPolicy: FocusShadeDisplayPolicy, + ): Set<ShadeDisplayPolicy> { + return setOf(defaultPolicy, externalPolicy, statusBarPolicy, focusPolicy) + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt b/packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt index 91020aa7bdb0..b155ada87efd 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt @@ -23,8 +23,6 @@ import com.android.app.tracing.coroutines.launchTraced import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.display.data.repository.DisplayRepository -import com.android.systemui.keyguard.data.repository.KeyguardRepository -import com.android.systemui.shade.ShadeOnDefaultDisplayWhenLocked import com.android.systemui.shade.domain.interactor.NotificationShadeElement import com.android.systemui.shade.domain.interactor.QSShadeElement import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractor.ShadeElement @@ -38,13 +36,10 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn /** * Moves the shade on the last display that received a status bar touch. @@ -57,9 +52,7 @@ class StatusBarTouchShadeDisplayPolicy @Inject constructor( displayRepository: DisplayRepository, - keyguardRepository: KeyguardRepository, @Background private val backgroundScope: CoroutineScope, - @ShadeOnDefaultDisplayWhenLocked private val shadeOnDefaultDisplayWhenLocked: Boolean, private val shadeInteractor: Lazy<ShadeInteractor>, private val qsShadeElement: Lazy<QSShadeElement>, private val notificationElement: Lazy<NotificationShadeElement>, @@ -72,20 +65,7 @@ constructor( private var latestIntent = AtomicReference<ShadeElement?>() private var timeoutJob: Job? = null - override val displayId: StateFlow<Int> = - if (shadeOnDefaultDisplayWhenLocked) { - keyguardRepository.isKeyguardShowing - .combine(currentDisplayId) { isKeyguardShowing, currentDisplayId -> - if (isKeyguardShowing) { - Display.DEFAULT_DISPLAY - } else { - currentDisplayId - } - } - .stateIn(backgroundScope, SharingStarted.WhileSubscribed(), currentDisplayId.value) - } else { - currentDisplayId - } + override val displayId: StateFlow<Int> = currentDisplayId private var removalListener: Job? = null diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt index edf503d03f3e..59d812403777 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt @@ -16,17 +16,20 @@ package com.android.systemui.shade.domain.interactor +import android.provider.Settings import androidx.annotation.FloatRange import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.shade.shared.model.ShadeMode +import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn /** @@ -76,29 +79,53 @@ interface ShadeModeInteractor { class ShadeModeInteractorImpl @Inject -constructor(@Application applicationScope: CoroutineScope, repository: ShadeRepository) : - ShadeModeInteractor { +constructor( + @Application applicationScope: CoroutineScope, + repository: ShadeRepository, + secureSettingsRepository: SecureSettingsRepository, +) : ShadeModeInteractor { + + private val isDualShadeEnabled: Flow<Boolean> = + secureSettingsRepository.boolSetting( + Settings.Secure.DUAL_SHADE, + defaultValue = DUAL_SHADE_ENABLED_DEFAULT, + ) override val isShadeLayoutWide: StateFlow<Boolean> = repository.isShadeLayoutWide override val shadeMode: StateFlow<ShadeMode> = - isShadeLayoutWide - .map(this::determineShadeMode) + combine(isDualShadeEnabled, repository.isShadeLayoutWide, ::determineShadeMode) .stateIn( applicationScope, SharingStarted.Eagerly, - initialValue = determineShadeMode(isShadeLayoutWide.value), + initialValue = + determineShadeMode( + isDualShadeEnabled = DUAL_SHADE_ENABLED_DEFAULT, + isShadeLayoutWide = repository.isShadeLayoutWide.value, + ), ) @FloatRange(from = 0.0, to = 1.0) override fun getTopEdgeSplitFraction(): Float = 0.5f - private fun determineShadeMode(isShadeLayoutWide: Boolean): ShadeMode { + private fun determineShadeMode( + isDualShadeEnabled: Boolean, + isShadeLayoutWide: Boolean, + ): ShadeMode { return when { - DualShade.isEnabled -> ShadeMode.Dual + isDualShadeEnabled || + // TODO(b/388793191): This ensures that the dual_shade aconfig flag can also enable + // Dual Shade, to avoid breaking unit tests. Remove this once all references to the + // flag are removed. + DualShade.isEnabled -> ShadeMode.Dual isShadeLayoutWide -> ShadeMode.Split else -> ShadeMode.Single } } + + companion object { + /* Whether the Dual Shade setting is enabled by default. */ + private const val DUAL_SHADE_ENABLED_DEFAULT = false + } } class ShadeModeInteractorEmptyImpl @Inject constructor() : ShadeModeInteractor { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index 7fc1510f1136..dcea8d85e10d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -273,12 +273,12 @@ public class CommandQueue extends IStatusBar.Stub implements default void toggleQuickSettingsPanel() { } /** - * Called to notify IME window status changes. + * Sets the new IME window status. * - * @param displayId The id of the display to notify. - * @param vis IME visibility. - * @param backDisposition Disposition mode of back button. - * @param showImeSwitcher {@code true} to show IME switch button. + * @param displayId The id of the display to which the IME is bound. + * @param vis The IME window visibility. + * @param backDisposition The IME back disposition mode. + * @param showImeSwitcher Whether the IME Switcher button should be shown. */ default void setImeWindowStatus(int displayId, @ImeWindowVisibility int vis, @BackDispositionMode int backDisposition, boolean showImeSwitcher) { } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index a5595edcbb95..4269f60e1c2a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -88,6 +88,7 @@ import com.android.keyguard.TrustGrantFlags; import com.android.keyguard.logging.KeyguardLogger; import com.android.settingslib.Utils; import com.android.settingslib.fuelgauge.BatteryStatus; +import com.android.systemui.Flags; import com.android.systemui.biometrics.AuthController; import com.android.systemui.biometrics.FaceHelpMessageDeferral; import com.android.systemui.biometrics.FaceHelpMessageDeferralFactory; @@ -199,7 +200,7 @@ public class KeyguardIndicationController { private CharSequence mBiometricMessage; private CharSequence mBiometricMessageFollowUp; private BiometricSourceType mBiometricMessageSource; - protected ColorStateList mInitialTextColorState; + private ColorStateList mInitialTextColorState; private boolean mVisible; private boolean mOrganizationOwnedDevice; @@ -393,13 +394,27 @@ public class KeyguardIndicationController { return mIndicationArea; } + /** + * Notify controller about configuration changes. + */ + public void onConfigurationChanged() { + // Get new text color in case theme has changed + if (Flags.indicationTextA11yFix()) { + setIndicationColorToThemeColor(); + } + } + public void setIndicationArea(ViewGroup indicationArea) { mIndicationArea = indicationArea; mTopIndicationView = indicationArea.findViewById(R.id.keyguard_indication_text); mLockScreenIndicationView = indicationArea.findViewById( R.id.keyguard_indication_text_bottom); - mInitialTextColorState = mTopIndicationView != null - ? mTopIndicationView.getTextColors() : ColorStateList.valueOf(Color.WHITE); + if (Flags.indicationTextA11yFix()) { + setIndicationColorToThemeColor(); + } else { + setIndicationTextColor(mTopIndicationView != null + ? mTopIndicationView.getTextColors() : ColorStateList.valueOf(Color.WHITE)); + } if (mRotateTextViewController != null) { mRotateTextViewController.destroy(); } @@ -436,6 +451,12 @@ public class KeyguardIndicationController { mIsLogoutEnabledCallback); } + @NonNull + private ColorStateList wallpaperTextColor() { + return ColorStateList.valueOf( + Utils.getColorAttrDefaultColor(mContext, R.attr.wallpaperTextColor)); + } + /** * Cleanup */ @@ -513,7 +534,7 @@ public class KeyguardIndicationController { .setMessage(mContext.getResources().getString( com.android.systemui.res.R.string.dismissible_keyguard_swipe) ) - .setTextColor(mInitialTextColorState) + .setTextColor(getInitialTextColorState()) .build(), /* updateImmediately */ true); } else { @@ -533,7 +554,7 @@ public class KeyguardIndicationController { INDICATION_TYPE_DISCLOSURE, new KeyguardIndication.Builder() .setMessage(disclosure) - .setTextColor(mInitialTextColorState) + .setTextColor(getInitialTextColorState()) .build(), /* updateImmediately */ false); } @@ -602,7 +623,7 @@ public class KeyguardIndicationController { INDICATION_TYPE_OWNER_INFO, new KeyguardIndication.Builder() .setMessage(finalInfo) - .setTextColor(mInitialTextColorState) + .setTextColor(getInitialTextColorState()) .build(), false); } else { @@ -624,7 +645,7 @@ public class KeyguardIndicationController { INDICATION_TYPE_BATTERY, new KeyguardIndication.Builder() .setMessage(powerIndication) - .setTextColor(mInitialTextColorState) + .setTextColor(getInitialTextColorState()) .build(), animate); } else { @@ -645,7 +666,7 @@ public class KeyguardIndicationController { new KeyguardIndication.Builder() .setMessage(mContext.getResources().getText( com.android.internal.R.string.lockscreen_storage_locked)) - .setTextColor(mInitialTextColorState) + .setTextColor(getInitialTextColorState()) .build(), false); } else { @@ -666,7 +687,7 @@ public class KeyguardIndicationController { .setMessage(mBiometricMessage) .setForceAccessibilityLiveRegionAssertive() .setMinVisibilityMillis(IMPORTANT_MSG_MIN_DURATION) - .setTextColor(mInitialTextColorState) + .setTextColor(getInitialTextColorState()) .build(), true ); @@ -680,7 +701,7 @@ public class KeyguardIndicationController { new KeyguardIndication.Builder() .setMessage(mBiometricMessageFollowUp) .setMinVisibilityMillis(IMPORTANT_MSG_MIN_DURATION) - .setTextColor(mInitialTextColorState) + .setTextColor(getInitialTextColorState()) .build(), true ); @@ -711,7 +732,7 @@ public class KeyguardIndicationController { INDICATION_TYPE_TRUST, new KeyguardIndication.Builder() .setMessage(trustGrantedIndication) - .setTextColor(mInitialTextColorState) + .setTextColor(getInitialTextColorState()) .build(), true); hideBiometricMessage(); @@ -722,7 +743,7 @@ public class KeyguardIndicationController { INDICATION_TYPE_TRUST, new KeyguardIndication.Builder() .setMessage(trustManagedIndication) - .setTextColor(mInitialTextColorState) + .setTextColor(getInitialTextColorState()) .build(), false); } else { @@ -751,7 +772,7 @@ public class KeyguardIndicationController { INDICATION_TYPE_PERSISTENT_UNLOCK_MESSAGE, new KeyguardIndication.Builder() .setMessage(mPersistentUnlockMessage) - .setTextColor(mInitialTextColorState) + .setTextColor(getInitialTextColorState()) .build(), true); } else { @@ -792,7 +813,7 @@ public class KeyguardIndicationController { new KeyguardIndication.Builder() .setMessage(mContext.getString( R.string.keyguard_indication_after_adaptive_auth_lock)) - .setTextColor(mInitialTextColorState) + .setTextColor(getInitialTextColorState()) .build(), true); } else { @@ -1179,7 +1200,8 @@ public class KeyguardIndicationController { } else { message = mContext.getString(R.string.keyguard_retry); } - mStatusBarKeyguardViewManager.setKeyguardMessage(message, mInitialTextColorState, + mStatusBarKeyguardViewManager.setKeyguardMessage(message, + getInitialTextColorState(), null); } } else { @@ -1232,7 +1254,7 @@ public class KeyguardIndicationController { public void dump(PrintWriter pw, String[] args) { pw.println("KeyguardIndicationController:"); - pw.println(" mInitialTextColorState: " + mInitialTextColorState); + pw.println(" mInitialTextColorState: " + getInitialTextColorState()); pw.println(" mPowerPluggedInWired: " + mPowerPluggedInWired); pw.println(" mPowerPluggedIn: " + mPowerPluggedIn); pw.println(" mPowerCharged: " + mPowerCharged); @@ -1253,6 +1275,22 @@ public class KeyguardIndicationController { mRotateTextViewController.dump(pw, args); } + protected ColorStateList getInitialTextColorState() { + return mInitialTextColorState; + } + + private void setIndicationColorToThemeColor() { + mInitialTextColorState = wallpaperTextColor(); + } + + /** + * @deprecated Use {@link #setIndicationColorToThemeColor} + */ + @Deprecated + private void setIndicationTextColor(ColorStateList color) { + mInitialTextColorState = color; + } + protected class BaseKeyguardCallback extends KeyguardUpdateMonitorCallback { @Override public void onTimeChanged() { @@ -1358,7 +1396,7 @@ public class KeyguardIndicationController { mBouncerMessageInteractor.setFaceAcquisitionMessage(helpString); } mStatusBarKeyguardViewManager.setKeyguardMessage(helpString, - mInitialTextColorState, biometricSourceType); + getInitialTextColorState(), biometricSourceType); } else if (mScreenLifecycle.getScreenState() == SCREEN_ON) { if (isCoExFaceAcquisitionMessage && msgId == FACE_ACQUIRED_TOO_DARK) { showBiometricMessage( @@ -1655,7 +1693,7 @@ public class KeyguardIndicationController { private void showErrorMessageNowOrLater(String errString, @Nullable String followUpMsg, BiometricSourceType biometricSourceType) { if (mStatusBarKeyguardViewManager.isBouncerShowing()) { - mStatusBarKeyguardViewManager.setKeyguardMessage(errString, mInitialTextColorState, + mStatusBarKeyguardViewManager.setKeyguardMessage(errString, getInitialTextColorState(), biometricSourceType); } else if (mScreenLifecycle.getScreenState() == SCREEN_ON) { showBiometricMessage(errString, followUpMsg, biometricSourceType); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java index ccea254defaa..4c6fa4839e5c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java @@ -284,7 +284,8 @@ public class NotificationListener extends NotificationListenerWithPlugins implem /* rankingAdjustment= */ 0, /* isBubble= */ false, /* proposedImportance= */ 0, - /* sensitiveContent= */ false + /* sensitiveContent= */ false, + /* summarization = */ null ); } return ranking; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java index c8f972774ab0..382fc7058bf0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java @@ -70,7 +70,7 @@ import com.android.systemui.flags.FeatureFlagsClassic; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; -import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.recents.LauncherProxyService; import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -152,7 +152,7 @@ public class NotificationLockscreenUserManagerImpl implements private final List<UserChangedListener> mListeners = new ArrayList<>(); private final BroadcastDispatcher mBroadcastDispatcher; private final NotificationClickNotifier mClickNotifier; - private final Lazy<OverviewProxyService> mOverviewProxyServiceLazy; + private final Lazy<LauncherProxyService> mLauncherProxyServiceLazy; private final FeatureFlagsClassic mFeatureFlags; private boolean mShowLockscreenNotifications; private LockPatternUtils mLockPatternUtils; @@ -235,8 +235,8 @@ public class NotificationLockscreenUserManagerImpl implements if (!keyguardPrivateNotifications()) { // Start the overview connection to the launcher service // Connect if user hasn't connected yet - if (mOverviewProxyServiceLazy.get().getProxy() == null) { - mOverviewProxyServiceLazy.get().startConnectionToCurrentUser(); + if (mLauncherProxyServiceLazy.get().getProxy() == null) { + mLauncherProxyServiceLazy.get().startConnectionToCurrentUser(); } } } else if (Objects.equals(action, NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION)) { @@ -318,7 +318,7 @@ public class NotificationLockscreenUserManagerImpl implements Lazy<NotificationVisibilityProvider> visibilityProviderLazy, Lazy<CommonNotifCollection> commonNotifCollectionLazy, NotificationClickNotifier clickNotifier, - Lazy<OverviewProxyService> overviewProxyServiceLazy, + Lazy<LauncherProxyService> launcherProxyServiceLazy, KeyguardManager keyguardManager, StatusBarStateController statusBarStateController, @Main Executor mainExecutor, @@ -343,7 +343,7 @@ public class NotificationLockscreenUserManagerImpl implements mVisibilityProviderLazy = visibilityProviderLazy; mCommonNotifCollectionLazy = commonNotifCollectionLazy; mClickNotifier = clickNotifier; - mOverviewProxyServiceLazy = overviewProxyServiceLazy; + mLauncherProxyServiceLazy = launcherProxyServiceLazy; statusBarStateController.addCallback(this); mLockPatternUtils = lockPatternUtils; mKeyguardManager = keyguardManager; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt index b7cad625b7b8..46456b841e3f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt @@ -30,6 +30,7 @@ import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNoti import com.android.systemui.statusbar.notification.domain.model.TopPinnedState import com.android.systemui.statusbar.notification.headsup.PinnedStatus import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -73,17 +74,24 @@ constructor( OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon(this.key) } val colors = this.promotedContent.toCustomColorsModel() - val onClickListener = + + val clickListener: () -> Unit = { + // The notification pipeline needs everything to run on the main thread, so keep + // this event on the main thread. + applicationScope.launch { + notifChipsInteractor.onPromotedNotificationChipTapped(this@toActivityChipModel.key) + } + } + val onClickListenerLegacy = View.OnClickListener { - // The notification pipeline needs everything to run on the main thread, so keep - // this event on the main thread. - applicationScope.launch { - notifChipsInteractor.onPromotedNotificationChipTapped( - this@toActivityChipModel.key - ) - } + StatusBarChipsModernization.assertInLegacyMode() + clickListener.invoke() } - val clickBehavior = OngoingActivityChipModel.ClickBehavior.None + val clickBehavior = + OngoingActivityChipModel.ClickBehavior.ShowHeadsUpNotification({ + StatusBarChipsModernization.assertInNewMode() + clickListener.invoke() + }) val isShowingHeadsUpFromChipTap = headsUpState is TopPinnedState.Pinned && @@ -95,7 +103,7 @@ constructor( return OngoingActivityChipModel.Shown.IconOnly( icon, colors, - onClickListener, + onClickListenerLegacy, clickBehavior, ) } @@ -105,7 +113,7 @@ constructor( icon, colors, this.promotedContent.shortCriticalText, - onClickListener, + onClickListenerLegacy, clickBehavior, ) } @@ -121,7 +129,7 @@ constructor( return OngoingActivityChipModel.Shown.IconOnly( icon, colors, - onClickListener, + onClickListenerLegacy, clickBehavior, ) } @@ -130,7 +138,7 @@ constructor( return OngoingActivityChipModel.Shown.IconOnly( icon, colors, - onClickListener, + onClickListenerLegacy, clickBehavior, ) } @@ -140,7 +148,7 @@ constructor( icon, colors, time = this.promotedContent.time.time, - onClickListener, + onClickListenerLegacy, clickBehavior, ) } @@ -149,7 +157,7 @@ constructor( icon, colors, startTimeMs = this.promotedContent.time.time, - onClickListener, + onClickListenerLegacy, clickBehavior, ) } @@ -159,7 +167,7 @@ constructor( icon, colors, startTimeMs = this.promotedContent.time.time, - onClickListener, + onClickListenerLegacy, clickBehavior, ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt index a682f9674e2e..279792ef7536 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt @@ -66,6 +66,9 @@ fun OngoingActivityChip(model: OngoingActivityChipModel.Shown, modifier: Modifie ChipBody(model, onClick = { clickBehavior.onClick(expandable) }) } } + is OngoingActivityChipModel.ClickBehavior.ShowHeadsUpNotification -> { + ChipBody(model, onClick = { clickBehavior.onClick() }) + } is OngoingActivityChipModel.ClickBehavior.None -> { ChipBody(model, modifier = modifier) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt index 68c8f8cb4254..c6d6da2ad9aa 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt @@ -170,5 +170,8 @@ sealed class OngoingActivityChipModel { /** The chip expands into a dialog or activity on click. */ data class ExpandAction(val onClick: (Expandable) -> Unit) : ClickBehavior + + /** Clicking the chip will show the heads up notification associated with the chip. */ + data class ShowHeadsUpNotification(val onClick: () -> Unit) : ClickBehavior } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/headsup/shared/StatusBarNoHunBehavior.kt b/packages/SystemUI/src/com/android/systemui/statusbar/headsup/shared/StatusBarNoHunBehavior.kt index 2ae54d7c6c83..c9024d934068 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/headsup/shared/StatusBarNoHunBehavior.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/headsup/shared/StatusBarNoHunBehavior.kt @@ -33,7 +33,7 @@ object StatusBarNoHunBehavior { /** Is the refactor enabled */ @JvmStatic inline val isEnabled - get() = Flags.statusBarNoHunBehavior() && android.app.Flags.notificationsRedesignAppIcons() + get() = Flags.statusBarNoHunBehavior() /** * Called to ensure code is only run when the flag is enabled. This protects users from the diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt index 3825c098ca5d..b6ef95893036 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification import android.app.Notification +import android.app.Notification.EXTRA_SUMMARIZED_CONTENT import android.content.Context import android.content.pm.LauncherApps import android.graphics.drawable.AnimatedImageDrawable @@ -66,6 +67,12 @@ constructor( messagingStyle.shortcutIcon = launcherApps.getShortcutIcon(shortcutInfo) shortcutInfo.label?.let { label -> messagingStyle.conversationTitle = label } } + if (NmSummarizationUiFlag.isEnabled) { + entry.sbn.notification.extras.putCharSequence( + EXTRA_SUMMARIZED_CONTENT, entry.ranking.summarization + ) + } + messagingStyle.unreadMessageCount = conversationNotificationManager.getUnreadCount(entry, recoveredBuilder) return messagingStyle diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NmSummarizationUiFlag.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NmSummarizationUiFlag.kt new file mode 100644 index 000000000000..feac0a514828 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NmSummarizationUiFlag.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.notification + +import android.app.Flags; +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** + * Helper for android.app.nm_summarization and android.nm_summarization_ui. The new functionality + * should be enabled if either flag is enabled. + */ +@Suppress("NOTHING_TO_INLINE") +object NmSummarizationUiFlag { + const val FLAG_DESC = "android.app.nm_summarization(_ui)" + + @JvmStatic + inline val isEnabled + get() = Flags.nmSummarizationUi() || Flags.nmSummarization() + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_DESC) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = + RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_DESC) +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index 417e57d2205f..5cc79df9130a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -836,6 +836,14 @@ public final class NotificationEntry extends ListEntry { } /** + * Returns whether the NotificationEntry is promoted ongoing. + */ + @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) + public boolean isOngoingPromoted() { + return mSbn.getNotification().isPromotedOngoing(); + } + + /** * Returns whether this row is considered blockable (i.e. it's not a system notif * or is not in an allowList). */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt index 331ef1c01596..aa5008b8416e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt @@ -40,6 +40,7 @@ internal constructor( @RedactionType val redactionType: Int, val isChildInGroup: Boolean, val isGroupSummary: Boolean, + val summarization: String?, ) { companion object { @JvmStatic @@ -61,6 +62,7 @@ internal constructor( AsyncGroupHeaderViewInflation.isEnabled && !oldAdjustment.isGroupSummary && newAdjustment.isGroupSummary -> true + oldAdjustment.summarization != newAdjustment.summarization -> true else -> false } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt index 97e55c19d2f4..465bc288cbc1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt @@ -152,5 +152,6 @@ constructor( }, isChildInGroup = entry.hasEverBeenGroupChild(), isGroupSummary = entry.hasEverBeenGroupSummary(), + summarization = entry.ranking.summarization ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java index d401283aa84e..96192b1ea315 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.footer.ui.view; import static android.graphics.PorterDuff.Mode.SRC_ATOP; import static com.android.systemui.Flags.notificationFooterBackgroundTintOptimization; +import static com.android.systemui.Flags.notificationShadeBlur; import static com.android.systemui.util.ColorUtilKt.hexColorString; import android.annotation.ColorInt; @@ -29,6 +30,7 @@ import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.Resources; +import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.Drawable; @@ -39,6 +41,8 @@ import android.widget.TextView; import androidx.annotation.NonNull; +import com.android.internal.graphics.ColorUtils; +import com.android.systemui.common.shared.colors.SurfaceEffectColors; import com.android.systemui.res.R; import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.statusbar.notification.ColorUpdateLogger; @@ -383,9 +387,23 @@ public class FooterView extends StackScrollerDecorView { final Drawable historyBg = NotifRedesignFooter.isEnabled() ? theme.getDrawable(R.drawable.notif_footer_btn_background) : null; final @ColorInt int scHigh; + if (!notificationFooterBackgroundTintOptimization()) { - scHigh = mContext.getColor( - com.android.internal.R.color.materialColorSurfaceContainerHigh); + if (notificationShadeBlur()) { + Color backgroundColor = Color.valueOf( + SurfaceEffectColors.surfaceEffect0(getResources())); + scHigh = ColorUtils.setAlphaComponent(backgroundColor.toArgb(), 0xFF); + // Apply alpha on background drawables. + int backgroundAlpha = (int) (backgroundColor.alpha() * 0xFF); + clearAllBg.setAlpha(backgroundAlpha); + settingsBg.setAlpha(backgroundAlpha); + if (historyBg != null) { + historyBg.setAlpha(backgroundAlpha); + } + } else { + scHigh = mContext.getColor( + com.android.internal.R.color.materialColorSurfaceContainerHigh); + } if (scHigh != 0) { final ColorFilter bgColorFilter = new PorterDuffColorFilter(scHigh, SRC_ATOP); clearAllBg.setColorFilter(bgColorFilter); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java index 0171fb72e158..be61dc95fe20 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java @@ -706,7 +706,7 @@ public class HeadsUpManagerImpl } private void updateHeadsUpFlow() { - mHeadsUpNotificationRows.setValue(new HashSet<>(getHeadsUpEntryPhoneMap().values())); + mHeadsUpNotificationRows.setValue(new HashSet<>(mHeadsUpEntryMap.values())); } @Override @@ -732,11 +732,6 @@ public class HeadsUpManagerImpl return mHeadsUpAnimatingAway.getValue(); } - @NonNull - private ArrayMap<String, HeadsUpEntry> getHeadsUpEntryPhoneMap() { - return mHeadsUpEntryMap; - } - /** * Called to notify the listeners that the HUN animating away animation has ended. */ @@ -1014,7 +1009,7 @@ public class HeadsUpManagerImpl @Override public void setRemoteInputActive( @NonNull NotificationEntry entry, boolean remoteInputActive) { - HeadsUpEntry headsUpEntry = getHeadsUpEntryPhone(entry.getKey()); + HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(entry.getKey()); if (headsUpEntry != null && headsUpEntry.mRemoteInputActive != remoteInputActive) { headsUpEntry.mRemoteInputActive = remoteInputActive; if (ExpandHeadsUpOnInlineReply.isEnabled() && remoteInputActive) { @@ -1029,11 +1024,6 @@ public class HeadsUpManagerImpl } } - @Nullable - private HeadsUpEntry getHeadsUpEntryPhone(@NonNull String key) { - return mHeadsUpEntryMap.get(key); - } - @Override public void setGutsShown(@NonNull NotificationEntry entry, boolean gutsShown) { HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey()); @@ -1125,7 +1115,7 @@ public class HeadsUpManagerImpl return true; } - HeadsUpEntry headsUpEntry = getHeadsUpEntryPhone(key); + HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key); HeadsUpEntry topEntry = getTopHeadsUpEntryPhone(); if (headsUpEntry == null || headsUpEntry != topEntry) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BundleNotificationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BundleNotificationInfo.java index aad618d50067..9c6e41c482b6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BundleNotificationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BundleNotificationInfo.java @@ -64,11 +64,11 @@ public class BundleNotificationInfo extends NotificationInfo { boolean isNonblockable, boolean wasShownHighPriority, AssistantFeedbackController assistantFeedbackController, - MetricsLogger metricsLogger) throws RemoteException { + MetricsLogger metricsLogger, OnClickListener onCloseClick) throws RemoteException { super.bindNotification(pm, iNotificationManager, onUserInteractionCallback, channelEditorDialogController, pkg, notificationChannel, entry, onSettingsClick, onAppSettingsClick, uiEventLogger, isDeviceProvisioned, isNonblockable, - wasShownHighPriority, assistantFeedbackController, metricsLogger); + wasShownHighPriority, assistantFeedbackController, metricsLogger, onCloseClick); // Additionally, bind the feedback button. ComponentName assistant = iNotificationManager.getAllowedNotificationAssistant(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 5d7b8e6e8a84..d986aaebc0f8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -21,6 +21,7 @@ import static android.app.Notification.Action.SEMANTIC_ACTION_MARK_CONVERSATION_ import static android.service.notification.NotificationListenerService.REASON_CANCEL; import static com.android.systemui.flags.Flags.ENABLE_NOTIFICATIONS_SIMULATE_SLOW_MEASURE; +import static com.android.systemui.Flags.notificationsPinnedHunInShade; import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.PARENT_DISMISSED; import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP; import static com.android.systemui.statusbar.policy.RemoteInputView.FOCUS_ANIMATION_MIN_SCALE; @@ -106,6 +107,7 @@ import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; import com.android.systemui.statusbar.notification.headsup.PinnedStatus; import com.android.systemui.statusbar.notification.logging.NotificationCounters; import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag; import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation; import com.android.systemui.statusbar.notification.row.shared.LockscreenOtpRedaction; @@ -163,6 +165,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private boolean mShowSnooze = false; private boolean mIsFaded; + private boolean mIsPromotedOngoing = false; + /** * Listener for when {@link ExpandableNotificationRow} is laid out. */ @@ -196,6 +200,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private int mMaxSmallHeightBeforeS; private int mMaxSmallHeight; private int mMaxExpandedHeight; + private int mMaxExpandedHeightForPromotedOngoing; private int mNotificationLaunchHeight; private boolean mMustStayOnScreen; @@ -331,6 +336,15 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private boolean mSaveSpaceOnLockscreen; /** + * It is added for unit testing purpose. + * Please do not use it for other purposes. + */ + @VisibleForTesting + public void setIgnoreLockscreenConstraints(boolean ignoreLockscreenConstraints) { + mIgnoreLockscreenConstraints = ignoreLockscreenConstraints; + } + + /** * True if we use intrinsic height regardless of vertical space available on lockscreen. */ private boolean mIgnoreLockscreenConstraints; @@ -804,6 +818,13 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } private void updateLimitsForView(NotificationContentView layout) { + final int maxExpandedHeight; + if (isPromotedOngoing()) { + maxExpandedHeight = mMaxExpandedHeightForPromotedOngoing; + } else { + maxExpandedHeight = mMaxExpandedHeight; + } + View contractedView = layout.getContractedChild(); boolean customView = contractedView != null && contractedView.getId() @@ -824,7 +845,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView smallHeight = mMaxSmallHeightBeforeS; } } else if (isCallLayout) { - smallHeight = mMaxExpandedHeight; + smallHeight = maxExpandedHeight; } else { smallHeight = mMaxSmallHeight; } @@ -848,7 +869,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (headsUpWrapper != null) { headsUpHeight = Math.max(headsUpHeight, headsUpWrapper.getMinLayoutHeight()); } - layout.setHeights(smallHeight, headsUpHeight, mMaxExpandedHeight); + + layout.setHeights(smallHeight, headsUpHeight, maxExpandedHeight); } @NonNull @@ -1258,6 +1280,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (mIsSummaryWithChildren) { return mChildrenContainer.getIntrinsicHeight(); } + if (isPromotedOngoing()) { + return getMaxExpandHeight(); + } if (mExpandedWhenPinned) { return Math.max(getMaxExpandHeight(), getHeadsUpHeight()); } else if (android.app.Flags.compactHeadsUpNotification() @@ -2077,6 +2102,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } mMaxExpandedHeight = NotificationUtils.getFontScaledHeight(mContext, R.dimen.notification_max_height); + mMaxExpandedHeightForPromotedOngoing = NotificationUtils.getFontScaledHeight(mContext, + R.dimen.notification_max_height_for_promoted_ongoing); mMaxHeadsUpHeightBeforeN = NotificationUtils.getFontScaledHeight(mContext, R.dimen.notification_max_heads_up_height_legacy); mMaxHeadsUpHeightBeforeP = NotificationUtils.getFontScaledHeight(mContext, @@ -2762,6 +2789,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (mIsSummaryWithChildren && !shouldShowPublic()) { return !mChildrenExpanded; } + if (isPromotedOngoing()) { + return false; + } return mEnableNonGroupedNotificationExpand && mExpandable; } @@ -2770,6 +2800,18 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mPrivateLayout.updateExpandButtons(isExpandable()); } + /** + * Set this notification to be promoted ongoing + */ + public void setPromotedOngoing(boolean promotedOngoing) { + if (PromotedNotificationUiForceExpanded.isUnexpectedlyInLegacyMode()) { + return; + } + + mIsPromotedOngoing = promotedOngoing; + setExpandable(!mIsPromotedOngoing); + } + @Override public void setClipToActualHeight(boolean clipToActualHeight) { super.setClipToActualHeight(clipToActualHeight || isUserLocked()); @@ -2839,6 +2881,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } public void setUserLocked(boolean userLocked) { + if (isPromotedOngoing()) return; + mUserLocked = userLocked; mPrivateLayout.setUserExpanding(userLocked); // This is intentionally not guarded with mIsSummaryWithChildren since we might have had @@ -3000,6 +3044,35 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } } + public boolean isPromotedOngoing() { + return PromotedNotificationUiForceExpanded.isEnabled() && mIsPromotedOngoing; + } + + private boolean isPromotedNotificationExpanded(boolean allowOnKeyguard) { + // public view in non group notifications is always collapsed. + if (shouldShowPublic()) { + return false; + } + // RON will always be expanded when it is not on keyguard. + if (!mOnKeyguard) { + return true; + } + // RON will always be expanded when it is allowed on keyguard. + // allowOnKeyguard is used for getting the maximum height by NotificationContentView and + // NotificationChildrenContainer. + if (allowOnKeyguard) { + return true; + } + + // RON will be expanded when it needs to ignore lockscreen constraints. + if (mIgnoreLockscreenConstraints) { + return true; + } + + // RON will need be collapsed when it needs to save space on the lock screen. + return !mSaveSpaceOnLockscreen; + } + /** * Check whether the view state is currently expanded. This is given by the system in {@link * #setSystemExpanded(boolean)} and can be overridden by user expansion or @@ -3013,6 +3086,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } public boolean isExpanded(boolean allowOnKeyguard) { + if (isPromotedOngoing()) { + return isPromotedNotificationExpanded(allowOnKeyguard); + } + return (!shouldShowPublic()) && (!mOnKeyguard || allowOnKeyguard) && (!hasUserChangedExpansion() && (isSystemExpanded() || isSystemChildExpanded()) @@ -3184,7 +3261,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView @Override public boolean mustStayOnScreen() { - return mIsHeadsUp && mMustStayOnScreen; + // Must stay on screen in the open shade regardless how much the stack is scrolled if: + // 1. Is HUN and not marked as seen yet (isHeadsUp && mustStayOnScreen) + // 2. Is an FSI HUN (isPinned) + return mIsHeadsUp && mMustStayOnScreen || notificationsPinnedHunInShade() && isPinned(); } /** @@ -4011,6 +4091,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView + (!shouldShowPublic() && mIsSummaryWithChildren)); pw.print(", mShowNoBackground: " + mShowNoBackground); pw.print(", clipBounds: " + getClipBounds()); + if (PromotedNotificationUiForceExpanded.isEnabled()) { + pw.print(", isPromotedOngoing: " + isPromotedOngoing()); + pw.print(", isExpandable: " + isExpandable()); + pw.print(", mExpandable: " + mExpandable); + } pw.println(); if (NotificationContentView.INCLUDE_HEIGHTS_TO_DUMP) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java index 1ff0d9262476..92c10abff735 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java @@ -288,14 +288,21 @@ public class HybridConversationNotificationView extends HybridNotificationView { public void setText( CharSequence titleText, CharSequence contentText, - CharSequence conversationSenderName + CharSequence conversationSenderName, + @Nullable String summarization ) { if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) return; - if (conversationSenderName == null) { + if (summarization != null) { mConversationSenderName.setVisibility(GONE); + titleText = null; + contentText = summarization; } else { - mConversationSenderName.setVisibility(VISIBLE); - mConversationSenderName.setText(conversationSenderName); + if (conversationSenderName == null) { + mConversationSenderName.setVisibility(GONE); + } else { + mConversationSenderName.setVisibility(VISIBLE); + mConversationSenderName.setText(conversationSenderName); + } } // TODO (b/217799515): super.bind() doesn't use contentView, remove the contentView // argument when the flag is removed diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt index 793b3b8b1e42..6aa5e405f29c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.row +import android.animation.ValueAnimator import android.content.Context import android.graphics.BlendMode import android.graphics.Canvas @@ -38,8 +39,8 @@ import androidx.compose.ui.viewinterop.AndroidView import com.android.internal.graphics.ColorUtils import com.android.systemui.res.R import com.android.systemui.surfaceeffects.shaderutil.ShaderUtilLibrary -import kotlin.math.max -import kotlin.math.roundToInt +import com.android.wm.shell.shared.animation.Interpolators +import kotlin.math.min /** * A background style for smarter-smart-actions. The style is composed by a simplex3d noise, @@ -48,7 +49,7 @@ import kotlin.math.roundToInt class MagicActionBackgroundDrawable( context: Context, primaryContainer: Int? = null, - private val seed: Float = 0f, + seed: Float = 0f, ) : Drawable() { private val pixelDensity = context.resources.displayMetrics.density @@ -60,17 +61,15 @@ class MagicActionBackgroundDrawable( .toFloat() private val buttonShape = Path() private val paddingVertical = - context.resources - .getDimensionPixelSize(R.dimen.smart_reply_button_padding_vertical) - .toFloat() + context.resources.getDimensionPixelSize(R.dimen.smart_action_button_icon_padding).toFloat() /** The color of the button background. */ private val mainColor = primaryContainer ?: context.getColor(com.android.internal.R.color.materialColorPrimaryContainer) - /** Slightly dimmed down version of [mainColor] used on the simplex noise. */ - private val dimColor: Int + /** Slightly brighter version of [mainColor] used on the simplex noise. */ + private val effectColor: Int get() { val labColor = arrayOf(0.0, 0.0, 0.0).toDoubleArray() ColorUtils.colorToLAB(mainColor, labColor) @@ -78,56 +77,97 @@ class MagicActionBackgroundDrawable( return ColorUtils.CAMToColor( camColor.hue, camColor.chroma, - max(0f, (labColor[0] - 20).toFloat()), + min(100f, (labColor[0] + 10).toFloat()), ) } private val bgShader = MagicActionBackgroundShader() private val bgPaint = Paint() private val outlinePaint = Paint() + private val gradientAnimator = + ValueAnimator.ofFloat(0f, 1f).apply { + duration = 2500 + interpolator = Interpolators.LINEAR + addUpdateListener { invalidateSelf() } + } + private val turbulenceAnimator = + ValueAnimator.ofFloat(seed, seed + TURBULENCE_MOVEMENT).apply { + duration = ANIMATION_DURATION + interpolator = Interpolators.LINEAR + addUpdateListener { invalidateSelf() } + start() + } + private val effectFadeAnimation = + ValueAnimator.ofFloat(0f, 1f).apply { + duration = 1000 + startDelay = ANIMATION_DURATION - 1000L + interpolator = Interpolators.STANDARD_DECELERATE + addUpdateListener { invalidateSelf() } + } init { bgShader.setColorUniform("in_color", mainColor) - bgShader.setColorUniform("in_dimColor", dimColor) + bgShader.setColorUniform("in_effectColor", effectColor) bgPaint.shader = bgShader outlinePaint.style = Paint.Style.STROKE // Stroke is doubled in width and then clipped, to avoid anti-aliasing artifacts at the edge // of the rectangle. outlinePaint.strokeWidth = outlineStrokeWidth * 2 outlinePaint.blendMode = BlendMode.SCREEN - outlinePaint.alpha = (255 * 0.32f).roundToInt() + outlinePaint.alpha = OUTLINE_ALPHA + + animate() + } + + private fun animate() { + turbulenceAnimator.start() + gradientAnimator.start() + effectFadeAnimation.start() } override fun draw(canvas: Canvas) { + updateShaders() + // We clip instead of drawing 2 rounded rects, otherwise there will be artifacts where // around the button background and the outline. + canvas.save() canvas.clipPath(buttonShape) + canvas.drawPath(buttonShape, bgPaint) + canvas.drawPath(buttonShape, outlinePaint) + canvas.restore() + } - canvas.drawRect(bounds, bgPaint) - canvas.drawRoundRect( - bounds.left.toFloat(), - bounds.top + paddingVertical, - bounds.right.toFloat(), - bounds.bottom - paddingVertical, - cornerRadius, - cornerRadius, - outlinePaint, - ) + private fun updateShaders() { + val effectAlpha = 1f - effectFadeAnimation.animatedValue as Float + val turbulenceZ = turbulenceAnimator.animatedValue as Float + bgShader.setFloatUniform("in_sparkleMove", turbulenceZ * 1000) + bgShader.setFloatUniform("in_noiseMove", 0f, 0f, turbulenceZ) + bgShader.setFloatUniform("in_turbulenceAlpha", effectAlpha) + bgShader.setFloatUniform("in_spkarkleAlpha", SPARKLE_ALPHA * effectAlpha) + val gradientOffset = gradientAnimator.animatedValue as Float * bounds.width() + val outlineGradient = + LinearGradient( + gradientOffset + bounds.left.toFloat(), + 0f, + gradientOffset + bounds.right.toFloat(), + 0f, + mainColor, + ColorUtils.setAlphaComponent(mainColor, 0), + Shader.TileMode.MIRROR, + ) + outlinePaint.shader = outlineGradient } override fun onBoundsChange(bounds: Rect) { super.onBoundsChange(bounds) val width = bounds.width().toFloat() - val height = bounds.height() - paddingVertical * 2 + val height = bounds.height().toFloat() if (width == 0f || height == 0f) return bgShader.setFloatUniform("in_gridNum", NOISE_SIZE) - bgShader.setFloatUniform("in_spkarkleAlpha", SPARKLE_ALPHA) - bgShader.setFloatUniform("in_noiseMove", 0f, 0f, 0f) bgShader.setFloatUniform("in_size", width, height) bgShader.setFloatUniform("in_aspectRatio", width / height) - bgShader.setFloatUniform("in_time", seed) bgShader.setFloatUniform("in_pixelDensity", pixelDensity) buttonShape.reset() @@ -140,18 +180,6 @@ class MagicActionBackgroundDrawable( cornerRadius, Path.Direction.CW, ) - - val outlineGradient = - LinearGradient( - bounds.left.toFloat(), - 0f, - bounds.right.toFloat(), - 0f, - mainColor, - ColorUtils.setAlphaComponent(mainColor, 0), - Shader.TileMode.CLAMP, - ) - outlinePaint.shader = outlineGradient } override fun setAlpha(alpha: Int) { @@ -168,10 +196,15 @@ class MagicActionBackgroundDrawable( companion object { /** Smoothness of the turbulence. Larger numbers yield more detail. */ - private const val NOISE_SIZE = 0.7f - + private const val NOISE_SIZE = 0.57f /** Strength of the sparkles overlaid on the turbulence. */ private const val SPARKLE_ALPHA = 0.15f + /** Alpha (0..255) of the button outline */ + private const val OUTLINE_ALPHA = 82 + /** Turbulence grid size */ + private const val TURBULENCE_MOVEMENT = 4.3f + /** Total animation duration in millis */ + private const val ANIMATION_DURATION = 5000L } } @@ -183,24 +216,25 @@ private class MagicActionBackgroundShader : RuntimeShader(SHADER) { """ uniform float in_gridNum; uniform vec3 in_noiseMove; + uniform half in_sparkleMove; uniform vec2 in_size; uniform float in_aspectRatio; - uniform half in_time; uniform half in_pixelDensity; + uniform float in_turbulenceAlpha; uniform float in_spkarkleAlpha; layout(color) uniform vec4 in_color; - layout(color) uniform vec4 in_dimColor; + layout(color) uniform vec4 in_effectColor; """ private const val MAIN_SHADER = """vec4 main(vec2 p) { vec2 uv = p / in_size.xy; uv.x *= in_aspectRatio; vec3 noiseP = vec3(uv + in_noiseMove.xy, in_noiseMove.z) * in_gridNum; - half luma = 1.0 - getLuminosity(half3(simplex3d(noiseP))); - half4 turbulenceColor = mix(in_color, in_dimColor, luma); - float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_time); + half luma = getLuminosity(half3(simplex3d(noiseP))); + half4 turbulenceColor = mix(in_color, in_effectColor, luma * in_turbulenceAlpha); + float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_sparkleMove); sparkle = min(sparkle * in_spkarkleAlpha, in_spkarkleAlpha); - return turbulenceColor + half4(half3(sparkle), 1.0); + return saturate(turbulenceColor + half4(sparkle)); } """ private const val SHADER = UNIFORMS + ShaderUtilLibrary.SHADER_LIB + MAIN_SHADER 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 57fe24f40acb..13ed6c449797 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 @@ -57,6 +57,7 @@ import com.android.systemui.statusbar.notification.ConversationNotificationProce import com.android.systemui.statusbar.notification.InflationException; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor; +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded; import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel; import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation; import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation; @@ -216,7 +217,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder messagingStyle, builder, row.getContext(), - false + false, + entry.getRanking().getSummarization() ); // If the messagingStyle is null, we want to inflate the normal view isConversation = viewModel.isConversation(); @@ -238,7 +240,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder messagingStyle, builder, row.getContext(), - true); + true, + entry.getRanking().getSummarization()); } else { result.mPublicInflatedSingleLineViewModel = SingleLineViewInflater.inflateRedactedSingleLineViewModel( @@ -1111,6 +1114,10 @@ public class NotificationContentInflater implements NotificationRowContentBinder entry.setHeadsUpStatusBarText(result.headsUpStatusBarText); entry.setHeadsUpStatusBarTextPublic(result.headsUpStatusBarTextPublic); + if (PromotedNotificationUiForceExpanded.isEnabled()) { + row.setPromotedOngoing(entry.isOngoingPromoted()); + } + Trace.endAsyncSection(APPLY_TRACE_METHOD, System.identityHashCode(row)); if (endListener != null) { endListener.onAsyncInflationFinished(entry); @@ -1313,7 +1320,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder messagingStyle, recoveredBuilder, mContext, - false + false, + mEntry.getRanking().getSummarization() ); result.mInflatedSingleLineView = SingleLineViewInflater.inflatePrivateSingleLineView( @@ -1333,7 +1341,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder messagingStyle, recoveredBuilder, mContext, - true + true, + null ); } else { result.mPublicInflatedSingleLineViewModel = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java index 9712db8a1812..b1e5b22f9b1a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java @@ -70,10 +70,10 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider; import com.android.systemui.statusbar.notification.collection.render.NotifGutsViewListener; import com.android.systemui.statusbar.notification.collection.render.NotifGutsViewManager; +import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.policy.DeviceProvisionedController; -import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; import com.android.systemui.util.kotlin.JavaAdapter; import com.android.systemui.wmshell.BubblesManager; @@ -422,7 +422,8 @@ public class NotificationGutsManager implements NotifGutsViewManager, CoreStarta row.getIsNonblockable(), mHighPriorityProvider.isHighPriority(row.getEntry()), mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + row.getCloseButtonOnClickListener(row)); } /** @@ -476,7 +477,8 @@ public class NotificationGutsManager implements NotifGutsViewManager, CoreStarta row.getIsNonblockable(), mHighPriorityProvider.isHighPriority(row.getEntry()), mAssistantFeedbackController, - mMetricsLogger); + mMetricsLogger, + row.getCloseButtonOnClickListener(row)); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java index 20120991b5ac..8d26f94ced21 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java @@ -202,7 +202,7 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G boolean isNonblockable, boolean wasShownHighPriority, AssistantFeedbackController assistantFeedbackController, - MetricsLogger metricsLogger) + MetricsLogger metricsLogger, OnClickListener onCloseClick) throws RemoteException { mINotificationManager = iNotificationManager; mMetricsLogger = metricsLogger; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java index 6e8ec9576f80..bf738aa1128f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java @@ -42,6 +42,7 @@ import android.widget.FrameLayout.LayoutParams; import com.android.app.animation.Interpolators; import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.Flags; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.res.R; import com.android.systemui.statusbar.AlphaOptimizedImageView; @@ -270,6 +271,10 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl mInfoItem = createPartialConversationItem(mContext); } else if (personNotifType >= PeopleNotificationIdentifier.TYPE_FULL_PERSON) { mInfoItem = createConversationItem(mContext); + } else if (android.app.Flags.uiRichOngoing() + && Flags.permissionHelperUiRichOngoing() + && entry.getSbn().getNotification().isPromotedOngoing()) { + mInfoItem = createPromotedItem(mContext); } else { mInfoItem = createInfoItem(mContext); } @@ -682,6 +687,16 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl R.drawable.ic_settings); } + static NotificationMenuItem createPromotedItem(Context context) { + Resources res = context.getResources(); + String infoDescription = res.getString(R.string.notification_menu_gear_description); + PromotedNotificationInfo infoContent = + (PromotedNotificationInfo) LayoutInflater.from(context).inflate( + R.layout.promoted_notification_info, null, false); + return new NotificationMenuItem(context, infoDescription, infoContent, + R.drawable.ic_settings); + } + static NotificationMenuItem createBundleItem(Context context) { Resources res = context.getResources(); String infoDescription = res.getString(R.string.notification_menu_gear_description); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt index 0b299d965b09..f4aae6e288a7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt @@ -719,6 +719,7 @@ constructor( builder = builder, systemUiContext = systemUiContext, redactText = false, + summarization = entry.ranking.summarization ) } else null @@ -735,6 +736,7 @@ constructor( builder = builder, systemUiContext = systemUiContext, redactText = true, + summarization = null ) } else { SingleLineViewInflater.inflateRedactedSingleLineViewModel( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java new file mode 100644 index 000000000000..e4a0fa5b534e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java @@ -0,0 +1,104 @@ +/* + * 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.INotificationManager; +import android.app.NotificationChannel; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.RemoteException; +import android.service.notification.StatusBarNotification; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; + +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.UiEventLogger; +import com.android.systemui.res.R; +import com.android.systemui.statusbar.notification.AssistantFeedbackController; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; + +/** + * The guts of a notification revealed when performing a long press, specifically + * for notifications that are shown as promoted. Contains extra controls to allow user to revoke + * app permissions for sending promoted notifications. + */ +public class PromotedNotificationInfo extends NotificationInfo { + private static final String TAG = "PromotedNotifInfoGuts"; + private INotificationManager mNotificationManager; + + public PromotedNotificationInfo(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void bindNotification( + PackageManager pm, + INotificationManager iNotificationManager, + OnUserInteractionCallback onUserInteractionCallback, + ChannelEditorDialogController channelEditorDialogController, + String pkg, + NotificationChannel notificationChannel, + NotificationEntry entry, + OnSettingsClickListener onSettingsClick, + OnAppSettingsClickListener onAppSettingsClick, + UiEventLogger uiEventLogger, + boolean isDeviceProvisioned, + boolean isNonblockable, + boolean wasShownHighPriority, + AssistantFeedbackController assistantFeedbackController, + MetricsLogger metricsLogger, OnClickListener onCloseClick) throws RemoteException { + super.bindNotification(pm, iNotificationManager, onUserInteractionCallback, + channelEditorDialogController, pkg, notificationChannel, entry, onSettingsClick, + onAppSettingsClick, uiEventLogger, isDeviceProvisioned, isNonblockable, + wasShownHighPriority, assistantFeedbackController, metricsLogger, onCloseClick); + + mNotificationManager = iNotificationManager; + + bindDismiss(entry.getSbn(), onCloseClick); + bindDemote(entry.getSbn(), pkg); + } + + + protected void bindDismiss(StatusBarNotification sbn, + View.OnClickListener onCloseClick) { + View dismissButton = findViewById(R.id.promoted_dismiss); + + dismissButton.setOnClickListener(onCloseClick); + dismissButton.setVisibility(!sbn.isNonDismissable() + && dismissButton.hasOnClickListeners() ? VISIBLE : GONE); + + } + + protected void bindDemote(StatusBarNotification sbn, String packageName) { + View demoteButton = findViewById(R.id.promoted_demote); + demoteButton.setOnClickListener(getDemoteClickListener(sbn, packageName)); + demoteButton.setVisibility(demoteButton.hasOnClickListeners() ? VISIBLE : GONE); + } + + private OnClickListener getDemoteClickListener(StatusBarNotification sbn, String packageName) { + return ((View unusedView) -> { + try { + // TODO(b/391661009): Signal AutomaticPromotionCoordinator here + mNotificationManager.setCanBePromoted(packageName, sbn.getUid(), false, true); + } catch (RemoteException e) { + Log.e(TAG, "Couldn't revoke live update permission", e); + } + }); + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt index fe2803bfc5d6..c051513ef3b4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt @@ -61,6 +61,7 @@ internal object SingleLineViewInflater { builder: Notification.Builder, systemUiContext: Context, redactText: Boolean, + summarization: String? ): SingleLineViewModel { if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) { return SingleLineViewModel(null, null, null) @@ -108,6 +109,7 @@ internal object SingleLineViewInflater { conversationSenderName = if (isGroupConversation) conversationTextData?.senderName else null, avatar = conversationAvatar, + summarization = summarization ) return SingleLineViewModel( @@ -132,6 +134,7 @@ internal object SingleLineViewInflater { .ic_redacted_notification_single_line_icon ) ), + null ) } else { null diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineViewBinder.kt index a17197c1f8ea..a50fc4c7986a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineViewBinder.kt @@ -32,6 +32,7 @@ object SingleLineViewBinder { viewModel?.titleText, viewModel?.contentText, viewModel?.conversationData?.conversationSenderName, + viewModel?.conversationData?.summarization ) } else { // bind the title and content text views diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/SingleLineViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/SingleLineViewModel.kt index d583fa5d97ed..32ded25f18a1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/SingleLineViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/SingleLineViewModel.kt @@ -46,6 +46,7 @@ data class SingleLineViewModel( data class ConversationData( val conversationSenderName: CharSequence?, val avatar: ConversationAvatar, + val summarization: String? ) /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt index a96d972af2c4..08bc8f5d5bb9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt @@ -29,6 +29,7 @@ import com.android.systemui.statusbar.LockscreenShadeTransitionController import com.android.systemui.statusbar.StatusBarState.KEYGUARD import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView import com.android.systemui.statusbar.notification.shared.NotificationMinimalism @@ -463,7 +464,12 @@ constructor( var size = if (onLockscreen) { - if (view is ExpandableNotificationRow && view.entry.isStickyAndNotDemoted) { + if ( + view is ExpandableNotificationRow && + (view.entry.isStickyAndNotDemoted || + (PromotedNotificationUiForceExpanded.isEnabled && + view.isPromotedOngoing)) + ) { height } else { view.getMinHeight(/* ignoreTemporaryStates= */ true).toFloat() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt index 1965b9538df0..f7401440cfcb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt @@ -36,6 +36,8 @@ import com.android.systemui.util.kotlin.DisposableHandles import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map /** Binds the shared notification container to its view-model. */ @SysUISingleton @@ -143,21 +145,25 @@ constructor( if (!SceneContainerFlag.isEnabled) { if (Flags.magicPortraitWallpapers()) { launch { - viewModel - .getNotificationStackAbsoluteBottom( - calculateMaxNotifications = calculateMaxNotifications, - calculateHeight = { maxNotifications -> - notificationStackSizeCalculator.computeHeight( - maxNotifs = maxNotifications, - shelfHeight = controller.getShelfHeight().toFloat(), - stack = controller.view, - ) - }, - controller.getShelfHeight().toFloat(), + combine( + viewModel.getNotificationStackAbsoluteBottom( + calculateMaxNotifications = calculateMaxNotifications, + calculateHeight = { maxNotifications -> + notificationStackSizeCalculator.computeHeight( + maxNotifs = maxNotifications, + shelfHeight = + controller.getShelfHeight().toFloat(), + stack = controller.view, + ) + }, + controller.getShelfHeight().toFloat(), + ), + viewModel.configurationBasedDimensions.map { it.marginTop }, + ::Pair, ) - .collect { bottom -> + .collect { (bottom: Float, marginTop: Int) -> keyguardInteractor.setNotificationStackAbsoluteBottom( - bottom + marginTop + bottom ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java index 16e9c717935c..a2f1ded042f4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java @@ -26,24 +26,31 @@ import android.graphics.drawable.Drawable; import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; -import android.widget.TextView; import androidx.annotation.StyleRes; +import androidx.core.graphics.ColorUtils; import com.android.app.animation.Interpolators; import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.Flags; import com.android.systemui.keyguard.KeyguardIndication; import com.android.systemui.res.R; +import com.android.systemui.shared.shadow.DoubleShadowTextView; /** * A view to show hints on Keyguard ("Swipe up to unlock", "Tap again to open"). */ -public class KeyguardIndicationTextView extends TextView { +public class KeyguardIndicationTextView extends DoubleShadowTextView { + // Minimum luminance for texts to receive shadows. + private static final float MIN_TEXT_SHADOW_LUMINANCE = 0.5f; public static final long Y_IN_DURATION = 600L; @StyleRes private static int sStyleId = R.style.TextAppearance_Keyguard_BottomArea; @StyleRes + private static int sStyleWithDoubleShadowTextId = + R.style.TextAppearance_Keyguard_BottomArea_DoubleShadow; + @StyleRes private static int sButtonStyleId = R.style.TextAppearance_Keyguard_BottomArea_Button; private boolean mAnimationsEnabled = true; @@ -226,7 +233,14 @@ public class KeyguardIndicationTextView extends TextView { if (mKeyguardIndicationInfo.getBackground() != null) { setTextAppearance(sButtonStyleId); } else { - setTextAppearance(sStyleId); + // If text is transparent or dark color, don't draw any shadow + if (Flags.indicationTextA11yFix() && ColorUtils.calculateLuminance( + mKeyguardIndicationInfo.getTextColor().getDefaultColor()) + > MIN_TEXT_SHADOW_LUMINANCE) { + setTextAppearance(sStyleWithDoubleShadowTextId); + } else { + setTextAppearance(sStyleId); + } } setBackground(mKeyguardIndicationInfo.getBackground()); setTextColor(mKeyguardIndicationInfo.getTextColor()); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java index 4c2bfe5ca257..40245aef4f67 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java @@ -23,6 +23,7 @@ import static com.android.systemui.Flags.updateUserSwitcherBackground; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; +import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.database.ContentObserver; @@ -56,6 +57,7 @@ import com.android.systemui.log.core.LogLevel; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.res.R; import com.android.systemui.scene.shared.flag.SceneContainerFlag; +import com.android.systemui.shade.ShadeDisplayAware; import com.android.systemui.shade.ShadeViewStateProvider; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.StatusBarState; @@ -114,6 +116,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat R.id.keyguard_hun_animator_start_tag); private final CoroutineDispatcher mCoroutineDispatcher; + private final Context mContext; private final CarrierTextController mCarrierTextController; private final ConfigurationController mConfigurationController; private final SystemStatusAnimationScheduler mAnimationScheduler; @@ -129,7 +132,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat private final KeyguardStatusBarViewModel mKeyguardStatusBarViewModel; private final BiometricUnlockController mBiometricUnlockController; private final SysuiStatusBarStateController mStatusBarStateController; - private final StatusBarContentInsetsProvider mInsetsProvider; + private final StatusBarContentInsetsProviderStore mInsetsProviderStore; private final UserManager mUserManager; private final StatusBarUserChipViewModel mStatusBarUserChipViewModel; private final SecureSettings mSecureSettings; @@ -314,6 +317,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat @Inject public KeyguardStatusBarViewController( @Main CoroutineDispatcher dispatcher, + @ShadeDisplayAware Context context, KeyguardStatusBarView view, CarrierTextController carrierTextController, ConfigurationController configurationController, @@ -347,6 +351,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat ) { super(view); mCoroutineDispatcher = dispatcher; + mContext = context; mCarrierTextController = carrierTextController; mConfigurationController = configurationController; mAnimationScheduler = animationScheduler; @@ -362,7 +367,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat mKeyguardStatusBarViewModel = keyguardStatusBarViewModel; mBiometricUnlockController = biometricUnlockController; mStatusBarStateController = statusBarStateController; - mInsetsProvider = statusBarContentInsetsProviderStore.getDefaultDisplay(); + mInsetsProviderStore = statusBarContentInsetsProviderStore; mUserManager = userManager; mStatusBarUserChipViewModel = userChipViewModel; mSecureSettings = secureSettings; @@ -404,6 +409,10 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat mStatusOverlayHoverListenerFactory = statusOverlayHoverListenerFactory; } + private StatusBarContentInsetsProvider insetsProvider() { + return mInsetsProviderStore.forDisplay(mContext.getDisplayId()); + } + @Override protected void onInit() { super.onInit(); @@ -446,7 +455,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat .createDarkAwareListener(mSystemIconsContainer, mView.darkChangeFlow()); mSystemIconsContainer.setOnHoverListener(hoverListener); mView.setOnApplyWindowInsetsListener( - (view, windowInsets) -> mView.updateWindowInsets(windowInsets, mInsetsProvider)); + (view, windowInsets) -> mView.updateWindowInsets(windowInsets, insetsProvider())); mSecureSettings.registerContentObserverForUserSync( Settings.Secure.STATUS_BAR_SHOW_VIBRATE_ICON, false, @@ -645,7 +654,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat * {@code OnApplyWindowInsetsListener}s. */ public void setDisplayCutout(@Nullable DisplayCutout displayCutout) { - mView.setDisplayCutout(displayCutout, mInsetsProvider); + mView.setDisplayCutout(displayCutout, insetsProvider()); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt index 3f44f7bdef90..caf8a43b2aaf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt @@ -46,7 +46,6 @@ import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore -import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider import com.android.systemui.statusbar.policy.Clock import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.window.StatusBarWindowStateController @@ -84,7 +83,7 @@ private constructor( private val configurationController: ConfigurationController, private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory, private val darkIconDispatcher: DarkIconDispatcher, - private val statusBarContentInsetsProvider: StatusBarContentInsetsProvider, + private val statusBarContentInsetsProviderStore: StatusBarContentInsetsProviderStore, private val lazyStatusBarShadeDisplayPolicy: Lazy<StatusBarTouchShadeDisplayPolicy>, ) : ViewController<PhoneStatusBarView>(view) { @@ -92,6 +91,8 @@ private constructor( private lateinit var clock: Clock private lateinit var startSideContainer: View private lateinit var endSideContainer: View + private val statusBarContentInsetsProvider + get() = statusBarContentInsetsProviderStore.forDisplay(context.displayId) private val iconsOnTouchListener = object : View.OnTouchListener { @@ -189,11 +190,9 @@ private constructor( init { // These should likely be done in `onInit`, not `init`. mView.setTouchEventHandler(PhoneStatusBarViewTouchHandler()) - mView.setHasCornerCutoutFetcher { - statusBarContentInsetsProvider.currentRotationHasCornerCutout() - } - mView.setInsetsFetcher { - statusBarContentInsetsProvider.getStatusBarContentInsetsForCurrentRotation() + statusBarContentInsetsProvider?.let { + mView.setHasCornerCutoutFetcher { it.currentRotationHasCornerCutout() } + mView.setInsetsFetcher { it.getStatusBarContentInsetsForCurrentRotation() } } mView.init(userChipViewModel) } @@ -393,7 +392,7 @@ private constructor( configurationController, statusOverlayHoverListenerFactory, darkIconDispatcher, - statusBarContentInsetsProviderStore.defaultDisplay, + statusBarContentInsetsProviderStore, lazyStatusBarShadeDisplayPolicy, ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index 0f6c3069609e..be48c3d928f4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -66,6 +66,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState; import com.android.systemui.keyguard.shared.model.ScrimAlpha; import com.android.systemui.keyguard.shared.model.TransitionState; import com.android.systemui.keyguard.shared.model.TransitionStep; +import com.android.systemui.keyguard.ui.transitions.BlurConfig; import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransitionViewModel; import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel; import com.android.systemui.res.R; @@ -258,6 +259,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump private int mScrimsVisibility; private final TriConsumer<ScrimState, Float, GradientColors> mScrimStateListener; private final LargeScreenShadeInterpolator mLargeScreenShadeInterpolator; + private final BlurConfig mBlurConfig; private Consumer<Integer> mScrimVisibleListener; private boolean mBlankScreen; private boolean mScreenBlankingCallbackCalled; @@ -339,9 +341,11 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump KeyguardTransitionInteractor keyguardTransitionInteractor, KeyguardInteractor keyguardInteractor, @Main CoroutineDispatcher mainDispatcher, - LargeScreenShadeInterpolator largeScreenShadeInterpolator) { + LargeScreenShadeInterpolator largeScreenShadeInterpolator, + BlurConfig blurConfig) { mScrimStateListener = lightBarController::setScrimState; mLargeScreenShadeInterpolator = largeScreenShadeInterpolator; + mBlurConfig = blurConfig; // All scrims default alpha need to match bouncer background alpha to make sure the // transitions involving the bouncer are smooth and don't overshoot the bouncer alpha. mDefaultScrimAlpha = @@ -406,7 +410,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump final ScrimState[] states = ScrimState.values(); for (int i = 0; i < states.length; i++) { - states[i].init(mScrimInFront, mScrimBehind, mDozeParameters, mDockManager); + states[i].init(mScrimInFront, mScrimBehind, mDozeParameters, mDockManager, mBlurConfig); states[i].setScrimBehindAlphaKeyguard(mScrimBehindAlphaKeyguard); states[i].setDefaultScrimAlpha(mDefaultScrimAlpha); } @@ -868,7 +872,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump * bounds instead. */ public void setClipsQsScrim(boolean clipScrim) { - if (Flags.notificationShadeBlur()) { + if (Flags.notificationShadeBlur() || Flags.bouncerUiRevamp()) { // Never clip scrims when blur is enabled, colors of UI elements are supposed to "add" // up across the scrims. mClipsQsScrim = false; @@ -1210,6 +1214,12 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump dispatchBackScrimState(mScrimBehind.getViewAlpha()); } + if (Flags.bouncerUiRevamp()) { + // Blur the notification scrim as needed. The blur is needed only when we show the + // expanded shade behind the bouncer. Without it, the notification scrim outline is + // visible behind the bouncer. + mNotificationsScrim.setBlurRadius(mState.getNotifBlurRadius()); + } // We also want to hide FLAG_SHOW_WHEN_LOCKED activities under the scrim. boolean hideFlagShowWhenLockedActivities = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java index 8170e6d91a0f..5f423cf35edd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java @@ -24,6 +24,7 @@ import android.graphics.Color; import com.android.app.tracing.coroutines.TrackTracer; import com.android.systemui.Flags; import com.android.systemui.dock.DockManager; +import com.android.systemui.keyguard.ui.transitions.BlurConfig; import com.android.systemui.res.R; import com.android.systemui.scrim.ScrimView; import com.android.systemui.shade.ui.ShadeColors; @@ -149,7 +150,14 @@ public enum ScrimState { @Override public void prepare(ScrimState previousState) { if (Flags.bouncerUiRevamp()) { - mBehindAlpha = 0f; + if (previousState == SHADE_LOCKED) { + mBehindAlpha = previousState.getBehindAlpha(); + mNotifAlpha = previousState.getNotifAlpha(); + mNotifBlurRadius = mBlurConfig.getMaxBlurRadiusPx(); + } else { + mNotifAlpha = 0f; + mBehindAlpha = 0f; + } mFrontAlpha = TRANSPARENT_BOUNCER_SCRIM_ALPHA; mFrontTint = mSurfaceColor; return; @@ -395,6 +403,7 @@ public enum ScrimState { DozeParameters mDozeParameters; DockManager mDockManager; boolean mDisplayRequiresBlanking; + protected BlurConfig mBlurConfig; boolean mLaunchingAffordanceWithPreview; boolean mOccludeAnimationPlaying; boolean mWakeLockScreenSensorActive; @@ -403,8 +412,12 @@ public enum ScrimState { boolean mClipQsScrim; int mBackgroundColor; + // This is needed to blur the scrim behind the scrimmed bouncer to avoid showing + // the notification section border + protected float mNotifBlurRadius = 0.0f; + public void init(ScrimView scrimInFront, ScrimView scrimBehind, DozeParameters dozeParameters, - DockManager dockManager) { + DockManager dockManager, BlurConfig blurConfig) { mBackgroundColor = scrimBehind.getContext().getColor(R.color.shade_scrim_background_dark); mScrimInFront = scrimInFront; mScrimBehind = scrimBehind; @@ -412,6 +425,7 @@ public enum ScrimState { mDozeParameters = dozeParameters; mDockManager = dockManager; mDisplayRequiresBlanking = dozeParameters.getDisplayNeedsBlanking(); + mBlurConfig = blurConfig; } /** Prepare state for transition. */ @@ -518,4 +532,8 @@ public enum ScrimState { public void setClipQsScrim(boolean clipsQsScrim) { mClipQsScrim = clipsQsScrim; } + + public float getNotifBlurRadius() { + return mNotifBlurRadius; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTransitionAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTransitionAnimatorController.kt index 705a11df83fc..e12b21eb2c4a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTransitionAnimatorController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTransitionAnimatorController.kt @@ -1,6 +1,7 @@ package com.android.systemui.statusbar.phone import android.view.View +import com.android.systemui.Flags import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.animation.TransitionAnimator import com.android.systemui.animation.TransitionAnimator.Companion.getProgress @@ -22,7 +23,7 @@ class StatusBarTransitionAnimatorController( private val notificationShadeWindowController: NotificationShadeWindowController, private val commandQueue: CommandQueue, @DisplayId private val displayId: Int, - private val isLaunchForActivity: Boolean = true + private val isLaunchForActivity: Boolean = true, ) : ActivityTransitionAnimator.Controller by delegate { private var hideIconsDuringLaunchAnimation: Boolean = true @@ -41,8 +42,16 @@ class StatusBarTransitionAnimatorController( } override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) { - delegate.onTransitionAnimationStart(isExpandingFullyAbove) - shadeAnimationInteractor.setIsLaunchingActivity(true) + if (Flags.shadeLaunchAccessibility()) { + // We set this before calling the delegate to make sure that accessibility is disabled + // for the whole duration of the transition, so that we don't have stray TalkBack events + // once the animating view becomes invisible. + shadeAnimationInteractor.setIsLaunchingActivity(true) + delegate.onTransitionAnimationStart(isExpandingFullyAbove) + } else { + delegate.onTransitionAnimationStart(isExpandingFullyAbove) + shadeAnimationInteractor.setIsLaunchingActivity(true) + } if (!isExpandingFullyAbove) { shadeController.collapseWithDuration( ActivityTransitionAnimator.TIMINGS.totalDuration.toInt() @@ -59,7 +68,7 @@ class StatusBarTransitionAnimatorController( override fun onTransitionAnimationProgress( state: TransitionAnimator.State, progress: Float, - linearProgress: Float + linearProgress: Float, ) { delegate.onTransitionAnimationProgress(state, progress, linearProgress) val hideIcons = @@ -67,7 +76,7 @@ class StatusBarTransitionAnimatorController( ActivityTransitionAnimator.TIMINGS, linearProgress, ANIMATION_DELAY_ICON_FADE_IN, - 100 + 100, ) == 0.0f if (hideIcons != hideIconsDuringLaunchAnimation) { hideIconsDuringLaunchAnimation = hideIcons diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt index 69b7e892a380..9795cda97f37 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt @@ -49,8 +49,10 @@ import androidx.compose.ui.input.pointer.pointerInteropFilter import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.android.systemui.inputdevice.tutorial.ui.composable.DoneButton +import com.android.systemui.keyboard.shortcut.ui.composable.hasCompactWindowSize import com.android.systemui.res.R import com.android.systemui.touchpad.tutorial.ui.gesture.isFourFingerTouchpadSwipe import com.android.systemui.touchpad.tutorial.ui.gesture.isThreeFingerTouchpadSwipe @@ -80,6 +82,7 @@ fun TutorialSelectionScreen( } ), ) { + val padding = if (hasCompactWindowSize()) 24.dp else 60.dp val configuration = LocalConfiguration.current when (configuration.orientation) { Configuration.ORIENTATION_LANDSCAPE -> { @@ -88,7 +91,7 @@ fun TutorialSelectionScreen( onHomeTutorialClicked = onHomeTutorialClicked, onRecentAppsTutorialClicked = onRecentAppsTutorialClicked, onSwitchAppsTutorialClicked = onSwitchAppsTutorialClicked, - modifier = Modifier.weight(1f).padding(60.dp), + modifier = Modifier.weight(1f).padding(padding), lastSelectedScreen, ) } @@ -98,7 +101,7 @@ fun TutorialSelectionScreen( onHomeTutorialClicked = onHomeTutorialClicked, onRecentAppsTutorialClicked = onRecentAppsTutorialClicked, onSwitchAppsTutorialClicked = onSwitchAppsTutorialClicked, - modifier = Modifier.weight(1f).padding(60.dp), + modifier = Modifier.weight(1f).padding(padding), lastSelectedScreen, ) } @@ -106,7 +109,7 @@ fun TutorialSelectionScreen( // because other composables have weight 1, Done button will be positioned first DoneButton( onDoneButtonClicked = onDoneButtonClicked, - modifier = Modifier.padding(horizontal = 60.dp), + modifier = Modifier.padding(horizontal = padding), ) } } @@ -146,7 +149,7 @@ private fun VerticalSelectionButtons( lastSelectedScreen: Screen, ) { Column( - verticalArrangement = Arrangement.spacedBy(20.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier, ) { @@ -244,8 +247,13 @@ private fun TutorialButton( modifier = Modifier.width(30.dp).height(30.dp), tint = iconColor, ) - Spacer(modifier = Modifier.height(16.dp)) - Text(text = text, style = MaterialTheme.typography.headlineLarge, color = iconColor) + if (!hasCompactWindowSize()) Spacer(modifier = Modifier.height(16.dp)) + Text( + text = text, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.headlineLarge, + color = iconColor, + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt index 39b434ad65f1..83b7c1818341 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt @@ -16,7 +16,6 @@ package com.android.systemui.volume.dialog -import android.app.Dialog import android.content.Context import android.graphics.PixelFormat import android.os.Bundle @@ -24,6 +23,7 @@ import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.view.WindowManager +import androidx.activity.ComponentDialog import com.android.app.tracing.coroutines.coroutineScopeTraced import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.lifecycle.repeatWhenAttached @@ -40,7 +40,7 @@ constructor( @Application context: Context, private val componentFactory: VolumeDialogComponent.Factory, private val visibilityInteractor: VolumeDialogVisibilityInteractor, -) : Dialog(context, R.style.Theme_SystemUI_Dialog_Volume) { +) : ComponentDialog(context, R.style.Theme_SystemUI_Dialog_Volume) { init { with(window!!) { diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt index 8c018606ebda..e261ceebf33e 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt @@ -17,6 +17,8 @@ package com.android.systemui.volume.dialog.domain.interactor import android.annotation.SuppressLint +import android.media.AudioManager.RINGER_MODE_NORMAL +import android.media.AudioManager.RINGER_MODE_SILENT import android.os.Handler import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.plugins.VolumeDialogController @@ -60,10 +62,10 @@ constructor( awaitClose { volumeDialogController.removeCallback(producer) } } .buffer(capacity = BUFFER_CAPACITY, onBufferOverflow = BufferOverflow.DROP_OLDEST) - .shareIn(replay = 0, scope = coroutineScope, started = SharingStarted.WhileSubscribed()) + .shareIn(replay = 0, scope = coroutineScope, started = SharingStarted.Eagerly) .onStart { emit(VolumeDialogEventModel.SubscribedToEvents) } - private class VolumeDialogEventModelProducer( + private inner class VolumeDialogEventModelProducer( private val scope: ProducerScope<VolumeDialogEventModel> ) : VolumeDialogController.Callbacks { override fun onShowRequested(reason: Int, keyguardLocked: Boolean, lockTaskModeState: Int) { @@ -93,14 +95,6 @@ constructor( // Configuration change is never emitted by the VolumeDialogControllerImpl now. override fun onConfigurationChanged() = Unit - override fun onShowVibrateHint() { - scope.trySend(VolumeDialogEventModel.ShowVibrateHint) - } - - override fun onShowSilentHint() { - scope.trySend(VolumeDialogEventModel.ShowSilentHint) - } - override fun onScreenOff() { scope.trySend(VolumeDialogEventModel.ScreenOff) } @@ -113,16 +107,6 @@ constructor( scope.trySend(VolumeDialogEventModel.AccessibilityModeChanged(showA11yStream == true)) } - // Captions button is remove from the Volume Dialog - override fun onCaptionComponentStateChanged( - isComponentEnabled: Boolean, - fromTooltip: Boolean, - ) = Unit - - // Captions button is remove from the Volume Dialog - override fun onCaptionEnabledStateChanged(isEnabled: Boolean, checkBeforeSwitch: Boolean) = - Unit - override fun onShowCsdWarning(csdWarning: Int, durationMs: Int) { scope.trySend( VolumeDialogEventModel.ShowCsdWarning( @@ -135,5 +119,25 @@ constructor( override fun onVolumeChangedFromKey() { scope.trySend(VolumeDialogEventModel.VolumeChangedFromKey) } + + // This should've been handled in side the controller itself. + override fun onShowVibrateHint() { + volumeDialogController.setRingerMode(RINGER_MODE_SILENT, false) + } + + // This should've been handled in side the controller itself. + override fun onShowSilentHint() { + volumeDialogController.setRingerMode(RINGER_MODE_NORMAL, false) + } + + // Captions button is remove from the Volume Dialog + override fun onCaptionComponentStateChanged( + isComponentEnabled: Boolean, + fromTooltip: Boolean, + ) = Unit + + // Captions button is remove from the Volume Dialog + override fun onCaptionEnabledStateChanged(isEnabled: Boolean, checkBeforeSwitch: Boolean) = + Unit } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogEventModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogEventModel.kt index 9793d2be6b98..a0214dc957a4 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogEventModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogEventModel.kt @@ -38,10 +38,6 @@ sealed interface VolumeDialogEventModel { data class LayoutDirectionChanged(val layoutDirection: Int) : VolumeDialogEventModel - data object ShowVibrateHint : VolumeDialogEventModel - - data object ShowSilentHint : VolumeDialogEventModel - data object ScreenOff : VolumeDialogEventModel data class ShowSafetyWarning(val flags: Int) : VolumeDialogEventModel diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt index 40719185e290..73f6236393b2 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt @@ -47,7 +47,6 @@ constructor( private val audioSystemRepository: AudioSystemRepository, private val ringerFeedbackRepository: VolumeDialogRingerFeedbackRepository, ) { - val ringerModel: Flow<VolumeDialogRingerModel> = volumeDialogStateInteractor.volumeDialogState .mapNotNull { toRingerModel(it) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponent.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponent.kt index 940c79c78d76..577e47bb3b83 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponent.kt @@ -18,7 +18,6 @@ package com.android.systemui.volume.dialog.sliders.dagger import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogOverscrollViewBinder -import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderHapticsViewBinder import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderViewBinder import dagger.BindsInstance import dagger.Subcomponent @@ -33,8 +32,6 @@ interface VolumeDialogSliderComponent { fun sliderViewBinder(): VolumeDialogSliderViewBinder - fun sliderHapticsViewBinder(): VolumeDialogSliderHapticsViewBinder - fun overscrollViewBinder(): VolumeDialogOverscrollViewBinder @Subcomponent.Factory diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/data/repository/VolumeDialogSliderTouchEventsRepository.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/data/repository/VolumeDialogSliderTouchEventsRepository.kt index 82885d65c513..07954f850286 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/data/repository/VolumeDialogSliderTouchEventsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/data/repository/VolumeDialogSliderTouchEventsRepository.kt @@ -16,8 +16,8 @@ package com.android.systemui.volume.dialog.sliders.data.repository -import android.view.MotionEvent import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope +import com.android.systemui.volume.dialog.sliders.shared.model.SliderInputEvent import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -26,10 +26,11 @@ import kotlinx.coroutines.flow.filterNotNull @VolumeDialogSliderScope class VolumeDialogSliderTouchEventsRepository @Inject constructor() { - private val mutableSliderTouchEvents: MutableStateFlow<MotionEvent?> = MutableStateFlow(null) - val sliderTouchEvent: Flow<MotionEvent> = mutableSliderTouchEvents.filterNotNull() + private val mutableSliderTouchEvents: MutableStateFlow<SliderInputEvent.Touch?> = + MutableStateFlow(null) + val sliderTouchEvent: Flow<SliderInputEvent.Touch> = mutableSliderTouchEvents.filterNotNull() - fun update(event: MotionEvent) { - mutableSliderTouchEvents.tryEmit(MotionEvent.obtain(event)) + fun update(touch: SliderInputEvent.Touch) { + mutableSliderTouchEvents.value = touch } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInputEventsInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInputEventsInteractor.kt index c7b4184a9f2f..351832bd275a 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInputEventsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInputEventsInteractor.kt @@ -16,7 +16,6 @@ package com.android.systemui.volume.dialog.sliders.domain.interactor -import android.view.MotionEvent import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogCallbacksInteractor import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogVisibilityInteractor @@ -45,7 +44,7 @@ constructor( val event: Flow<SliderInputEvent> = merge( - repository.sliderTouchEvent.map { SliderInputEvent.Touch(it) }, + repository.sliderTouchEvent, volumeDialogCallbacksInteractor.event .filterIsInstance(VolumeDialogEventModel.VolumeChangedFromKey::class) .map { SliderInputEvent.Button }, @@ -55,7 +54,7 @@ constructor( event.onEach { visibilityInteractor.resetDismissTimeout() }.launchIn(coroutineScope) } - fun onTouchEvent(newEvent: MotionEvent) { - repository.update(newEvent) + fun onTouchEvent(pointerEvent: SliderInputEvent.Touch) { + repository.update(pointerEvent) } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/shared/model/SliderInputEvent.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/shared/model/SliderInputEvent.kt index 37dbb4b3a81d..841730857d71 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/shared/model/SliderInputEvent.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/shared/model/SliderInputEvent.kt @@ -16,12 +16,20 @@ package com.android.systemui.volume.dialog.sliders.shared.model -import android.view.MotionEvent - /** Models input event happened on the Volume Slider */ sealed interface SliderInputEvent { - data class Touch(val event: MotionEvent) : SliderInputEvent + interface Touch : SliderInputEvent { + + val x: Float + val y: Float + + data class Start(override val x: Float, override val y: Float) : Touch + + data class Move(override val x: Float, override val y: Float) : Touch + + data class End(override val x: Float, override val y: Float) : Touch + } data object Button : SliderInputEvent } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinder.kt index 8109b50aa34a..38feb69aad7b 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinder.kt @@ -20,11 +20,9 @@ import android.view.View import androidx.dynamicanimation.animation.FloatValueHolder import androidx.dynamicanimation.animation.SpringAnimation import androidx.dynamicanimation.animation.SpringForce -import com.android.systemui.res.R import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogOverscrollViewModel import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogOverscrollViewModel.OverscrollEventModel -import com.google.android.material.slider.Slider import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.launchIn @@ -51,10 +49,6 @@ constructor(private val viewModel: VolumeDialogOverscrollViewModel) { ) .addUpdateListener { _, value, _ -> viewsToAnimate.setTranslationY(value) } - view.requireViewById<Slider>(R.id.volume_dialog_slider).addOnChangeListener { s, value, _ -> - viewModel.setSlider(value = value, min = s.valueFrom, max = s.valueTo) - } - viewModel.overscrollEvent .onEach { event -> when (event) { diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinder.kt deleted file mode 100644 index 5a7fbc6341f2..000000000000 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinder.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.volume.dialog.sliders.ui - -import android.view.View -import com.android.systemui.haptics.slider.HapticSlider -import com.android.systemui.haptics.slider.HapticSliderPlugin -import com.android.systemui.res.R -import com.android.systemui.statusbar.VibratorHelper -import com.android.systemui.util.time.SystemClock -import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope -import com.android.systemui.volume.dialog.sliders.shared.model.SliderInputEvent -import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderInputEventsViewModel -import com.google.android.material.slider.Slider -import com.google.android.msdl.domain.MSDLPlayer -import javax.inject.Inject -import kotlin.math.roundToInt -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach - -@VolumeDialogSliderScope -class VolumeDialogSliderHapticsViewBinder -@Inject -constructor( - private val inputEventsViewModel: VolumeDialogSliderInputEventsViewModel, - private val vibratorHelper: VibratorHelper, - private val msdlPlayer: MSDLPlayer, - private val systemClock: SystemClock, -) { - - fun CoroutineScope.bind(view: View) { - val sliderView = view.requireViewById<Slider>(R.id.volume_dialog_slider) - val hapticSliderPlugin = - HapticSliderPlugin( - slider = HapticSlider.Slider(sliderView), - vibratorHelper = vibratorHelper, - msdlPlayer = msdlPlayer, - systemClock = systemClock, - ) - hapticSliderPlugin.startInScope(this) - - sliderView.addOnChangeListener { _, value, fromUser -> - hapticSliderPlugin.onProgressChanged(value.roundToInt(), fromUser) - } - sliderView.addOnSliderTouchListener( - object : Slider.OnSliderTouchListener { - - override fun onStartTrackingTouch(slider: Slider) { - hapticSliderPlugin.onStartTrackingTouch() - } - - override fun onStopTrackingTouch(slider: Slider) { - hapticSliderPlugin.onStopTrackingTouch() - } - } - ) - - inputEventsViewModel.event - .onEach { - when (it) { - is SliderInputEvent.Button -> hapticSliderPlugin.onKeyDown() - is SliderInputEvent.Touch -> hapticSliderPlugin.onTouchEvent(it.event) - } - } - .launchIn(this) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt index d40302408dd6..21a392776235 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt @@ -16,96 +16,211 @@ package com.android.systemui.volume.dialog.sliders.ui -import android.annotation.SuppressLint +import android.graphics.drawable.Drawable import android.view.View -import androidx.dynamicanimation.animation.FloatPropertyCompat -import androidx.dynamicanimation.animation.SpringAnimation -import androidx.dynamicanimation.animation.SpringForce +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SliderDefaults +import androidx.compose.material3.SliderState +import androidx.compose.material3.VerticalSlider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.compose.ui.graphics.painter.DrawablePainter +import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel +import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.res.R import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope -import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderInputEventsViewModel -import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderStateModel +import com.android.systemui.volume.dialog.sliders.ui.compose.VolumeDialogSliderTrack +import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogOverscrollViewModel import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderViewModel -import com.google.android.material.slider.Slider -import com.google.android.material.slider.Slider.OnSliderTouchListener +import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider import javax.inject.Inject +import kotlin.math.round import kotlin.math.roundToInt -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.isActive @VolumeDialogSliderScope class VolumeDialogSliderViewBinder @Inject constructor( private val viewModel: VolumeDialogSliderViewModel, - private val inputViewModel: VolumeDialogSliderInputEventsViewModel, + private val overscrollViewModel: VolumeDialogOverscrollViewModel, + private val hapticsViewModelFactory: SliderHapticsViewModel.Factory, ) { + fun bind(view: View) { + val sliderComposeView: ComposeView = view.requireViewById(R.id.volume_dialog_slider) + sliderComposeView.setContent { + VolumeDialogSlider( + viewModel = viewModel, + overscrollViewModel = overscrollViewModel, + hapticsViewModelFactory = + if (com.android.systemui.Flags.hapticsForComposeSliders()) { + hapticsViewModelFactory + } else { + null + }, + ) + } + } +} - private val sliderValueProperty = - object : FloatPropertyCompat<Slider>("value") { - override fun getValue(slider: Slider): Float = slider.value +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) +@Composable +private fun VolumeDialogSlider( + viewModel: VolumeDialogSliderViewModel, + overscrollViewModel: VolumeDialogOverscrollViewModel, + hapticsViewModelFactory: SliderHapticsViewModel.Factory?, + modifier: Modifier = Modifier, +) { + + val colors = + SliderDefaults.colors( + thumbColor = MaterialTheme.colorScheme.primary, + activeTickColor = MaterialTheme.colorScheme.surfaceContainerHighest, + inactiveTickColor = MaterialTheme.colorScheme.primary, + activeTrackColor = MaterialTheme.colorScheme.primary, + inactiveTrackColor = MaterialTheme.colorScheme.surfaceContainerHighest, + ) + val collectedSliderState by viewModel.state.collectAsStateWithLifecycle(null) + val sliderState = collectedSliderState ?: return - override fun setValue(slider: Slider, value: Float) { - slider.value = value + val interactionSource = remember { MutableInteractionSource() } + val hapticsViewModel: SliderHapticsViewModel? = + hapticsViewModelFactory?.let { + rememberViewModel(traceName = "SliderHapticsViewModel") { + it.create( + interactionSource, + sliderState.valueRange, + Orientation.Vertical, + VolumeHapticsConfigsProvider.sliderHapticFeedbackConfig(sliderState.valueRange), + VolumeHapticsConfigsProvider.seekableSliderTrackerConfig, + ) } } - private val springForce = - SpringForce().apply { - stiffness = SpringForce.STIFFNESS_MEDIUM - dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY - } - @SuppressLint("ClickableViewAccessibility") - fun CoroutineScope.bind(view: View) { - var isInitialUpdate = true - val sliderView: Slider = view.requireViewById(R.id.volume_dialog_slider) - val animation = SpringAnimation(sliderView, sliderValueProperty) - animation.spring = springForce - sliderView.setOnTouchListener { _, event -> - inputViewModel.onTouchEvent(event) - false - } - sliderView.addOnChangeListener { _, value, fromUser -> - viewModel.setStreamVolume(value.roundToInt(), fromUser) + val state = + remember(sliderState.valueRange) { + SliderState( + value = sliderState.value, + valueRange = sliderState.valueRange, + steps = + (sliderState.valueRange.endInclusive - sliderState.valueRange.start - 1) + .toInt(), + ) + .apply { + onValueChangeFinished = { + viewModel.onStreamChangeFinished(value.roundToInt()) + hapticsViewModel?.onValueChangeEnded() + } + setOnValueChangeListener { + value = it + hapticsViewModel?.addVelocityDataPoint(it) + overscrollViewModel.setSlider( + value = value, + min = valueRange.start, + max = valueRange.endInclusive, + ) + viewModel.setStreamVolume(it, true) + } + } } - sliderView.addOnSliderTouchListener( - object : OnSliderTouchListener { - override fun onStartTrackingTouch(slider: Slider) {} + var lastDiscreteStep by remember { mutableFloatStateOf(round(sliderState.value)) } + LaunchedEffect(sliderState.value) { + state.value = sliderState.value + snapshotFlow { sliderState.value } + .map { round(it) } + .filter { it != lastDiscreteStep } + .distinctUntilChanged() + .collect { discreteStep -> + lastDiscreteStep = discreteStep + hapticsViewModel?.onValueChange(discreteStep) + } + } - override fun onStopTrackingTouch(slider: Slider) { - viewModel.onStreamChangeFinished(slider.value.roundToInt()) + VerticalSlider( + state = state, + enabled = !sliderState.isDisabled, + reverseDirection = true, + colors = colors, + interactionSource = interactionSource, + modifier = + modifier.pointerInput(Unit) { + coroutineScope { + val currentContext = currentCoroutineContext() + awaitPointerEventScope { + while (currentContext.isActive) { + viewModel.onTouchEvent(awaitPointerEvent()) + } + } } - } - ) + }, + track = { + VolumeDialogSliderTrack( + state, + colors = colors, + isEnabled = !sliderState.isDisabled, + activeTrackEndIcon = { iconsState -> + VolumeIcon(sliderState.icon, iconsState.isActiveTrackEndIconVisible) + }, + inactiveTrackEndIcon = { iconsState -> + VolumeIcon(sliderState.icon, !iconsState.isActiveTrackEndIconVisible) + }, + ) + }, + ) +} - viewModel.isDisabledByZenMode.onEach { sliderView.isEnabled = !it }.launchIn(this) - viewModel.state - .onEach { - sliderView.setModel(it, animation, isInitialUpdate) - isInitialUpdate = false - } - .launchIn(this) +@Composable +private fun BoxScope.VolumeIcon( + drawable: Drawable, + isVisible: Boolean, + modifier: Modifier = Modifier, +) { + AnimatedVisibility( + visible = isVisible, + enter = fadeIn(animationSpec = tween(durationMillis = 50)), + exit = fadeOut(animationSpec = tween(durationMillis = 50)), + modifier = modifier.align(Alignment.Center).size(40.dp).padding(10.dp), + ) { + Icon(painter = DrawablePainter(drawable), contentDescription = null) } +} - @SuppressLint("UseCompatLoadingForDrawables") - private fun Slider.setModel( - model: VolumeDialogSliderStateModel, - animation: SpringAnimation, - isInitialUpdate: Boolean, - ) { - valueFrom = model.minValue - animation.setMinValue(model.minValue) - valueTo = model.maxValue - animation.setMaxValue(model.maxValue) - // coerce the current value to the new value range before animating it. This prevents - // animating from the value that is outside of current [valueFrom, valueTo]. - value = value.coerceIn(valueFrom, valueTo) - trackIconActiveStart = model.icon - if (isInitialUpdate) { - value = model.value - } else { - animation.animateToFinalPosition(model.value) - } +@OptIn(ExperimentalMaterial3Api::class) +fun SliderState.setOnValueChangeListener(onValueChange: ((Float) -> Unit)?) { + with(javaClass.getDeclaredField("onValueChange")) { + val oldIsAccessible = isAccessible + AutoCloseable { isAccessible = oldIsAccessible } + .use { + isAccessible = true + set(this@setOnValueChangeListener, onValueChange) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt index 75d427acc05b..c66955a0c187 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt @@ -71,7 +71,6 @@ constructor(private val viewModel: VolumeDialogSlidersViewModel) { viewsToAnimate: Array<View>, ) { with(component.sliderViewBinder()) { bind(sliderContainer) } - with(component.sliderHapticsViewBinder()) { bind(sliderContainer) } with(component.overscrollViewBinder()) { bind(sliderContainer, viewsToAnimate) } } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt new file mode 100644 index 000000000000..1dd9ddac79be --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt @@ -0,0 +1,347 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.dialog.sliders.ui.compose + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.width +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.SliderColors +import androidx.compose.material3.SliderDefaults +import androidx.compose.material3.SliderState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasurePolicy +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastFilter +import androidx.compose.ui.util.fastFirst +import kotlin.math.min + +@Composable +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) +fun VolumeDialogSliderTrack( + sliderState: SliderState, + colors: SliderColors, + isEnabled: Boolean, + modifier: Modifier = Modifier, + thumbTrackGapSize: Dp = 6.dp, + trackCornerSize: Dp = 12.dp, + trackInsideCornerSize: Dp = 2.dp, + trackSize: Dp = 40.dp, + activeTrackStartIcon: (@Composable BoxScope.(iconsState: SliderIconsState) -> Unit)? = null, + activeTrackEndIcon: (@Composable BoxScope.(iconsState: SliderIconsState) -> Unit)? = null, + inactiveTrackStartIcon: (@Composable BoxScope.(iconsState: SliderIconsState) -> Unit)? = null, + inactiveTrackEndIcon: (@Composable BoxScope.(iconsState: SliderIconsState) -> Unit)? = null, +) { + val measurePolicy = remember(sliderState) { TrackMeasurePolicy(sliderState) } + Layout( + measurePolicy = measurePolicy, + content = { + SliderDefaults.Track( + sliderState = sliderState, + colors = colors, + enabled = isEnabled, + trackCornerSize = trackCornerSize, + trackInsideCornerSize = trackInsideCornerSize, + drawStopIndicator = null, + thumbTrackGapSize = thumbTrackGapSize, + drawTick = { _, _ -> }, + modifier = Modifier.width(trackSize).layoutId(Contents.Track), + ) + + TrackIcon( + icon = activeTrackStartIcon, + contentsId = Contents.Active.TrackStartIcon, + isEnabled = isEnabled, + colors = colors, + state = measurePolicy, + ) + TrackIcon( + icon = activeTrackEndIcon, + contentsId = Contents.Active.TrackEndIcon, + isEnabled = isEnabled, + colors = colors, + state = measurePolicy, + ) + TrackIcon( + icon = inactiveTrackStartIcon, + contentsId = Contents.Inactive.TrackStartIcon, + isEnabled = isEnabled, + colors = colors, + state = measurePolicy, + ) + TrackIcon( + icon = inactiveTrackEndIcon, + contentsId = Contents.Inactive.TrackEndIcon, + isEnabled = isEnabled, + colors = colors, + state = measurePolicy, + ) + }, + modifier = modifier, + ) +} + +@Composable +private fun TrackIcon( + icon: (@Composable BoxScope.(sliderIconsState: SliderIconsState) -> Unit)?, + isEnabled: Boolean, + contentsId: Contents, + state: SliderIconsState, + colors: SliderColors, + modifier: Modifier = Modifier, +) { + icon ?: return + Box(modifier = modifier.layoutId(contentsId).fillMaxSize()) { + CompositionLocalProvider( + LocalContentColor provides contentsId.getColor(colors, isEnabled) + ) { + icon(state) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +private class TrackMeasurePolicy(private val sliderState: SliderState) : + MeasurePolicy, SliderIconsState { + + private val isVisible: Map<Contents, MutableState<Boolean>> = + mutableMapOf( + Contents.Active.TrackStartIcon to mutableStateOf(false), + Contents.Active.TrackEndIcon to mutableStateOf(false), + Contents.Inactive.TrackStartIcon to mutableStateOf(false), + Contents.Inactive.TrackEndIcon to mutableStateOf(false), + ) + + override val isActiveTrackStartIconVisible: Boolean + get() = isVisible.getValue(Contents.Active.TrackStartIcon).value + + override val isActiveTrackEndIconVisible: Boolean + get() = isVisible.getValue(Contents.Active.TrackEndIcon).value + + override val isInactiveTrackStartIconVisible: Boolean + get() = isVisible.getValue(Contents.Inactive.TrackStartIcon).value + + override val isInactiveTrackEndIconVisible: Boolean + get() = isVisible.getValue(Contents.Inactive.TrackEndIcon).value + + override fun MeasureScope.measure( + measurables: List<Measurable>, + constraints: Constraints, + ): MeasureResult { + val track = measurables.fastFirst { it.layoutId == Contents.Track }.measure(constraints) + + val iconSize = min(track.width, track.height) + val iconConstraints = constraints.copy(maxWidth = iconSize, maxHeight = iconSize) + + val icons = + measurables + .fastFilter { it.layoutId != Contents.Track } + .associateBy( + keySelector = { it.layoutId as Contents }, + valueTransform = { it.measure(iconConstraints) }, + ) + + return layout(track.width, track.height) { + with(Contents.Track) { + performPlacing( + placeable = track, + width = track.width, + height = track.height, + sliderState = sliderState, + ) + } + + for (iconLayoutId in icons.keys) { + with(iconLayoutId) { + performPlacing( + placeable = icons.getValue(iconLayoutId), + width = track.width, + height = track.height, + sliderState = sliderState, + ) + + isVisible.getValue(iconLayoutId).value = + isVisible( + placeable = icons.getValue(iconLayoutId), + width = track.width, + height = track.height, + sliderState = sliderState, + ) + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +private sealed interface Contents { + + data object Track : Contents { + override fun Placeable.PlacementScope.performPlacing( + placeable: Placeable, + width: Int, + height: Int, + sliderState: SliderState, + ) = placeable.place(x = 0, y = 0) + + override fun isVisible( + placeable: Placeable, + width: Int, + height: Int, + sliderState: SliderState, + ) = true + + override fun getColor(sliderColors: SliderColors, isEnabled: Boolean): Color = + error("Unsupported") + } + + interface Active : Contents { + override fun getColor(sliderColors: SliderColors, isEnabled: Boolean): Color { + return if (isEnabled) { + sliderColors.activeTickColor + } else { + sliderColors.disabledActiveTickColor + } + } + + data object TrackStartIcon : Active { + override fun Placeable.PlacementScope.performPlacing( + placeable: Placeable, + width: Int, + height: Int, + sliderState: SliderState, + ) = + placeable.place( + x = 0, + y = (height * (1 - sliderState.coercedValueAsFraction)).toInt(), + ) + + override fun isVisible( + placeable: Placeable, + width: Int, + height: Int, + sliderState: SliderState, + ): Boolean = (height * (sliderState.coercedValueAsFraction)).toInt() > placeable.height + } + + data object TrackEndIcon : Active { + override fun Placeable.PlacementScope.performPlacing( + placeable: Placeable, + width: Int, + height: Int, + sliderState: SliderState, + ) = placeable.place(x = 0, y = (height - placeable.height)) + + override fun isVisible( + placeable: Placeable, + width: Int, + height: Int, + sliderState: SliderState, + ): Boolean = (height * (sliderState.coercedValueAsFraction)).toInt() > placeable.height + } + } + + interface Inactive : Contents { + + override fun getColor(sliderColors: SliderColors, isEnabled: Boolean): Color { + return if (isEnabled) { + sliderColors.inactiveTickColor + } else { + sliderColors.disabledInactiveTickColor + } + } + + data object TrackStartIcon : Inactive { + override fun Placeable.PlacementScope.performPlacing( + placeable: Placeable, + width: Int, + height: Int, + sliderState: SliderState, + ) { + placeable.place(x = 0, y = 0) + } + + override fun isVisible( + placeable: Placeable, + width: Int, + height: Int, + sliderState: SliderState, + ): Boolean = + (height * (1 - sliderState.coercedValueAsFraction)).toInt() > placeable.height + } + + data object TrackEndIcon : Inactive { + override fun Placeable.PlacementScope.performPlacing( + placeable: Placeable, + width: Int, + height: Int, + sliderState: SliderState, + ) { + placeable.place( + x = 0, + y = + (height * (1 - sliderState.coercedValueAsFraction)).toInt() - + placeable.height, + ) + } + + override fun isVisible( + placeable: Placeable, + width: Int, + height: Int, + sliderState: SliderState, + ): Boolean = + (height * (1 - sliderState.coercedValueAsFraction)).toInt() > placeable.height + } + } + + fun Placeable.PlacementScope.performPlacing( + placeable: Placeable, + width: Int, + height: Int, + sliderState: SliderState, + ) + + fun isVisible(placeable: Placeable, width: Int, height: Int, sliderState: SliderState): Boolean + + fun getColor(sliderColors: SliderColors, isEnabled: Boolean): Color +} + +/** Provides visibility state for each of the Slider's icons. */ +interface SliderIconsState { + val isActiveTrackStartIconVisible: Boolean + val isActiveTrackEndIconVisible: Boolean + val isInactiveTrackStartIconVisible: Boolean + val isInactiveTrackEndIconVisible: Boolean +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogOverscrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogOverscrollViewModel.kt index 0d41860d9f57..0fdf5d6266d0 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogOverscrollViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogOverscrollViewModel.kt @@ -95,18 +95,17 @@ constructor( private fun overscrollEvents(direction: Float): Flow<OverscrollEventModel> { var startPosition: Float? = null return inputEventsInteractor.event - .mapNotNull { (it as? SliderInputEvent.Touch)?.event } + .mapNotNull { it as? SliderInputEvent.Touch } .transform { touchEvent -> // Skip events from inside the slider bounds for the case when the user adjusts - // slider - // towards max when the slider is already on max value. - if (touchEvent.isFinalEvent()) { + // slider towards max when the slider is already on max value. + if (touchEvent is SliderInputEvent.Touch.End) { startPosition = null emit(OverscrollEventModel.Animate(0f)) return@transform } val currentStartPosition = startPosition - val newPosition: Float = touchEvent.rawY + val newPosition: Float = touchEvent.y if (currentStartPosition == null) { startPosition = newPosition } else { @@ -126,11 +125,6 @@ constructor( } } - /** @return true when the [MotionEvent] indicates the end of the gesture. */ - private fun MotionEvent.isFinalEvent(): Boolean { - return actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL - } - /** Models overscroll event */ sealed interface OverscrollEventModel { diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModel.kt deleted file mode 100644 index 755776ac9723..000000000000 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModel.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.volume.dialog.sliders.ui.viewmodel - -import android.view.MotionEvent -import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog -import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope -import com.android.systemui.volume.dialog.sliders.domain.interactor.VolumeDialogSliderInputEventsInteractor -import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.stateIn - -@VolumeDialogSliderScope -class VolumeDialogSliderInputEventsViewModel -@Inject -constructor( - @VolumeDialog private val coroutineScope: CoroutineScope, - private val interactor: VolumeDialogSliderInputEventsInteractor, -) { - - val event = - interactor.event.stateIn(coroutineScope, SharingStarted.Eagerly, null).filterNotNull() - - fun onTouchEvent(event: MotionEvent) { - interactor.onTouchEvent(event) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt index 8df9e788905c..b01046b377b0 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt @@ -20,17 +20,20 @@ import android.graphics.drawable.Drawable import com.android.systemui.volume.dialog.shared.model.VolumeDialogStreamModel data class VolumeDialogSliderStateModel( - val minValue: Float, - val maxValue: Float, val value: Float, + val isDisabled: Boolean, + val valueRange: ClosedFloatingPointRange<Float>, val icon: Drawable, ) -fun VolumeDialogStreamModel.toStateModel(icon: Drawable): VolumeDialogSliderStateModel { +fun VolumeDialogStreamModel.toStateModel( + isDisabled: Boolean, + icon: Drawable, +): VolumeDialogSliderStateModel { return VolumeDialogSliderStateModel( - minValue = levelMin.toFloat(), value = level.toFloat(), - maxValue = levelMax.toFloat(), + isDisabled = isDisabled, + valueRange = levelMin.toFloat()..levelMax.toFloat(), icon = icon, ) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt index a752f1f78e74..e89d5ab53560 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt @@ -16,6 +16,9 @@ package com.android.systemui.volume.dialog.sliders.ui.viewmodel +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.PointerEvent +import androidx.compose.ui.input.pointer.PointerEventType import com.android.systemui.util.time.SystemClock import com.android.systemui.volume.Events import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog @@ -23,20 +26,23 @@ import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogVisibili import com.android.systemui.volume.dialog.shared.VolumeDialogLogger import com.android.systemui.volume.dialog.shared.model.VolumeDialogStreamModel import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope +import com.android.systemui.volume.dialog.sliders.domain.interactor.VolumeDialogSliderInputEventsInteractor import com.android.systemui.volume.dialog.sliders.domain.interactor.VolumeDialogSliderInteractor import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType +import com.android.systemui.volume.dialog.sliders.shared.model.SliderInputEvent import javax.inject.Inject +import kotlin.math.roundToInt import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn @@ -62,6 +68,7 @@ constructor( private val visibilityInteractor: VolumeDialogVisibilityInteractor, @VolumeDialog private val coroutineScope: CoroutineScope, private val volumeDialogSliderIconProvider: VolumeDialogSliderIconProvider, + private val inputEventsInteractor: VolumeDialogSliderInputEventsInteractor, private val systemClock: SystemClock, private val logger: VolumeDialogLogger, ) { @@ -77,11 +84,12 @@ constructor( .stateIn(coroutineScope, SharingStarted.Eagerly, null) .filterNotNull() - val isDisabledByZenMode: Flow<Boolean> = interactor.isDisabledByZenMode val state: Flow<VolumeDialogSliderStateModel> = - model - .flatMapLatest { streamModel -> - with(streamModel) { + combine( + interactor.isDisabledByZenMode, + model, + model.flatMapLatest { streamModel -> + with(streamModel) { val isMuted = muteSupported && muted when (sliderType) { is VolumeDialogSliderType.Stream -> @@ -101,7 +109,9 @@ constructor( } } } - .map { icon -> streamModel.toStateModel(icon) } + }, + ) { isDisabledByZenMode, model, icon -> + model.toStateModel(icon = icon, isDisabled = isDisabledByZenMode) } .stateIn(coroutineScope, SharingStarted.Eagerly, null) .filterNotNull() @@ -116,11 +126,14 @@ constructor( .launchIn(coroutineScope) } - fun setStreamVolume(volume: Int, fromUser: Boolean) { + fun setStreamVolume(volume: Float, fromUser: Boolean) { if (fromUser) { visibilityInteractor.resetDismissTimeout() userVolumeUpdates.value = - VolumeUpdate(newVolumeLevel = volume, timestampMillis = getTimestampMillis()) + VolumeUpdate( + newVolumeLevel = volume.roundToInt(), + timestampMillis = getTimestampMillis(), + ) } } @@ -128,6 +141,28 @@ constructor( logger.onVolumeSliderAdjustmentFinished(volume = volume, stream = sliderType.audioStream) } + fun onTouchEvent(pointerEvent: PointerEvent) { + val position: Offset = pointerEvent.changes.first().position + when (pointerEvent.type) { + PointerEventType.Press -> + inputEventsInteractor.onTouchEvent( + SliderInputEvent.Touch.Start(position.x, position.y) + ) + PointerEventType.Move -> + inputEventsInteractor.onTouchEvent( + SliderInputEvent.Touch.Move(position.x, position.y) + ) + PointerEventType.Scroll -> + inputEventsInteractor.onTouchEvent( + SliderInputEvent.Touch.Move(position.x, position.y) + ) + PointerEventType.Release -> + inputEventsInteractor.onTouchEvent( + SliderInputEvent.Touch.End(position.x, position.y) + ) + } + } + private fun getTimestampMillis(): Long = systemClock.uptimeMillis() private data class VolumeUpdate(val newVolumeLevel: Int, val timestampMillis: Long) diff --git a/packages/SystemUI/src/com/android/systemui/volume/haptics/ui/VolumeHapticsConfigsProvider.kt b/packages/SystemUI/src/com/android/systemui/volume/haptics/ui/VolumeHapticsConfigsProvider.kt new file mode 100644 index 000000000000..92e9bf2d1ffc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/haptics/ui/VolumeHapticsConfigsProvider.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.haptics.ui + +import com.android.systemui.haptics.slider.SeekableSliderTrackerConfig +import com.android.systemui.haptics.slider.SliderHapticFeedbackConfig + +object VolumeHapticsConfigsProvider { + + fun sliderHapticFeedbackConfig( + valueRange: ClosedFloatingPointRange<Float> + ): SliderHapticFeedbackConfig { + val sliderStepSize = 1f / (valueRange.endInclusive - valueRange.start) + return SliderHapticFeedbackConfig( + lowerBookendScale = 0.2f, + progressBasedDragMinScale = 0.2f, + progressBasedDragMaxScale = 0.5f, + deltaProgressForDragThreshold = 0f, + additionalVelocityMaxBump = 0.2f, + maxVelocityToScale = 0.1f, /* slider progress(from 0 to 1) per sec */ + sliderStepSize = sliderStepSize, + ) + } + + val seekableSliderTrackerConfig = + SeekableSliderTrackerConfig(lowerBookendThreshold = 0f, upperBookendThreshold = 1f) +} diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/GradientColorWallpaper.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/GradientColorWallpaper.kt index c1fb0e80cafc..760e94c72f19 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/GradientColorWallpaper.kt +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/GradientColorWallpaper.kt @@ -16,6 +16,7 @@ package com.android.systemui.wallpapers +import android.app.Flags import android.graphics.Canvas import android.graphics.Paint import android.service.wallpaper.WallpaperService @@ -26,7 +27,15 @@ import androidx.core.graphics.toRectF /** A wallpaper that shows a static gradient color image wallpaper. */ class GradientColorWallpaper : WallpaperService() { - override fun onCreateEngine(): Engine = GradientColorWallpaperEngine() + override fun onCreateEngine(): Engine = + if (Flags.enableConnectedDisplaysWallpaper()) { + GradientColorWallpaperEngine() + } else { + EmptyWallpaperEngine() + } + + /** Empty engine used when the feature flag is disabled. */ + inner class EmptyWallpaperEngine : Engine() inner class GradientColorWallpaperEngine : Engine() { init { diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt index 8487ee751948..ec74f4f47bc9 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt @@ -36,4 +36,5 @@ class NoopWallpaperRepository @Inject constructor() : WallpaperRepository { override val wallpaperInfo: StateFlow<WallpaperInfo?> = MutableStateFlow(null).asStateFlow() override val wallpaperSupportsAmbientMode = flowOf(false) override var rootView: View? = null + override val shouldSendFocalArea: StateFlow<Boolean> = MutableStateFlow(false).asStateFlow() } diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt index ed43f8323c31..9794c619041e 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt @@ -31,7 +31,6 @@ import com.android.systemui.Flags import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.keyguard.data.repository.KeyguardClockRepository import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.shared.Flags.ambientAod import com.android.systemui.user.data.model.SelectedUserModel @@ -45,6 +44,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine @@ -65,6 +65,9 @@ interface WallpaperRepository { /** Set rootView to get its windowToken afterwards */ var rootView: View? + + /** when we use magic portrait wallpapers, we should always get its bounds from keyguard */ + val shouldSendFocalArea: StateFlow<Boolean> } @SysUISingleton @@ -76,7 +79,6 @@ constructor( broadcastDispatcher: BroadcastDispatcher, userRepository: UserRepository, keyguardRepository: KeyguardRepository, - keyguardClockRepository: KeyguardClockRepository, private val wallpaperManager: WallpaperManager, context: Context, ) : WallpaperRepository { @@ -97,27 +99,7 @@ constructor( // Only update the wallpaper status once the user selection has finished. .filter { it.selectionStatus == SelectionStatus.SELECTION_COMPLETE } - /** The bottom of notification stack respect to the top of screen. */ - private val notificationStackAbsoluteBottom: StateFlow<Float> = - keyguardRepository.notificationStackAbsoluteBottom - - /** The top of shortcut respect to the top of screen. */ - private val shortcutAbsoluteTop: StateFlow<Float> = keyguardRepository.shortcutAbsoluteTop - - /** - * The top of notification stack to give a default state of lockscreen remaining space for - * states with notifications to compare with. It's the bottom of smartspace date and weather - * smartspace in small clock state, plus proper bottom margin. - */ - private val notificationStackDefaultTop = keyguardClockRepository.notificationDefaultTop @VisibleForTesting var sendLockscreenLayoutJob: Job? = null - private val lockscreenRemainingSpaceWithNotification: Flow<Triple<Float, Float, Float>> = - combine( - notificationStackAbsoluteBottom, - notificationStackDefaultTop, - shortcutAbsoluteTop, - ::Triple, - ) override val wallpaperInfo: StateFlow<WallpaperInfo?> = if (!wallpaperManager.isWallpaperSupported) { @@ -140,15 +122,16 @@ constructor( override var rootView: View? = null - val shouldSendNotificationLayout = + override val shouldSendFocalArea = wallpaperInfo .map { - val shouldSendNotificationLayout = shouldSendNotificationLayout(it) + val shouldSendNotificationLayout = + it?.component?.className == MAGIC_PORTRAIT_CLASSNAME if (shouldSendNotificationLayout) { sendLockscreenLayoutJob = scope.launch { - lockscreenRemainingSpaceWithNotification.collect { - (notificationBottom, notificationDefaultTop, shortcutTop) -> + keyguardRepository.wallpaperFocalAreaBounds.collect { + wallpaperFocalAreaBounds -> wallpaperManager.sendWallpaperCommand( /* windowToken = */ rootView?.windowToken, /* action = */ WallpaperManager @@ -157,14 +140,22 @@ constructor( /* y = */ 0, /* z = */ 0, /* extras = */ Bundle().apply { - putFloat("screenLeft", 0F) - putFloat("smartspaceBottom", notificationDefaultTop) - putFloat("notificationBottom", notificationBottom) putFloat( - "screenRight", - context.resources.displayMetrics.widthPixels.toFloat(), + "wallpaperFocalAreaLeft", + wallpaperFocalAreaBounds.left, + ) + putFloat( + "wallpaperFocalAreaRight", + wallpaperFocalAreaBounds.right, + ) + putFloat( + "wallpaperFocalAreaTop", + wallpaperFocalAreaBounds.top, + ) + putFloat( + "wallpaperFocalAreaBottom", + wallpaperFocalAreaBounds.bottom, ) - putFloat("shortCutTop", shortcutTop) }, ) } @@ -176,10 +167,9 @@ constructor( } .stateIn( scope, - // Always be listening for wallpaper changes. - if (Flags.magicPortraitWallpapers()) SharingStarted.Eagerly - else SharingStarted.Lazily, - initialValue = false, + // Always be listening for wallpaper changes when magic portrait flag is on + if (Flags.magicPortraitWallpapers()) SharingStarted.Eagerly else WhileSubscribed(), + initialValue = Flags.magicPortraitWallpapers(), ) private suspend fun getWallpaper(selectedUser: SelectedUserModel): WallpaperInfo? { @@ -188,14 +178,6 @@ constructor( } } - private fun shouldSendNotificationLayout(wallpaperInfo: WallpaperInfo?): Boolean { - return if (wallpaperInfo != null && wallpaperInfo.component != null) { - wallpaperInfo.component!!.className == MAGIC_PORTRAIT_CLASSNAME - } else { - false - } - } - companion object { const val MAGIC_PORTRAIT_CLASSNAME = "com.google.android.apps.magicportrait.service.MagicPortraitWallpaperService" diff --git a/packages/SystemUI/src/com/google/android/systemui/lowlightclock/LowLightClockDreamService.java b/packages/SystemUI/src/com/google/android/systemui/lowlightclock/LowLightClockDreamService.java new file mode 100644 index 000000000000..8a5f7eaf8776 --- /dev/null +++ b/packages/SystemUI/src/com/google/android/systemui/lowlightclock/LowLightClockDreamService.java @@ -0,0 +1,161 @@ +/* + * 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.google.android.systemui.lowlightclock; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.annotation.Nullable; +import android.service.dreams.DreamService; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextClock; +import android.widget.TextView; + +import com.android.dream.lowlight.LowLightTransitionCoordinator; +import com.android.systemui.lowlightclock.ChargingStatusProvider; +import com.android.systemui.lowlightclock.LowLightClockAnimationProvider; +import com.android.systemui.lowlightclock.LowLightDisplayController; +import com.android.systemui.res.R; + +import java.util.Optional; + +import javax.inject.Inject; +import javax.inject.Provider; + +/** + * A dark themed text clock dream to be shown when the device is in a low light environment. + */ +public class LowLightClockDreamService extends DreamService implements + LowLightTransitionCoordinator.LowLightExitListener { + private static final String TAG = "LowLightClockDreamService"; + + private final ChargingStatusProvider mChargingStatusProvider; + private final LowLightDisplayController mDisplayController; + private final LowLightClockAnimationProvider mAnimationProvider; + private final LowLightTransitionCoordinator mLowLightTransitionCoordinator; + private boolean mIsDimBrightnessSupported = false; + + private TextView mChargingStatusTextView; + private TextClock mTextClock; + @Nullable + private Animator mAnimationIn; + @Nullable + private Animator mAnimationOut; + + @Inject + public LowLightClockDreamService( + ChargingStatusProvider chargingStatusProvider, + LowLightClockAnimationProvider animationProvider, + LowLightTransitionCoordinator lowLightTransitionCoordinator, + Optional<Provider<LowLightDisplayController>> displayController) { + super(); + + mAnimationProvider = animationProvider; + mDisplayController = displayController.map(Provider::get).orElse(null); + mChargingStatusProvider = chargingStatusProvider; + mLowLightTransitionCoordinator = lowLightTransitionCoordinator; + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + + setInteractive(false); + setFullscreen(true); + + setContentView(LayoutInflater.from(getApplicationContext()).inflate( + R.layout.low_light_clock_dream, null)); + + mTextClock = findViewById(R.id.low_light_text_clock); + + mChargingStatusTextView = findViewById(R.id.charging_status_text_view); + + mChargingStatusProvider.startUsing(this::updateChargingMessage); + + mLowLightTransitionCoordinator.setLowLightExitListener(this); + } + + @Override + public void onDreamingStarted() { + mAnimationIn = mAnimationProvider.provideAnimationIn(mTextClock, mChargingStatusTextView); + mAnimationIn.start(); + + if (mDisplayController != null) { + mIsDimBrightnessSupported = mDisplayController.isDisplayBrightnessModeSupported(); + + if (mIsDimBrightnessSupported) { + Log.v(TAG, "setting dim brightness state"); + mDisplayController.setDisplayBrightnessModeEnabled(true); + } else { + Log.v(TAG, "dim brightness not supported"); + } + } + } + + @Override + public void onDreamingStopped() { + if (mIsDimBrightnessSupported) { + Log.v(TAG, "clearing dim brightness state"); + mDisplayController.setDisplayBrightnessModeEnabled(false); + } + } + + @Override + public void onWakeUp() { + if (mAnimationIn != null) { + mAnimationIn.cancel(); + } + mAnimationOut = mAnimationProvider.provideAnimationOut(mTextClock, mChargingStatusTextView); + mAnimationOut.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animator) { + LowLightClockDreamService.super.onWakeUp(); + } + }); + mAnimationOut.start(); + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + if (mAnimationOut != null) { + mAnimationOut.cancel(); + } + + mChargingStatusProvider.stopUsing(); + + mLowLightTransitionCoordinator.setLowLightExitListener(null); + } + + private void updateChargingMessage(boolean showChargingStatus, String chargingStatusMessage) { + mChargingStatusTextView.setText(chargingStatusMessage); + mChargingStatusTextView.setVisibility(showChargingStatus ? View.VISIBLE : View.INVISIBLE); + } + + @Override + public Animator onBeforeExitLowLight() { + mAnimationOut = mAnimationProvider.provideAnimationOut(mTextClock, mChargingStatusTextView); + mAnimationOut.start(); + + // Return the animator so that the transition coordinator waits for the low light exit + // animations to finish before entering low light, as otherwise the default DreamActivity + // animation plays immediately and there's no time for this animation to play. + return mAnimationOut; + } +} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index 2645811fa4ad..312d2ffd74e4 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -38,6 +38,7 @@ import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_STATE_CANCELL import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_STATE_STOPPED; import static com.android.keyguard.KeyguardUpdateMonitor.DEFAULT_CANCEL_SIGNAL_TIMEOUT; import static com.android.keyguard.KeyguardUpdateMonitor.HAL_POWER_PRESS_TIMEOUT; +import static com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2; import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED; import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN; @@ -139,6 +140,7 @@ import com.android.systemui.biometrics.AuthController; import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider; import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor; import com.android.systemui.deviceentry.data.repository.FaceWakeUpTriggersConfig; import com.android.systemui.deviceentry.data.repository.FaceWakeUpTriggersConfigImpl; import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor; @@ -180,6 +182,9 @@ import org.mockito.MockitoAnnotations; import org.mockito.MockitoSession; import org.mockito.quality.Strictness; +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -189,9 +194,6 @@ import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import platform.test.runner.parameterized.ParameterizedAndroidJunit4; -import platform.test.runner.parameterized.Parameters; - @SmallTest @RunWith(ParameterizedAndroidJunit4.class) @TestableLooper.RunWithLooper @@ -304,6 +306,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { private JavaAdapter mJavaAdapter; @Mock private SceneInteractor mSceneInteractor; + @Mock + private CommunalSceneInteractor mCommunalSceneInteractor; @Captor private ArgumentCaptor<FaceAuthenticationListener> mFaceAuthenticationListener; @@ -1084,6 +1088,49 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } @Test + @EnableFlags(FLAG_GLANCEABLE_HUB_V2) + public void udfpsStopsListeningWhenCommunalShowing() { + // GIVEN keyguard showing + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); + mKeyguardUpdateMonitor.setKeyguardShowing(true, false); + + assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(true)).isTrue(); + + // WHEN communal is shown + mKeyguardUpdateMonitor.onCommunalShowingChanged(true); + + // THEN shouldn't listen for fingerprint + assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(true)).isFalse(); + + // WHEN alternate bouncer shows on top of communal, we should listen for fingerprint + mKeyguardUpdateMonitor.setAlternateBouncerVisibility(true); + assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(true)).isTrue(); + + // WHEN communal is hidden + mKeyguardUpdateMonitor.onCommunalShowingChanged(false); + mKeyguardUpdateMonitor.setAlternateBouncerVisibility(false); + + // THEN listen for fingerprint + assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(true)).isTrue(); + } + + @Test + @EnableFlags(FLAG_GLANCEABLE_HUB_V2) + public void sfpsNotAffectedByCommunalShowing() { + // GIVEN keyguard showing + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); + mKeyguardUpdateMonitor.setKeyguardShowing(true, false); + + assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue(); + + // WHEN communal is shown + mKeyguardUpdateMonitor.onCommunalShowingChanged(true); + + // THEN we should still listen for fingerprint if not UDFPS + assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue(); + } + + @Test public void testFingerprintPowerPressed_restartsFingerprintListeningStateWithDelay() { mKeyguardUpdateMonitor.mFingerprintAuthenticationCallback .onAuthenticationError(FingerprintManager.BIOMETRIC_ERROR_POWER_PRESSED, ""); @@ -2669,7 +2716,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mTaskStackChangeListeners, mSelectedUserInteractor, mActivityTaskManager, () -> mAlternateBouncerInteractor, () -> mJavaAdapter, - () -> mSceneInteractor); + () -> mSceneInteractor, + mCommunalSceneInteractor); setAlternateBouncerVisibility(false); setPrimaryBouncerVisibility(false); setStrongAuthTracker(KeyguardUpdateMonitorTest.this.mStrongAuthTracker); diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java index 057ddcd54e68..8bfd2545ff2b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java @@ -21,7 +21,7 @@ import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ import static com.android.systemui.accessibility.AccessibilityLogger.MagnificationSettingsEvent; import static com.android.systemui.accessibility.WindowMagnificationSettings.MagnificationSize; -import static com.android.systemui.recents.OverviewProxyService.OverviewProxyListener; +import static com.android.systemui.recents.LauncherProxyService.LauncherProxyListener; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_MAGNIFICATION_OVERLAP; import static org.mockito.ArgumentMatchers.any; @@ -53,7 +53,7 @@ import androidx.test.filters.SmallTest; import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.systemui.SysuiTestCase; import com.android.systemui.model.SysUiState; -import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.recents.LauncherProxyService; import com.android.systemui.settings.FakeDisplayTracker; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.util.settings.SecureSettings; @@ -80,13 +80,13 @@ public class MagnificationTest extends SysuiTestCase { @Mock private IMagnificationConnectionCallback mConnectionCallback; @Mock - private OverviewProxyService mOverviewProxyService; + private LauncherProxyService mLauncherProxyService; @Mock private SecureSettings mSecureSettings; private CommandQueue mCommandQueue; private MagnificationImpl mMagnification; - private OverviewProxyListener mOverviewProxyListener; + private LauncherProxyListener mLauncherProxyListener; private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext); @Mock @@ -130,7 +130,7 @@ public class MagnificationTest extends SysuiTestCase { mMagnification = new MagnificationImpl(getContext(), getContext().getMainThreadHandler(), mContext.getMainExecutor(), mCommandQueue, mModeSwitchesController, - mSysUiState, mOverviewProxyService, mSecureSettings, mDisplayTracker, + mSysUiState, mLauncherProxyService, mSecureSettings, mDisplayTracker, getContext().getSystemService(DisplayManager.class), mA11yLogger, mIWindowManager, getContext().getSystemService(AccessibilityManager.class), mViewCaptureAwareWindowManager); @@ -140,10 +140,10 @@ public class MagnificationTest extends SysuiTestCase { mContext.getSystemService(DisplayManager.class), mMagnificationSettingsController); mMagnification.start(); - final ArgumentCaptor<OverviewProxyListener> listenerArgumentCaptor = - ArgumentCaptor.forClass(OverviewProxyListener.class); - verify(mOverviewProxyService).addCallback(listenerArgumentCaptor.capture()); - mOverviewProxyListener = listenerArgumentCaptor.getValue(); + final ArgumentCaptor<LauncherProxyListener> listenerArgumentCaptor = + ArgumentCaptor.forClass(LauncherProxyListener.class); + verify(mLauncherProxyService).addCallback(listenerArgumentCaptor.capture()); + mLauncherProxyListener = listenerArgumentCaptor.getValue(); } @Test @@ -336,7 +336,7 @@ public class MagnificationTest extends SysuiTestCase { @Test public void overviewProxyIsConnected_noController_resetFlag() { - mOverviewProxyListener.onConnectionChanged(true); + mLauncherProxyListener.onConnectionChanged(true); verify(mSysUiState).setFlag(SYSUI_STATE_MAGNIFICATION_OVERLAP, false); verify(mSysUiState).commitUpdate(mContext.getDisplayId()); @@ -349,7 +349,7 @@ public class MagnificationTest extends SysuiTestCase { mContext.getSystemService(DisplayManager.class), mController); mMagnification.mWindowMagnificationControllerSupplier.get(TEST_DISPLAY); - mOverviewProxyListener.onConnectionChanged(true); + mLauncherProxyListener.onConnectionChanged(true); verify(mController).updateSysUIStateFlag(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt index 21519b0cb38a..ab691c630f97 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt @@ -304,9 +304,7 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { testScope = TestScope(testDispatcher) underTest = KeyguardQuickAffordanceInteractor( - keyguardInteractor = - KeyguardInteractorFactory.create(featureFlags = featureFlags) - .keyguardInteractor, + keyguardInteractor = kosmos.keyguardInteractor, shadeInteractor = kosmos.shadeInteractor, lockPatternUtils = lockPatternUtils, keyguardStateController = keyguardStateController, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt index b5a227104900..051aba3d593f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt @@ -44,9 +44,10 @@ import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanc import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState.AOD import com.android.systemui.keyguard.shared.model.KeyguardState.GONE @@ -191,9 +192,8 @@ class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() { dockManager = DockManagerFake() biometricSettingsRepository = FakeBiometricSettingsRepository() - val withDeps = KeyguardInteractorFactory.create() - keyguardInteractor = withDeps.keyguardInteractor - repository = withDeps.repository + keyguardInteractor = kosmos.keyguardInteractor + repository = kosmos.fakeKeyguardRepository whenever(userTracker.userHandle).thenReturn(mock()) whenever(lockPatternUtils.getStrongAuthForUser(ArgumentMatchers.anyInt())) diff --git a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/AmbientLightModeMonitorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/AmbientLightModeMonitorTest.kt new file mode 100644 index 000000000000..43ee388e44a7 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/AmbientLightModeMonitorTest.kt @@ -0,0 +1,110 @@ +/* + * 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.lowlightclock + +import android.hardware.Sensor +import android.hardware.SensorEventListener +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.sensors.AsyncSensorManager +import java.util.Optional +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.eq +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class AmbientLightModeMonitorTest : SysuiTestCase() { + @Mock private lateinit var sensorManager: AsyncSensorManager + @Mock private lateinit var sensor: Sensor + @Mock private lateinit var algorithm: AmbientLightModeMonitor.DebounceAlgorithm + + private lateinit var ambientLightModeMonitor: AmbientLightModeMonitor + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + ambientLightModeMonitor = + AmbientLightModeMonitor(Optional.of(algorithm), sensorManager, Optional.of(sensor)) + } + + @Test + fun shouldRegisterSensorEventListenerOnStart() { + val callback = mock(AmbientLightModeMonitor.Callback::class.java) + ambientLightModeMonitor.start(callback) + + verify(sensorManager).registerListener(any(), eq(sensor), anyInt()) + } + + @Test + fun shouldUnregisterSensorEventListenerOnStop() { + val callback = mock(AmbientLightModeMonitor.Callback::class.java) + ambientLightModeMonitor.start(callback) + + val sensorEventListener = captureSensorEventListener() + + ambientLightModeMonitor.stop() + + verify(sensorManager).unregisterListener(eq(sensorEventListener)) + } + + @Test + fun shouldStartDebounceAlgorithmOnStart() { + val callback = mock(AmbientLightModeMonitor.Callback::class.java) + ambientLightModeMonitor.start(callback) + + verify(algorithm).start(eq(callback)) + } + + @Test + fun shouldStopDebounceAlgorithmOnStop() { + val callback = mock(AmbientLightModeMonitor.Callback::class.java) + ambientLightModeMonitor.start(callback) + ambientLightModeMonitor.stop() + + verify(algorithm).stop() + } + + @Test + fun shouldNotRegisterForSensorUpdatesIfSensorNotAvailable() { + val ambientLightModeMonitor = + AmbientLightModeMonitor(Optional.of(algorithm), sensorManager, Optional.empty()) + + val callback = mock(AmbientLightModeMonitor.Callback::class.java) + ambientLightModeMonitor.start(callback) + + verify(sensorManager, never()).registerListener(any(), any(Sensor::class.java), anyInt()) + } + + // Captures [SensorEventListener], assuming it has been registered with [sensorManager]. + private fun captureSensorEventListener(): SensorEventListener { + val captor = ArgumentCaptor.forClass(SensorEventListener::class.java) + verify(sensorManager).registerListener(captor.capture(), any(), anyInt()) + return captor.value + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ChargingStatusProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ChargingStatusProviderTest.java new file mode 100644 index 000000000000..2c8c1e1e70b1 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ChargingStatusProviderTest.java @@ -0,0 +1,226 @@ +/* + * 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.lowlightclock; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.res.Resources; +import android.os.BatteryManager; +import android.os.RemoteException; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.internal.app.IBatteryStats; +import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.keyguard.KeyguardUpdateMonitorCallback; +import com.android.settingslib.fuelgauge.BatteryStatus; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.res.R; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class ChargingStatusProviderTest extends SysuiTestCase { + @Mock + private Resources mResources; + @Mock + private IBatteryStats mBatteryInfo; + @Mock + private KeyguardUpdateMonitor mKeyguardUpdateMonitor; + @Mock + private ChargingStatusProvider.Callback mCallback; + + private ChargingStatusProvider mProvider; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + mProvider = new ChargingStatusProvider( + mContext, mResources, mBatteryInfo, mKeyguardUpdateMonitor); + } + + @Test + public void testStartUsingReportsStatusToCallback() { + mProvider.startUsing(mCallback); + verify(mCallback).onChargingStatusChanged(false, null); + } + + @Test + public void testStartUsingRegistersCallbackWithKeyguardUpdateMonitor() { + mProvider.startUsing(mCallback); + verify(mKeyguardUpdateMonitor).registerCallback(any()); + } + + @Test + public void testCallbackNotCalledAfterStopUsing() { + mProvider.startUsing(mCallback); + ArgumentCaptor<KeyguardUpdateMonitorCallback> keyguardUpdateMonitorCallbackArgumentCaptor = + ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class); + verify(mKeyguardUpdateMonitor) + .registerCallback(keyguardUpdateMonitorCallbackArgumentCaptor.capture()); + mProvider.stopUsing(); + keyguardUpdateMonitorCallbackArgumentCaptor.getValue() + .onRefreshBatteryInfo(getChargingBattery()); + verify(mCallback, never()).onChargingStatusChanged(eq(true), any()); + } + + @Test + public void testKeyguardUpdateMonitorCallbackRemovedAfterStopUsing() { + mProvider.startUsing(mCallback); + ArgumentCaptor<KeyguardUpdateMonitorCallback> keyguardUpdateMonitorCallbackArgumentCaptor = + ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class); + verify(mKeyguardUpdateMonitor) + .registerCallback(keyguardUpdateMonitorCallbackArgumentCaptor.capture()); + mProvider.stopUsing(); + verify(mKeyguardUpdateMonitor) + .removeCallback(keyguardUpdateMonitorCallbackArgumentCaptor.getValue()); + } + + @Test + public void testChargingStatusReportsHideWhenNotPluggedIn() { + ArgumentCaptor<KeyguardUpdateMonitorCallback> keyguardUpdateMonitorCallbackArgumentCaptor = + ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class); + mProvider.startUsing(mCallback); + verify(mKeyguardUpdateMonitor) + .registerCallback(keyguardUpdateMonitorCallbackArgumentCaptor.capture()); + keyguardUpdateMonitorCallbackArgumentCaptor.getValue() + .onRefreshBatteryInfo(getUnpluggedBattery()); + // Once for init() and once for the status change. + verify(mCallback, times(2)).onChargingStatusChanged(false, null); + } + + @Test + public void testChargingStatusReportsShowWhenBatteryOverheated() { + ArgumentCaptor<KeyguardUpdateMonitorCallback> keyguardUpdateMonitorCallbackArgumentCaptor = + ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class); + mProvider.startUsing(mCallback); + verify(mCallback).onChargingStatusChanged(false, null); + verify(mKeyguardUpdateMonitor) + .registerCallback(keyguardUpdateMonitorCallbackArgumentCaptor.capture()); + keyguardUpdateMonitorCallbackArgumentCaptor.getValue() + .onRefreshBatteryInfo(getBatteryDefender()); + verify(mCallback).onChargingStatusChanged(eq(true), any()); + } + + @Test + public void testChargingStatusReportsShowWhenPluggedIn() { + ArgumentCaptor<KeyguardUpdateMonitorCallback> keyguardUpdateMonitorCallbackArgumentCaptor = + ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class); + mProvider.startUsing(mCallback); + verify(mCallback).onChargingStatusChanged(false, null); + verify(mKeyguardUpdateMonitor) + .registerCallback(keyguardUpdateMonitorCallbackArgumentCaptor.capture()); + keyguardUpdateMonitorCallbackArgumentCaptor.getValue() + .onRefreshBatteryInfo(getChargingBattery()); + verify(mCallback).onChargingStatusChanged(eq(true), any()); + } + + @Test + public void testChargingStatusReportsChargingLimitedWhenOverheated() { + ArgumentCaptor<KeyguardUpdateMonitorCallback> keyguardUpdateMonitorCallbackArgumentCaptor = + ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class); + mProvider.startUsing(mCallback); + verify(mCallback).onChargingStatusChanged(false, null); + verify(mKeyguardUpdateMonitor) + .registerCallback(keyguardUpdateMonitorCallbackArgumentCaptor.capture()); + keyguardUpdateMonitorCallbackArgumentCaptor.getValue() + .onRefreshBatteryInfo(getBatteryDefender()); + verify(mResources).getString(eq(R.string.keyguard_plugged_in_charging_limited), any()); + } + + @Test + public void testChargingStatusReportsChargedWhenCharged() { + ArgumentCaptor<KeyguardUpdateMonitorCallback> keyguardUpdateMonitorCallbackArgumentCaptor = + ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class); + mProvider.startUsing(mCallback); + verify(mCallback).onChargingStatusChanged(false, null); + verify(mKeyguardUpdateMonitor) + .registerCallback(keyguardUpdateMonitorCallbackArgumentCaptor.capture()); + keyguardUpdateMonitorCallbackArgumentCaptor.getValue() + .onRefreshBatteryInfo(getChargedBattery()); + verify(mResources).getString(R.string.keyguard_charged); + } + + @Test + public void testChargingStatusReportsPluggedInWhenDockedAndChargingTimeUnknown() throws + RemoteException { + ArgumentCaptor<KeyguardUpdateMonitorCallback> keyguardUpdateMonitorCallbackArgumentCaptor = + ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class); + mProvider.startUsing(mCallback); + verify(mCallback).onChargingStatusChanged(false, null); + verify(mKeyguardUpdateMonitor) + .registerCallback(keyguardUpdateMonitorCallbackArgumentCaptor.capture()); + when(mBatteryInfo.computeChargeTimeRemaining()).thenReturn(-1L); + keyguardUpdateMonitorCallbackArgumentCaptor.getValue() + .onRefreshBatteryInfo(getChargingBattery()); + verify(mResources).getString( + eq(R.string.keyguard_plugged_in_dock), any()); + } + + @Test + public void testChargingStatusReportsTimeRemainingWhenDockedAndCharging() throws + RemoteException { + ArgumentCaptor<KeyguardUpdateMonitorCallback> keyguardUpdateMonitorCallbackArgumentCaptor = + ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class); + mProvider.startUsing(mCallback); + verify(mCallback).onChargingStatusChanged(false, null); + verify(mKeyguardUpdateMonitor) + .registerCallback(keyguardUpdateMonitorCallbackArgumentCaptor.capture()); + when(mBatteryInfo.computeChargeTimeRemaining()).thenReturn(1L); + keyguardUpdateMonitorCallbackArgumentCaptor.getValue() + .onRefreshBatteryInfo(getChargingBattery()); + verify(mResources).getString( + eq(R.string.keyguard_indication_charging_time_dock), any(), any()); + } + + private BatteryStatus getUnpluggedBattery() { + return new BatteryStatus(BatteryManager.BATTERY_STATUS_NOT_CHARGING, + 80, BatteryManager.BATTERY_PLUGGED_ANY, BatteryManager.BATTERY_HEALTH_GOOD, + 0, true); + } + + private BatteryStatus getChargingBattery() { + return new BatteryStatus(BatteryManager.BATTERY_STATUS_CHARGING, + 80, BatteryManager.BATTERY_PLUGGED_DOCK, + BatteryManager.BATTERY_HEALTH_GOOD, 0, true); + } + + private BatteryStatus getChargedBattery() { + return new BatteryStatus(BatteryManager.BATTERY_STATUS_FULL, + 100, BatteryManager.BATTERY_PLUGGED_DOCK, + BatteryManager.BATTERY_HEALTH_GOOD, 0, true); + } + + private BatteryStatus getBatteryDefender() { + return new BatteryStatus(BatteryManager.BATTERY_STATUS_CHARGING, + 80, BatteryManager.BATTERY_PLUGGED_DOCK, + BatteryManager.CHARGING_POLICY_ADAPTIVE_LONGLIFE, 0, true); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/DirectBootConditionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/DirectBootConditionTest.kt new file mode 100644 index 000000000000..173f243cb2b0 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/DirectBootConditionTest.kt @@ -0,0 +1,102 @@ +/* + * 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.lowlightclock + +import android.content.Intent +import android.os.UserManager +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.shared.condition.Condition +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +class DirectBootConditionTest : SysuiTestCase() { + @Mock private lateinit var userManager: UserManager + @Mock private lateinit var callback: Condition.Callback + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + } + + @Test + fun receiverRegisteredOnStart() = runTest { + val condition = buildCondition(this) + // No receivers are registered yet + assertThat(fakeBroadcastDispatcher.numReceiversRegistered).isEqualTo(0) + condition.addCallback(callback) + advanceUntilIdle() + // Receiver is registered after a callback is added + assertThat(fakeBroadcastDispatcher.numReceiversRegistered).isEqualTo(1) + condition.removeCallback(callback) + } + + @Test + fun unregisterReceiverOnStop() = runTest { + val condition = buildCondition(this) + + condition.addCallback(callback) + advanceUntilIdle() + + assertThat(fakeBroadcastDispatcher.numReceiversRegistered).isEqualTo(1) + + condition.removeCallback(callback) + advanceUntilIdle() + + // Receiver is unregistered when nothing is listening to the condition + assertThat(fakeBroadcastDispatcher.numReceiversRegistered).isEqualTo(0) + } + + @Test + fun callbackTriggeredWhenUserUnlocked() = runTest { + val condition = buildCondition(this) + + setUserUnlocked(false) + condition.addCallback(callback) + advanceUntilIdle() + + assertThat(condition.isConditionMet).isTrue() + + setUserUnlocked(true) + advanceUntilIdle() + + assertThat(condition.isConditionMet).isFalse() + condition.removeCallback(callback) + } + + private fun buildCondition(scope: CoroutineScope): DirectBootCondition { + return DirectBootCondition(fakeBroadcastDispatcher, userManager, scope) + } + + private fun setUserUnlocked(unlocked: Boolean) { + whenever(userManager.isUserUnlocked).thenReturn(unlocked) + fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( + context, + Intent(Intent.ACTION_USER_UNLOCKED), + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ForceLowLightConditionTest.java b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ForceLowLightConditionTest.java new file mode 100644 index 000000000000..7297e0f3bff5 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ForceLowLightConditionTest.java @@ -0,0 +1,110 @@ +/* + * 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.lowlightclock; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.shared.condition.Condition; +import com.android.systemui.statusbar.commandline.Command; +import com.android.systemui.statusbar.commandline.CommandRegistry; + +import kotlin.jvm.functions.Function0; + +import kotlinx.coroutines.CoroutineScope; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.io.PrintWriter; +import java.util.Arrays; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class ForceLowLightConditionTest extends SysuiTestCase { + @Mock + private CommandRegistry mCommandRegistry; + + @Mock + private Condition.Callback mCallback; + + @Mock + private PrintWriter mPrintWriter; + + @Mock + CoroutineScope mScope; + + private ForceLowLightCondition mCondition; + private Command mCommand; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mCondition = new ForceLowLightCondition(mScope, mCommandRegistry); + mCondition.addCallback(mCallback); + ArgumentCaptor<Function0<Command>> commandCaptor = + ArgumentCaptor.forClass(Function0.class); + verify(mCommandRegistry).registerCommand(eq(ForceLowLightCondition.COMMAND_ROOT), + commandCaptor.capture()); + mCommand = commandCaptor.getValue().invoke(); + } + + @Test + public void testEnableLowLight() { + mCommand.execute(mPrintWriter, + Arrays.asList(ForceLowLightCondition.COMMAND_ENABLE_LOW_LIGHT)); + verify(mCallback).onConditionChanged(mCondition); + assertThat(mCondition.isConditionSet()).isTrue(); + assertThat(mCondition.isConditionMet()).isTrue(); + } + + @Test + public void testDisableLowLight() { + mCommand.execute(mPrintWriter, + Arrays.asList(ForceLowLightCondition.COMMAND_DISABLE_LOW_LIGHT)); + verify(mCallback).onConditionChanged(mCondition); + assertThat(mCondition.isConditionSet()).isTrue(); + assertThat(mCondition.isConditionMet()).isFalse(); + } + + @Test + public void testClearEnableLowLight() { + mCommand.execute(mPrintWriter, + Arrays.asList(ForceLowLightCondition.COMMAND_ENABLE_LOW_LIGHT)); + verify(mCallback).onConditionChanged(mCondition); + assertThat(mCondition.isConditionSet()).isTrue(); + assertThat(mCondition.isConditionMet()).isTrue(); + Mockito.clearInvocations(mCallback); + mCommand.execute(mPrintWriter, + Arrays.asList(ForceLowLightCondition.COMMAND_CLEAR_LOW_LIGHT)); + verify(mCallback).onConditionChanged(mCondition); + assertThat(mCondition.isConditionSet()).isFalse(); + assertThat(mCondition.isConditionMet()).isFalse(); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightClockAnimationProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightClockAnimationProviderTest.kt new file mode 100644 index 000000000000..663880f098cd --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightClockAnimationProviderTest.kt @@ -0,0 +1,75 @@ +/* + * 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.lowlightclock + +import android.animation.Animator +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper(setAsMainLooper = true) +class LowLightClockAnimationProviderTest : SysuiTestCase() { + + private val underTest by lazy { + LowLightClockAnimationProvider( + Y_TRANSLATION_ANIMATION_OFFSET, + Y_TRANSLATION_ANIMATION_DURATION_MILLIS, + ALPHA_ANIMATION_IN_START_DELAY_MILLIS, + ALPHA_ANIMATION_DURATION_MILLIS, + ) + } + + @Test + fun animationOutEndsImmediatelyIfViewIsNull() { + val animator = underTest.provideAnimationOut(null, null) + + val listener = mock<Animator.AnimatorListener>() + animator.addListener(listener) + + animator.start() + verify(listener).onAnimationStart(any(), eq(false)) + verify(listener).onAnimationEnd(any(), eq(false)) + } + + @Test + fun animationInEndsImmediatelyIfViewIsNull() { + val animator = underTest.provideAnimationIn(null, null) + + val listener = mock<Animator.AnimatorListener>() + animator.addListener(listener) + + animator.start() + verify(listener).onAnimationStart(any(), eq(false)) + verify(listener).onAnimationEnd(any(), eq(false)) + } + + private companion object { + const val Y_TRANSLATION_ANIMATION_OFFSET = 100 + const val Y_TRANSLATION_ANIMATION_DURATION_MILLIS = 100L + const val ALPHA_ANIMATION_IN_START_DELAY_MILLIS = 200L + const val ALPHA_ANIMATION_DURATION_MILLIS = 300L + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightClockDreamServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightClockDreamServiceTest.java new file mode 100644 index 000000000000..22a13cc41425 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightClockDreamServiceTest.java @@ -0,0 +1,160 @@ +/* + * 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.lowlightclock; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.animation.Animator; +import android.os.RemoteException; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.dream.lowlight.LowLightTransitionCoordinator; +import com.android.systemui.SysuiTestCase; + +import com.google.android.systemui.lowlightclock.LowLightClockDreamService; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Optional; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class LowLightClockDreamServiceTest extends SysuiTestCase { + @Mock + private ChargingStatusProvider mChargingStatusProvider; + @Mock + private LowLightDisplayController mDisplayController; + @Mock + private LowLightClockAnimationProvider mAnimationProvider; + @Mock + private LowLightTransitionCoordinator mLowLightTransitionCoordinator; + @Mock + Animator mAnimationInAnimator; + @Mock + Animator mAnimationOutAnimator; + + private LowLightClockDreamService mService; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mService = new LowLightClockDreamService( + mChargingStatusProvider, + mAnimationProvider, + mLowLightTransitionCoordinator, + Optional.of(() -> mDisplayController)); + + when(mAnimationProvider.provideAnimationIn(any(), any())).thenReturn(mAnimationInAnimator); + when(mAnimationProvider.provideAnimationOut(any())).thenReturn( + mAnimationOutAnimator); + } + + @Test + public void testSetDbmStateWhenSupported() throws RemoteException { + when(mDisplayController.isDisplayBrightnessModeSupported()).thenReturn(true); + + mService.onDreamingStarted(); + + verify(mDisplayController).setDisplayBrightnessModeEnabled(true); + } + + @Test + public void testNotSetDbmStateWhenNotSupported() throws RemoteException { + when(mDisplayController.isDisplayBrightnessModeSupported()).thenReturn(false); + + mService.onDreamingStarted(); + + verify(mDisplayController, never()).setDisplayBrightnessModeEnabled(anyBoolean()); + } + + @Test + public void testClearDbmState() throws RemoteException { + when(mDisplayController.isDisplayBrightnessModeSupported()).thenReturn(true); + + mService.onDreamingStarted(); + clearInvocations(mDisplayController); + + mService.onDreamingStopped(); + + verify(mDisplayController).setDisplayBrightnessModeEnabled(false); + } + + @Test + public void testAnimationsStartedOnDreamingStarted() { + mService.onDreamingStarted(); + + // Entry animation started. + verify(mAnimationInAnimator).start(); + } + + @Test + public void testAnimationsStartedOnWakeUp() { + // Start dreaming then wake up. + mService.onDreamingStarted(); + mService.onWakeUp(); + + // Entry animation started. + verify(mAnimationInAnimator).cancel(); + + // Exit animation started. + verify(mAnimationOutAnimator).start(); + } + + @Test + public void testAnimationsStartedBeforeExitingLowLight() { + mService.onBeforeExitLowLight(); + + // Exit animation started. + verify(mAnimationOutAnimator).start(); + } + + @Test + public void testWakeUpAnimationCancelledOnDetach() { + mService.onWakeUp(); + + // Exit animation started. + verify(mAnimationOutAnimator).start(); + + mService.onDetachedFromWindow(); + + verify(mAnimationOutAnimator).cancel(); + } + + @Test + public void testExitLowLightAnimationCancelledOnDetach() { + mService.onBeforeExitLowLight(); + + // Exit animation started. + verify(mAnimationOutAnimator).start(); + + mService.onDetachedFromWindow(); + + verify(mAnimationOutAnimator).cancel(); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightConditionTest.java b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightConditionTest.java new file mode 100644 index 000000000000..2c216244985e --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightConditionTest.java @@ -0,0 +1,143 @@ +/* + * 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.lowlightclock; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.internal.logging.UiEventLogger; +import com.android.systemui.SysuiTestCase; + +import kotlinx.coroutines.CoroutineScope; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class LowLightConditionTest extends SysuiTestCase { + @Mock + private AmbientLightModeMonitor mAmbientLightModeMonitor; + @Mock + private UiEventLogger mUiEventLogger; + @Mock + CoroutineScope mScope; + private LowLightCondition mCondition; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mCondition = new LowLightCondition(mScope, mAmbientLightModeMonitor, mUiEventLogger); + mCondition.start(); + } + + @Test + public void testLowLightFalse() { + changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT); + assertThat(mCondition.isConditionMet()).isFalse(); + } + + @Test + public void testLowLightTrue() { + changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK); + assertThat(mCondition.isConditionMet()).isTrue(); + } + + @Test + public void testUndecidedLowLightStateIgnored() { + changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK); + assertThat(mCondition.isConditionMet()).isTrue(); + changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_UNDECIDED); + assertThat(mCondition.isConditionMet()).isTrue(); + } + + @Test + public void testLowLightChange() { + changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT); + assertThat(mCondition.isConditionMet()).isFalse(); + changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK); + assertThat(mCondition.isConditionMet()).isTrue(); + } + + @Test + public void testResetIsConditionMetUponStop() { + changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK); + assertThat(mCondition.isConditionMet()).isTrue(); + + mCondition.stop(); + assertThat(mCondition.isConditionMet()).isFalse(); + } + + @Test + public void testLoggingAmbientLightNotLowToLow() { + changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK); + // Only logged once. + verify(mUiEventLogger, times(1)).log(any()); + // Logged with the correct state. + verify(mUiEventLogger).log(LowLightDockEvent.AMBIENT_LIGHT_TO_DARK); + } + + @Test + public void testLoggingAmbientLightLowToLow() { + changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK); + reset(mUiEventLogger); + + changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK); + // Doesn't log. + verify(mUiEventLogger, never()).log(any()); + } + + @Test + public void testLoggingAmbientLightNotLowToNotLow() { + changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT); + // Doesn't log. + verify(mUiEventLogger, never()).log(any()); + } + + @Test + public void testLoggingAmbientLightLowToNotLow() { + changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK); + reset(mUiEventLogger); + + changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT); + // Only logged once. + verify(mUiEventLogger).log(any()); + // Logged with the correct state. + verify(mUiEventLogger).log(LowLightDockEvent.AMBIENT_LIGHT_TO_LIGHT); + } + + private void changeLowLightMode(int mode) { + ArgumentCaptor<AmbientLightModeMonitor.Callback> ambientLightCallbackCaptor = + ArgumentCaptor.forClass(AmbientLightModeMonitor.Callback.class); + verify(mAmbientLightModeMonitor).start(ambientLightCallbackCaptor.capture()); + ambientLightCallbackCaptor.getValue().onChange(mode); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java new file mode 100644 index 000000000000..69485e848a6a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java @@ -0,0 +1,183 @@ +/* + * 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.lowlightclock; + +import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT; +import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR; +import static com.android.systemui.keyguard.ScreenLifecycle.SCREEN_ON; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.ComponentName; +import android.content.pm.PackageManager; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.dream.lowlight.LowLightDreamManager; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.keyguard.ScreenLifecycle; +import com.android.systemui.shared.condition.Condition; +import com.android.systemui.shared.condition.Monitor; + +import dagger.Lazy; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Set; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class LowLightMonitorTest extends SysuiTestCase { + + @Mock + private Lazy<LowLightDreamManager> mLowLightDreamManagerLazy; + @Mock + private LowLightDreamManager mLowLightDreamManager; + @Mock + private Monitor mMonitor; + @Mock + private ScreenLifecycle mScreenLifecycle; + @Mock + private LowLightLogger mLogger; + + private LowLightMonitor mLowLightMonitor; + + @Mock + Lazy<Set<Condition>> mLazyConditions; + + @Mock + private PackageManager mPackageManager; + + @Mock + private ComponentName mDreamComponent; + + Condition mCondition = mock(Condition.class); + Set<Condition> mConditionSet = Set.of(mCondition); + + @Captor + ArgumentCaptor<Monitor.Subscription> mPreconditionsSubscriptionCaptor; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mLowLightDreamManagerLazy.get()).thenReturn(mLowLightDreamManager); + when(mLazyConditions.get()).thenReturn(mConditionSet); + mLowLightMonitor = new LowLightMonitor(mLowLightDreamManagerLazy, + mMonitor, mLazyConditions, mScreenLifecycle, mLogger, mDreamComponent, + mPackageManager); + } + + @Test + public void testSetAmbientLowLightWhenInLowLight() { + mLowLightMonitor.onConditionsChanged(true); + // Verify setting low light when condition is true + verify(mLowLightDreamManager).setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); + } + + @Test + public void testExitAmbientLowLightWhenNotInLowLight() { + mLowLightMonitor.onConditionsChanged(true); + mLowLightMonitor.onConditionsChanged(false); + // Verify ambient light toggles back to light mode regular + verify(mLowLightDreamManager).setAmbientLightMode(AMBIENT_LIGHT_MODE_REGULAR); + } + + @Test + public void testStartMonitorLowLightConditionsWhenScreenTurnsOn() { + mLowLightMonitor.onScreenTurnedOn(); + + // Verify subscribing to low light conditions monitor when screen turns on. + verify(mMonitor).addSubscription(any()); + } + + @Test + public void testStopMonitorLowLightConditionsWhenScreenTurnsOff() { + final Monitor.Subscription.Token token = mock(Monitor.Subscription.Token.class); + when(mMonitor.addSubscription(any())).thenReturn(token); + mLowLightMonitor.onScreenTurnedOn(); + + // Verify removing subscription when screen turns off. + mLowLightMonitor.onScreenTurnedOff(); + verify(mMonitor).removeSubscription(token); + } + + @Test + public void testSubscribeToLowLightConditionsOnlyOnceWhenScreenTurnsOn() { + final Monitor.Subscription.Token token = mock(Monitor.Subscription.Token.class); + when(mMonitor.addSubscription(any())).thenReturn(token); + + mLowLightMonitor.onScreenTurnedOn(); + mLowLightMonitor.onScreenTurnedOn(); + // Verify subscription is only added once. + verify(mMonitor, times(1)).addSubscription(any()); + } + + @Test + public void testSubscribedToExpectedConditions() { + final Monitor.Subscription.Token token = mock(Monitor.Subscription.Token.class); + when(mMonitor.addSubscription(any())).thenReturn(token); + + mLowLightMonitor.onScreenTurnedOn(); + mLowLightMonitor.onScreenTurnedOn(); + Set<Condition> conditions = captureConditions(); + // Verify Monitor is subscribed to the expected conditions + assertThat(conditions).isEqualTo(mConditionSet); + } + + @Test + public void testNotUnsubscribeIfNotSubscribedWhenScreenTurnsOff() { + mLowLightMonitor.onScreenTurnedOff(); + + // Verify doesn't remove subscription since there is none. + verify(mMonitor, never()).removeSubscription(any()); + } + + @Test + public void testSubscribeIfScreenIsOnWhenStarting() { + when(mScreenLifecycle.getScreenState()).thenReturn(SCREEN_ON); + mLowLightMonitor.start(); + // Verify to add subscription on start if the screen state is on + verify(mMonitor, times(1)).addSubscription(any()); + } + + @Test + public void testNoSubscribeIfDreamNotPresent() { + LowLightMonitor lowLightMonitor = new LowLightMonitor(mLowLightDreamManagerLazy, + mMonitor, mLazyConditions, mScreenLifecycle, mLogger, null, mPackageManager); + when(mScreenLifecycle.getScreenState()).thenReturn(SCREEN_ON); + lowLightMonitor.start(); + verify(mScreenLifecycle, never()).addObserver(any()); + } + + private Set<Condition> captureConditions() { + verify(mMonitor).addSubscription(mPreconditionsSubscriptionCaptor.capture()); + return mPreconditionsSubscriptionCaptor.getValue().getConditions(); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ScreenSaverEnabledConditionTest.java b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ScreenSaverEnabledConditionTest.java new file mode 100644 index 000000000000..366c071fb93f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ScreenSaverEnabledConditionTest.java @@ -0,0 +1,108 @@ +/* + * 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.lowlightclock; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.res.Resources; +import android.database.ContentObserver; +import android.os.UserHandle; +import android.provider.Settings; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.util.settings.SecureSettings; + +import kotlinx.coroutines.CoroutineScope; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class ScreenSaverEnabledConditionTest extends SysuiTestCase { + @Mock + private Resources mResources; + @Mock + private SecureSettings mSecureSettings; + @Mock + CoroutineScope mScope; + @Captor + private ArgumentCaptor<ContentObserver> mSettingsObserverCaptor; + private ScreenSaverEnabledCondition mCondition; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + // Default dreams to enabled by default + doReturn(true).when(mResources).getBoolean( + com.android.internal.R.bool.config_dreamsEnabledByDefault); + + mCondition = new ScreenSaverEnabledCondition(mScope, mResources, mSecureSettings); + } + + @Test + public void testScreenSaverInitiallyEnabled() { + setScreenSaverEnabled(true); + mCondition.start(); + assertThat(mCondition.isConditionMet()).isTrue(); + } + + @Test + public void testScreenSaverInitiallyDisabled() { + setScreenSaverEnabled(false); + mCondition.start(); + assertThat(mCondition.isConditionMet()).isFalse(); + } + + @Test + public void testScreenSaverStateChanges() { + setScreenSaverEnabled(false); + mCondition.start(); + assertThat(mCondition.isConditionMet()).isFalse(); + + setScreenSaverEnabled(true); + final ContentObserver observer = captureSettingsObserver(); + observer.onChange(/* selfChange= */ false); + assertThat(mCondition.isConditionMet()).isTrue(); + } + + private void setScreenSaverEnabled(boolean enabled) { + when(mSecureSettings.getIntForUser(eq(Settings.Secure.SCREENSAVER_ENABLED), anyInt(), + eq(UserHandle.USER_CURRENT))).thenReturn(enabled ? 1 : 0); + } + + private ContentObserver captureSettingsObserver() { + verify(mSecureSettings).registerContentObserverForUserSync( + eq(Settings.Secure.SCREENSAVER_ENABLED), + mSettingsObserverCaptor.capture(), eq(UserHandle.USER_CURRENT)); + return mSettingsObserverCaptor.getValue(); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerImplTest.java index d59a404b15bb..0924df2538e1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerImplTest.java @@ -52,7 +52,7 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.kosmos.KosmosJavaAdapter; import com.android.systemui.model.SysUiState; import com.android.systemui.navigationbar.views.NavigationBar; -import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.recents.LauncherProxyService; import com.android.systemui.settings.FakeDisplayTracker; import com.android.systemui.shared.recents.utilities.Utilities; import com.android.systemui.shared.system.TaskStackChangeListeners; @@ -109,7 +109,7 @@ public class NavigationBarControllerImplTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); mNavigationBarController = spy( new NavigationBarControllerImpl(mContext, - mock(OverviewProxyService.class), + mock(LauncherProxyService.class), mock(NavigationModeController.class), mock(SysUiState.class), mCommandQueue, diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt index a192446e535b..50b8f37f8d25 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.qs.tiles.dialog import android.content.Intent import android.os.Handler import android.os.fakeExecutorHandler +import android.platform.test.annotations.EnableFlags import android.telephony.SubscriptionManager import android.telephony.TelephonyManager import android.telephony.telephonyManager @@ -38,8 +39,7 @@ import com.android.internal.logging.UiEventLogger import com.android.settingslib.wifi.WifiEnterpriseRestrictionUtils import com.android.systemui.Flags import com.android.systemui.SysuiTestCase -import com.android.systemui.animation.DialogTransitionAnimator -import com.android.systemui.flags.setFlagValue +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.Kosmos import com.android.systemui.res.R import com.android.systemui.statusbar.policy.KeyguardStateController @@ -62,6 +62,8 @@ import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) @RunWithLooper(setAsMainLooper = true) +@EnableSceneContainer +@EnableFlags(Flags.FLAG_QS_TILE_DETAILED_VIEW, Flags.FLAG_DUAL_SHADE) @UiThreadTest class InternetDetailsContentManagerTest : SysuiTestCase() { private val kosmos = Kosmos() @@ -74,11 +76,8 @@ class InternetDetailsContentManagerTest : SysuiTestCase() { private val internetDetailsContentController: InternetDetailsContentController = mock<InternetDetailsContentController>() private val keyguard: KeyguardStateController = mock<KeyguardStateController>() - private val dialogTransitionAnimator: DialogTransitionAnimator = - mock<DialogTransitionAnimator>() private val bgExecutor = FakeExecutor(FakeSystemClock()) private lateinit var internetDetailsContentManager: InternetDetailsContentManager - private var subTitle: View? = null private var ethernet: LinearLayout? = null private var mobileDataLayout: LinearLayout? = null private var mobileToggleSwitch: Switch? = null @@ -96,8 +95,6 @@ class InternetDetailsContentManagerTest : SysuiTestCase() { @Before fun setUp() { - // TODO: b/377388104 enable this flag after integrating with details view. - mSetFlagsRule.setFlagValue(Flags.FLAG_QS_TILE_DETAILED_VIEW, false) whenever(telephonyManager.createForSubscriptionId(ArgumentMatchers.anyInt())) .thenReturn(telephonyManager) whenever(internetWifiEntry.title).thenReturn(WIFI_TITLE) @@ -133,9 +130,7 @@ class InternetDetailsContentManagerTest : SysuiTestCase() { canConfigWifi = true, coroutineScope = scope, context = mContext, - internetDialog = null, uiEventLogger = mock<UiEventLogger>(), - dialogTransitionAnimator = dialogTransitionAnimator, handler = handler, backgroundExecutor = bgExecutor, keyguard = keyguard, @@ -146,7 +141,6 @@ class InternetDetailsContentManagerTest : SysuiTestCase() { internetDetailsContentManager.connectedWifiEntry = internetWifiEntry internetDetailsContentManager.wifiEntriesCount = wifiEntries.size - subTitle = contentView.requireViewById(R.id.internet_dialog_subtitle) ethernet = contentView.requireViewById(R.id.ethernet_layout) mobileDataLayout = contentView.requireViewById(R.id.mobile_network_layout) mobileToggleSwitch = contentView.requireViewById(R.id.mobile_toggle) @@ -185,32 +179,6 @@ class InternetDetailsContentManagerTest : SysuiTestCase() { } @Test - fun updateContent_withApmOn_internetDialogSubTitleGone() { - whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(true) - internetDetailsContentManager.updateContent(true) - bgExecutor.runAllReady() - - internetDetailsContentManager.internetContentData.observe( - internetDetailsContentManager.lifecycleOwner!! - ) { - assertThat(subTitle!!.visibility).isEqualTo(View.VISIBLE) - } - } - - @Test - fun updateContent_withApmOff_internetDialogSubTitleVisible() { - whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(false) - internetDetailsContentManager.updateContent(true) - bgExecutor.runAllReady() - - internetDetailsContentManager.internetContentData.observe( - internetDetailsContentManager.lifecycleOwner!! - ) { - assertThat(subTitle!!.visibility).isEqualTo(View.VISIBLE) - } - } - - @Test fun updateContent_apmOffAndHasEthernet_showEthernet() { whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(false) whenever(internetDetailsContentController.hasEthernet()).thenReturn(true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt index 4e1ccfb07220..69b762b470b7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt @@ -40,11 +40,11 @@ import com.android.systemui.model.sceneContainerPlugin import com.android.systemui.navigationbar.NavigationBarController import com.android.systemui.navigationbar.NavigationModeController import com.android.systemui.process.ProcessWrapper -import com.android.systemui.recents.OverviewProxyService.ACTION_QUICKSTEP +import com.android.systemui.recents.LauncherProxyService.ACTION_QUICKSTEP import com.android.systemui.settings.FakeDisplayTracker import com.android.systemui.settings.UserTracker import com.android.systemui.shade.ShadeViewController -import com.android.systemui.shared.recents.IOverviewProxy +import com.android.systemui.shared.recents.ILauncherProxy import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_WAKEFULNESS_MASK import com.android.systemui.shared.system.QuickStepContract.WAKEFULNESS_ASLEEP import com.android.systemui.shared.system.QuickStepContract.WAKEFULNESS_AWAKE @@ -83,12 +83,12 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper(setAsMainLooper = true) -class OverviewProxyServiceTest : SysuiTestCase() { +class LauncherProxyServiceTest : SysuiTestCase() { @Main private val executor: Executor = MoreExecutors.directExecutor() private val kosmos = testKosmos() - private lateinit var subject: OverviewProxyService + private lateinit var subject: LauncherProxyService @Mock private val dumpManager = DumpManager() @Mock private val processWrapper = ProcessWrapper() private val displayTracker = FakeDisplayTracker(mContext) @@ -97,10 +97,10 @@ class OverviewProxyServiceTest : SysuiTestCase() { private val wakefulnessLifecycle = WakefulnessLifecycle(mContext, null, fakeSystemClock, dumpManager) - @Mock private lateinit var overviewProxy: IOverviewProxy.Stub + @Mock private lateinit var launcherProxy: ILauncherProxy.Stub @Mock private lateinit var packageManager: PackageManager - // The following mocks belong to not-yet-tested parts of OverviewProxyService. + // The following mocks belong to not-yet-tested parts of LauncherProxyService. @Mock private lateinit var commandQueue: CommandQueue @Mock private lateinit var shellInterface: ShellInterface @Mock private lateinit var navBarController: NavigationBarController @@ -127,18 +127,18 @@ class OverviewProxyServiceTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) val serviceComponent = ComponentName("test_package", "service_provider") - context.addMockService(serviceComponent, overviewProxy) + context.addMockService(serviceComponent, launcherProxy) context.addMockServiceResolver( TestableContext.MockServiceResolver { if (it.action == ACTION_QUICKSTEP) serviceComponent else null } ) - whenever(overviewProxy.queryLocalInterface(ArgumentMatchers.anyString())) - .thenReturn(overviewProxy) - whenever(overviewProxy.asBinder()).thenReturn(overviewProxy) + whenever(launcherProxy.queryLocalInterface(ArgumentMatchers.anyString())) + .thenReturn(launcherProxy) + whenever(launcherProxy.asBinder()).thenReturn(launcherProxy) // packageManager.resolveServiceAsUser has to return non-null for - // OverviewProxyService#isEnabled to become true. + // LauncherProxyService#isEnabled to become true. context.setMockPackageManager(packageManager) whenever(packageManager.resolveServiceAsUser(any(), anyInt(), anyInt())) .thenReturn(mock(ResolveInfo::class.java)) @@ -147,7 +147,7 @@ class OverviewProxyServiceTest : SysuiTestCase() { // return isSystemUser as true by default. `when`(processWrapper.isSystemUser).thenReturn(true) - subject = createOverviewProxyService(context) + subject = createLauncherProxyService(context) } @After @@ -159,11 +159,11 @@ class OverviewProxyServiceTest : SysuiTestCase() { fun wakefulnessLifecycle_dispatchFinishedWakingUpSetsSysUIflagToAWAKE() { // WakefulnessLifecycle is initialized to AWAKE initially, and won't emit a noop. wakefulnessLifecycle.dispatchFinishedGoingToSleep() - clearInvocations(overviewProxy) + clearInvocations(launcherProxy) wakefulnessLifecycle.dispatchFinishedWakingUp() - verify(overviewProxy) + verify(launcherProxy) .onSystemUiStateChanged( longThat { it and SYSUI_STATE_WAKEFULNESS_MASK == WAKEFULNESS_AWAKE } ) @@ -173,7 +173,7 @@ class OverviewProxyServiceTest : SysuiTestCase() { fun wakefulnessLifecycle_dispatchStartedWakingUpSetsSysUIflagToWAKING() { wakefulnessLifecycle.dispatchStartedWakingUp(PowerManager.WAKE_REASON_UNKNOWN) - verify(overviewProxy) + verify(launcherProxy) .onSystemUiStateChanged( longThat { it and SYSUI_STATE_WAKEFULNESS_MASK == WAKEFULNESS_WAKING } ) @@ -183,7 +183,7 @@ class OverviewProxyServiceTest : SysuiTestCase() { fun wakefulnessLifecycle_dispatchFinishedGoingToSleepSetsSysUIflagToASLEEP() { wakefulnessLifecycle.dispatchFinishedGoingToSleep() - verify(overviewProxy) + verify(launcherProxy) .onSystemUiStateChanged( longThat { it and SYSUI_STATE_WAKEFULNESS_MASK == WAKEFULNESS_ASLEEP } ) @@ -195,56 +195,56 @@ class OverviewProxyServiceTest : SysuiTestCase() { PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON ) - verify(overviewProxy) + verify(launcherProxy) .onSystemUiStateChanged( longThat { it and SYSUI_STATE_WAKEFULNESS_MASK == WAKEFULNESS_GOING_TO_SLEEP } ) } @Test - fun connectToOverviewService_primaryUserNoVisibleBgUsersSupported_expectBindService() { + fun connectToLauncherService_primaryUserNoVisibleBgUsersSupported_expectBindService() { `when`(processWrapper.isSystemUser).thenReturn(true) `when`(userManager.isVisibleBackgroundUsersSupported()).thenReturn(false) val spyContext = spy(context) - val ops = createOverviewProxyService(spyContext) + val ops = createLauncherProxyService(spyContext) ops.startConnectionToCurrentUser() verify(spyContext, atLeast(1)).bindServiceAsUser(any(), any(), anyInt(), any()) } @Test - fun connectToOverviewService_nonPrimaryUserNoVisibleBgUsersSupported_expectNoBindService() { + fun connectToLauncherService_nonPrimaryUserNoVisibleBgUsersSupported_expectNoBindService() { `when`(processWrapper.isSystemUser).thenReturn(false) `when`(userManager.isVisibleBackgroundUsersSupported()).thenReturn(false) val spyContext = spy(context) - val ops = createOverviewProxyService(spyContext) + val ops = createLauncherProxyService(spyContext) ops.startConnectionToCurrentUser() verify(spyContext, times(0)).bindServiceAsUser(any(), any(), anyInt(), any()) } @Test - fun connectToOverviewService_nonPrimaryBgUserVisibleBgUsersSupported_expectBindService() { + fun connectToLauncherService_nonPrimaryBgUserVisibleBgUsersSupported_expectBindService() { `when`(processWrapper.isSystemUser).thenReturn(false) `when`(userManager.isVisibleBackgroundUsersSupported()).thenReturn(true) `when`(userManager.isUserForeground()).thenReturn(false) val spyContext = spy(context) - val ops = createOverviewProxyService(spyContext) + val ops = createLauncherProxyService(spyContext) ops.startConnectionToCurrentUser() verify(spyContext, atLeast(1)).bindServiceAsUser(any(), any(), anyInt(), any()) } @Test - fun connectToOverviewService_nonPrimaryFgUserVisibleBgUsersSupported_expectNoBindService() { + fun connectToLauncherService_nonPrimaryFgUserVisibleBgUsersSupported_expectNoBindService() { `when`(processWrapper.isSystemUser).thenReturn(false) `when`(userManager.isVisibleBackgroundUsersSupported()).thenReturn(true) `when`(userManager.isUserForeground()).thenReturn(true) val spyContext = spy(context) - val ops = createOverviewProxyService(spyContext) + val ops = createLauncherProxyService(spyContext) ops.startConnectionToCurrentUser() verify(spyContext, times(0)).bindServiceAsUser(any(), any(), anyInt(), any()) } - private fun createOverviewProxyService(ctx: Context): OverviewProxyService { - return OverviewProxyService( + private fun createLauncherProxyService(ctx: Context): LauncherProxyService { + return LauncherProxyService( ctx, executor, commandQueue, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index e68153ad2606..70450d29c74e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -57,7 +57,10 @@ import com.android.systemui.res.R import com.android.systemui.settings.brightness.data.repository.BrightnessMirrorShowingRepository import com.android.systemui.settings.brightness.domain.interactor.BrightnessMirrorShowingInteractor import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler +import com.android.systemui.shade.data.repository.ShadeAnimationRepository +import com.android.systemui.shade.data.repository.ShadeRepositoryImpl import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor +import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractorLegacyImpl import com.android.systemui.statusbar.BlurUtils import com.android.systemui.statusbar.DragDownHelper import com.android.systemui.statusbar.LockscreenShadeTransitionController @@ -219,6 +222,10 @@ class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization) : notificationShadeDepthController, view, shadeViewController, + ShadeAnimationInteractorLegacyImpl( + ShadeAnimationRepository(), + ShadeRepositoryImpl(testScope), + ), panelExpansionInteractor, ShadeExpansionStateManager(), stackScrollLayoutController, @@ -521,6 +528,18 @@ class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization) : verify(view).findViewById<ViewGroup>(R.id.keyguard_message_area) } + @EnableFlags(Flags.FLAG_SHADE_LAUNCH_ACCESSIBILITY) + @Test + fun notifiesTheViewWhenLaunchAnimationIsRunning() { + testScope.runTest { + underTest.setExpandAnimationRunning(true) + verify(view).setAnimatingContentLaunch(true) + + underTest.setExpandAnimationRunning(false) + verify(view).setAnimatingContentLaunch(false) + } + } + @Test @DisableSceneContainer fun setsUpCommunalHubLayout_whenFlagEnabled() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt index a04ca038021e..9abe9aa5e598 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt @@ -32,8 +32,8 @@ import com.android.systemui.fragments.FragmentService import com.android.systemui.navigationbar.NavigationModeController import com.android.systemui.navigationbar.NavigationModeController.ModeChangedListener import com.android.systemui.plugins.qs.QS -import com.android.systemui.recents.OverviewProxyService -import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener +import com.android.systemui.recents.LauncherProxyService +import com.android.systemui.recents.LauncherProxyService.LauncherProxyListener import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController @@ -71,7 +71,7 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { private val view = mock<NotificationsQuickSettingsContainer>() private val navigationModeController = mock<NavigationModeController>() - private val overviewProxyService = mock<OverviewProxyService>() + private val mLauncherProxyService = mock<LauncherProxyService>() private val shadeHeaderController = mock<ShadeHeaderController>() private val shadeInteractor = mock<ShadeInteractor>() private val fragmentService = mock<FragmentService>() @@ -81,7 +81,7 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { private val largeScreenHeaderHelper = mock<LargeScreenHeaderHelper>() @Captor lateinit var navigationModeCaptor: ArgumentCaptor<ModeChangedListener> - @Captor lateinit var taskbarVisibilityCaptor: ArgumentCaptor<OverviewProxyListener> + @Captor lateinit var taskbarVisibilityCaptor: ArgumentCaptor<LauncherProxyListener> @Captor lateinit var windowInsetsCallbackCaptor: ArgumentCaptor<Consumer<WindowInsets>> @Captor lateinit var constraintSetCaptor: ArgumentCaptor<ConstraintSet> @Captor lateinit var attachStateListenerCaptor: ArgumentCaptor<View.OnAttachStateChangeListener> @@ -89,7 +89,7 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { lateinit var underTest: NotificationsQSContainerController private lateinit var navigationModeCallback: ModeChangedListener - private lateinit var taskbarVisibilityCallback: OverviewProxyListener + private lateinit var taskbarVisibilityCallback: LauncherProxyListener private lateinit var windowInsetsCallback: Consumer<WindowInsets> private lateinit var fakeSystemClock: FakeSystemClock private lateinit var delayableExecutor: FakeExecutor @@ -110,7 +110,7 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { NotificationsQSContainerController( view, navigationModeController, - overviewProxyService, + mLauncherProxyService, shadeHeaderController, shadeInteractor, fragmentService, @@ -127,7 +127,7 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { overrideResource(R.dimen.qs_footer_action_inset, FOOTER_ACTIONS_INSET) whenever(navigationModeController.addListener(navigationModeCaptor.capture())) .thenReturn(GESTURES_NAVIGATION) - doNothing().`when`(overviewProxyService).addCallback(taskbarVisibilityCaptor.capture()) + doNothing().`when`(mLauncherProxyService).addCallback(taskbarVisibilityCaptor.capture()) doNothing().`when`(view).setInsetsChangedListener(windowInsetsCallbackCaptor.capture()) doNothing().`when`(view).applyConstraints(constraintSetCaptor.capture()) doNothing().`when`(view).addOnAttachStateChangeListener(attachStateListenerCaptor.capture()) @@ -402,7 +402,7 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { NotificationsQSContainerController( container, navigationModeController, - overviewProxyService, + mLauncherProxyService, shadeHeaderController, shadeInteractor, fragmentService, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt index 24f8843e935d..4c12cc886e33 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt @@ -32,8 +32,8 @@ import com.android.systemui.fragments.FragmentService import com.android.systemui.navigationbar.NavigationModeController import com.android.systemui.navigationbar.NavigationModeController.ModeChangedListener import com.android.systemui.plugins.qs.QS -import com.android.systemui.recents.OverviewProxyService -import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener +import com.android.systemui.recents.LauncherProxyService +import com.android.systemui.recents.LauncherProxyService.LauncherProxyListener import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController @@ -70,7 +70,7 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { private val view = mock<NotificationsQuickSettingsContainer>() private val navigationModeController = mock<NavigationModeController>() - private val overviewProxyService = mock<OverviewProxyService>() + private val mLauncherProxyService = mock<LauncherProxyService>() private val shadeHeaderController = mock<ShadeHeaderController>() private val shadeInteractor = mock<ShadeInteractor>() private val fragmentService = mock<FragmentService>() @@ -80,7 +80,7 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { private val largeScreenHeaderHelper = mock<LargeScreenHeaderHelper>() @Captor lateinit var navigationModeCaptor: ArgumentCaptor<ModeChangedListener> - @Captor lateinit var taskbarVisibilityCaptor: ArgumentCaptor<OverviewProxyListener> + @Captor lateinit var taskbarVisibilityCaptor: ArgumentCaptor<LauncherProxyListener> @Captor lateinit var windowInsetsCallbackCaptor: ArgumentCaptor<Consumer<WindowInsets>> @Captor lateinit var constraintSetCaptor: ArgumentCaptor<ConstraintSet> @Captor lateinit var attachStateListenerCaptor: ArgumentCaptor<View.OnAttachStateChangeListener> @@ -88,7 +88,7 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { lateinit var underTest: NotificationsQSContainerController private lateinit var navigationModeCallback: ModeChangedListener - private lateinit var taskbarVisibilityCallback: OverviewProxyListener + private lateinit var taskbarVisibilityCallback: LauncherProxyListener private lateinit var windowInsetsCallback: Consumer<WindowInsets> private lateinit var fakeSystemClock: FakeSystemClock private lateinit var delayableExecutor: FakeExecutor @@ -110,7 +110,7 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { NotificationsQSContainerController( view, navigationModeController, - overviewProxyService, + mLauncherProxyService, shadeHeaderController, shadeInteractor, fragmentService, @@ -127,7 +127,7 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { overrideResource(R.dimen.qs_footer_action_inset, FOOTER_ACTIONS_INSET) whenever(navigationModeController.addListener(navigationModeCaptor.capture())) .thenReturn(GESTURES_NAVIGATION) - doNothing().`when`(overviewProxyService).addCallback(taskbarVisibilityCaptor.capture()) + doNothing().`when`(mLauncherProxyService).addCallback(taskbarVisibilityCaptor.capture()) doNothing().`when`(view).setInsetsChangedListener(windowInsetsCallbackCaptor.capture()) doNothing().`when`(view).applyConstraints(constraintSetCaptor.capture()) doNothing().`when`(view).addOnAttachStateChangeListener(attachStateListenerCaptor.capture()) @@ -457,7 +457,7 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { NotificationsQSContainerController( container, navigationModeController, - overviewProxyService, + mLauncherProxyService, shadeHeaderController, shadeInteractor, fragmentService, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt index eae828562223..e8ab76181af2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt @@ -53,6 +53,7 @@ import com.android.systemui.shade.ShadeHeaderController.Companion.QQS_HEADER_CON import com.android.systemui.shade.ShadeHeaderController.Companion.QS_HEADER_CONSTRAINT import com.android.systemui.shade.carrier.ShadeCarrierGroup import com.android.systemui.shade.carrier.ShadeCarrierGroupController +import com.android.systemui.shade.data.repository.shadeDisplaysRepository import com.android.systemui.statusbar.data.repository.fakeStatusBarContentInsetsProviderStore import com.android.systemui.statusbar.phone.StatusIconContainer import com.android.systemui.statusbar.phone.StatusOverlayHoverListenerFactory @@ -96,7 +97,7 @@ class ShadeHeaderControllerTest : SysuiTestCase() { private val kosmos = testKosmos() private val insetsProviderStore = kosmos.fakeStatusBarContentInsetsProviderStore - private val insetsProvider = insetsProviderStore.defaultDisplay + private val insetsProvider = insetsProviderStore.forDisplay(context.displayId) @Mock(answer = Answers.RETURNS_MOCKS) private lateinit var view: MotionLayout @Mock private lateinit var statusIcons: StatusIconContainer @@ -196,6 +197,7 @@ class ShadeHeaderControllerTest : SysuiTestCase() { privacyIconsController, insetsProviderStore, configurationController, + kosmos.shadeDisplaysRepository, variableDateViewControllerFactory, batteryMeterViewController, dumpManager, @@ -294,7 +296,7 @@ class ShadeHeaderControllerTest : SysuiTestCase() { verify(clock).setTextAppearance(R.style.TextAppearance_QS_Status) verify(date).setTextAppearance(R.style.TextAppearance_QS_Status) - verify(carrierGroup).updateTextAppearance(R.style.TextAppearance_QS_Status_Carriers) + verify(carrierGroup).updateTextAppearance(R.style.TextAppearance_QS_Status) } @Test @@ -809,6 +811,43 @@ class ShadeHeaderControllerTest : SysuiTestCase() { } @Test + fun sameInsetsTwice_listenerCallsOnApplyWindowInsetsOnlyOnce() { + val windowInsets = createWindowInsets() + + val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java) + verify(view).setOnApplyWindowInsetsListener(capture(captor)) + + val listener = captor.value + + listener.onApplyWindowInsets(view, windowInsets) + + verify(view, times(1)).onApplyWindowInsets(any()) + + listener.onApplyWindowInsets(view, windowInsets) + + verify(view, times(1)).onApplyWindowInsets(any()) + } + + @Test + fun twoDifferentInsets_listenerCallsOnApplyWindowInsetsTwice() { + val windowInsets1 = WindowInsets(Rect(1, 2, 3, 4)) + val windowInsets2 = WindowInsets(Rect(5, 6, 7, 8)) + + val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java) + verify(view).setOnApplyWindowInsetsListener(capture(captor)) + + val listener = captor.value + + listener.onApplyWindowInsets(view, windowInsets1) + + verify(view, times(1)).onApplyWindowInsets(any()) + + listener.onApplyWindowInsets(view, windowInsets2) + + verify(view, times(2)).onApplyWindowInsets(any()) + } + + @Test fun alarmIconNotIgnored() { verify(statusIcons, Mockito.never()) .addIgnoredSlot(context.getString(com.android.internal.R.string.status_bar_alarm_clock)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java index 5aee92939ed5..493468e8f675 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java @@ -69,6 +69,7 @@ import com.android.systemui.statusbar.notification.FeedbackIcon; import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.headsup.PinnedStatus; +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded; import com.android.systemui.statusbar.notification.row.ExpandableView.OnHeightChangedListener; import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; import com.android.systemui.statusbar.notification.shared.NotificationContentAlphaOptimization; @@ -878,6 +879,85 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { } @Test + @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME) + public void isExpanded_sensitivePromotedNotification_notExpanded() throws Exception { + // GIVEN + final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); + row.setPromotedOngoing(true); + row.setSensitive(/* sensitive= */true, /* hideSensitive= */false); + row.setHideSensitiveForIntrinsicHeight(/* hideSensitive= */true); + + // THEN + assertThat(row.isExpanded()).isFalse(); + } + + @Test + @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME) + public void isExpanded_promotedNotificationNotOnKeyguard_expanded() throws Exception { + // GIVEN + final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); + row.setPromotedOngoing(true); + row.setOnKeyguard(false); + + // THEN + assertThat(row.isExpanded()).isTrue(); + } + + @Test + @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME) + public void isExpanded_promotedNotificationAllowOnKeyguard_expanded() throws Exception { + // GIVEN + final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); + row.setPromotedOngoing(true); + row.setOnKeyguard(true); + + // THEN + assertThat(row.isExpanded(/* allowOnKeyguard = */ true)).isTrue(); + } + + @Test + @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME) + public void isExpanded_promotedNotificationIgnoreLockscreenConstraints_expanded() + throws Exception { + // GIVEN + final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); + row.setPromotedOngoing(true); + row.setOnKeyguard(true); + row.setIgnoreLockscreenConstraints(true); + + // THEN + assertThat(row.isExpanded()).isTrue(); + } + + @Test + @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME) + public void isExpanded_promotedNotificationSaveSpaceOnLockScreen_notExpanded() + throws Exception { + // GIVEN + final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); + row.setPromotedOngoing(true); + row.setOnKeyguard(true); + row.setSaveSpaceOnLockscreen(true); + + // THEN + assertThat(row.isExpanded()).isFalse(); + } + + @Test + @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME) + public void isExpanded_promotedNotificationNotSaveSpaceOnLockScreen_expanded() + throws Exception { + // GIVEN + final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); + row.setPromotedOngoing(true); + row.setOnKeyguard(true); + row.setSaveSpaceOnLockscreen(false); + + // THEN + assertThat(row.isExpanded()).isTrue(); + } + + @Test public void onDisappearAnimationFinished_shouldSetFalse_headsUpAnimatingAway() throws Exception { final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); @@ -941,6 +1021,57 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { assertThat(row.getImageResolver().getContext()).isSameInstanceAs(userContext); } + @Test + @EnableFlags(com.android.systemui.Flags.FLAG_NOTIFICATIONS_PINNED_HUN_IN_SHADE) + public void mustStayOnScreen_false() throws Exception { + final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); + assertThat(row.mustStayOnScreen()).isFalse(); + } + + @Test + @EnableFlags(com.android.systemui.Flags.FLAG_NOTIFICATIONS_PINNED_HUN_IN_SHADE) + public void mustStayOnScreen_isHeadsUp_markedAsSeen() throws Exception { + final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); + // When the row is a HUN + row.setHeadsUp(true); + //Then it must stay on screen + assertThat(row.mustStayOnScreen()).isTrue(); + // And when the user has seen it + row.markHeadsUpSeen(); + // Then it should NOT stay on screen anymore + assertThat(row.mustStayOnScreen()).isFalse(); + } + + @Test + @EnableFlags(com.android.systemui.Flags.FLAG_NOTIFICATIONS_PINNED_HUN_IN_SHADE) + public void mustStayOnScreen_isPinned_markedAsSeen() throws Exception { + final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); + // When a HUN is pinned + row.setHeadsUp(true); + row.setPinnedStatus(PinnedStatus.PinnedBySystem); + //Then it must stay on screen + assertThat(row.mustStayOnScreen()).isTrue(); + // And when the user has seen it + row.markHeadsUpSeen(); + // Then it should still stay on screen + assertThat(row.mustStayOnScreen()).isTrue(); + } + + @Test + @DisableFlags(com.android.systemui.Flags.FLAG_NOTIFICATIONS_PINNED_HUN_IN_SHADE) + public void mustStayOnScreen_isPinned_markedAsSeen_false() throws Exception { + final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); + // When a HUN is pinned + row.setHeadsUp(true); + row.setPinnedStatus(PinnedStatus.PinnedBySystem); + //Then it must stay on screen + assertThat(row.mustStayOnScreen()).isTrue(); + // And when the user has seen it + row.markHeadsUpSeen(); + // Then it should NOT stay on screen anymore + assertThat(row.mustStayOnScreen()).isFalse(); + } + private void setDrawableIconsInImageView(CachingIconView icon, Drawable iconDrawable, Drawable rightIconDrawable) { ImageView iconView = mock(ImageView.class); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt index 61943f2283e0..8645a40319f4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt @@ -444,6 +444,7 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { eq(true), /* wasShownHighPriority */ eq(assistantFeedbackController), any<MetricsLogger>(), + any<View.OnClickListener>(), ) } @@ -476,6 +477,7 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { eq(false), /* wasShownHighPriority */ eq(assistantFeedbackController), any<MetricsLogger>(), + any<View.OnClickListener>(), ) } @@ -508,6 +510,7 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { eq(false), /* wasShownHighPriority */ eq(assistantFeedbackController), any<MetricsLogger>(), + any<View.OnClickListener>(), ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewBinderTest.kt index 1eb88c5a5616..0457255fee4e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewBinderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewBinderTest.kt @@ -19,10 +19,12 @@ import android.app.Notification import android.app.Person import android.platform.test.annotations.EnableFlags import android.testing.TestableLooper +import android.view.View.GONE import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.R import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.RankingBuilder import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_SINGLE_LINE import com.android.systemui.statusbar.notification.row.SingleLineViewInflater.inflatePrivateSingleLineView @@ -90,6 +92,7 @@ class SingleLineViewBinderTest : SysuiTestCase() { builder = notificationBuilder, systemUiContext = context, redactText = false, + summarization = null ) // WHEN: binds the viewHolder @@ -151,6 +154,7 @@ class SingleLineViewBinderTest : SysuiTestCase() { builder = notificationBuilder, systemUiContext = context, redactText = false, + summarization = null ) // WHEN: binds the view SingleLineViewBinder.bind(viewModel, view) @@ -200,6 +204,7 @@ class SingleLineViewBinderTest : SysuiTestCase() { builder = notificationBuilder, systemUiContext = context, redactText = false, + summarization = null ) // WHEN: binds the view with the view model SingleLineViewBinder.bind(viewModel, view) @@ -211,6 +216,70 @@ class SingleLineViewBinderTest : SysuiTestCase() { assertNull(viewModel.conversationData) } + @Test + @EnableFlags(AsyncHybridViewInflation.FLAG_NAME, android.app.Flags.FLAG_NM_SUMMARIZATION_UI, + android.app.Flags.FLAG_NM_SUMMARIZATION) + fun bindSummarizedGroupConversationSingleLineView() { + // GIVEN a row with a group conversation notification + val user = + Person.Builder() + .setName(USER_NAME) + .build() + val style = + Notification.MessagingStyle(user) + .addMessage(MESSAGE_TEXT, System.currentTimeMillis(), user) + .addMessage( + "How about lunch?", + System.currentTimeMillis(), + Person.Builder().setName("user2").build(), + ) + .setGroupConversation(true) + notificationBuilder.setStyle(style).setShortcutId(SHORTCUT_ID) + val notification = notificationBuilder.build() + val row = helper.createRow(notification) + val rb = RankingBuilder(row.entry.ranking) + rb.setSummarization("summary!") + row.entry.ranking = rb.build() + + val view = + inflatePrivateSingleLineView( + isConversation = true, + reinflateFlags = FLAG_CONTENT_VIEW_SINGLE_LINE, + entry = row.entry, + context = context, + logger = mock(), + ) + as HybridConversationNotificationView + + val publicView = + inflatePublicSingleLineView( + isConversation = true, + reinflateFlags = FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE, + entry = row.entry, + context = context, + logger = mock(), + ) + as HybridConversationNotificationView + assertNotNull(publicView) + + val viewModel = + SingleLineViewInflater.inflateSingleLineViewModel( + notification = notification, + messagingStyle = style, + builder = notificationBuilder, + systemUiContext = context, + redactText = false, + summarization = "summary" + ) + // WHEN: binds the view + SingleLineViewBinder.bind(viewModel, view) + + // THEN: the single-line conversation view should only include summarization content + assertEquals(viewModel.conversationData?.summarization, view.textView.text) + assertEquals("", view.conversationSenderNameView.text) + assertEquals(GONE, view.conversationSenderNameView.visibility) + } + private companion object { const val CHANNEL_ID = "CHANNEL_ID" const val CONTENT_TITLE = "A Cool New Feature" @@ -218,5 +287,6 @@ class SingleLineViewBinderTest : SysuiTestCase() { const val USER_NAME = "USER_NAME" const val MESSAGE_TEXT = "MESSAGE_TEXT" const val SHORTCUT_ID = "Shortcut" + const val SUMMARIZATION = "summarization" } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt index ef70e277832e..13724a8b44da 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt @@ -272,6 +272,35 @@ class SingleLineViewInflaterTest : SysuiTestCase() { } } + @Test + fun createViewModelForSummarizedConversationNotification() { + // Given: a non-group conversation notification + val notificationType = OneToOneConversation() + val notification = getNotification(notificationType) + + // When: inflate the SingleLineViewModel + val singleLineViewModel = notification.makeSingleLineViewModel(notificationType) + + // Then: the inflated SingleLineViewModel should be as expected + // titleText: Notification.ConversationTitle + // contentText: the last message text + // conversationSenderName: null, because it's not a group conversation + // conversationData.avatar: a single icon of the last sender + // summarizedText: the summary text from the ranking + assertEquals(CONVERSATION_TITLE, singleLineViewModel.titleText) + assertEquals(LAST_MESSAGE, singleLineViewModel.contentText) + assertNull( + singleLineViewModel.conversationData?.conversationSenderName, + "Sender name should be null for one-on-one conversation" + ) + assertTrue { + singleLineViewModel.conversationData + ?.avatar + ?.equalsTo(SingleIcon(firstSenderIcon.loadDrawable(context))) == true + } + assertEquals("summary", singleLineViewModel.conversationData?.summarization) + } + sealed class NotificationType(val largeIcon: Icon? = null) class NonMessaging(largeIcon: Icon? = null) : NotificationType(largeIcon) @@ -380,7 +409,8 @@ class SingleLineViewInflaterTest : SysuiTestCase() { if (isConversation) messagingStyle else null, builder, context, - false + false, + "summary" ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt index 437ccb6a9821..68f66611c981 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt @@ -90,6 +90,8 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { private val kosmos = testKosmos() private val statusBarContentInsetsProviderStore = kosmos.fakeStatusBarContentInsetsProviderStore private val statusBarContentInsetsProvider = statusBarContentInsetsProviderStore.defaultDisplay + private val statusBarContentInsetsProviderForSecondaryDisplay = + statusBarContentInsetsProviderStore.forDisplay(SECONDARY_DISPLAY_ID) private val fakeDarkIconDispatcher = kosmos.fakeDarkIconDispatcher @Mock private lateinit var shadeViewController: ShadeViewController @@ -144,6 +146,12 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { controller = createAndInitController(view) } + `when`( + statusBarContentInsetsProviderForSecondaryDisplay + .getStatusBarContentInsetsForCurrentRotation() + ) + .thenReturn(Insets.NONE) + val contextForSecondaryDisplay = SysuiTestableContext( mContext.createDisplayContext( diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java index a02d333d1507..a7fe1ba76590 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java @@ -59,6 +59,7 @@ import com.android.internal.colorextraction.ColorExtractor.GradientColors; import com.android.keyguard.BouncerPanelExpansionCalculator; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.DejankUtils; +import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.ShadeInterpolation; import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants; @@ -71,6 +72,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInterac import com.android.systemui.keyguard.shared.model.KeyguardState; import com.android.systemui.keyguard.shared.model.TransitionState; import com.android.systemui.keyguard.shared.model.TransitionStep; +import com.android.systemui.keyguard.ui.transitions.BlurConfig; import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransitionViewModel; import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel; import com.android.systemui.kosmos.KosmosJavaAdapter; @@ -110,6 +112,9 @@ import java.util.Map; @RunWith(AndroidJUnit4.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) @SmallTest +// TODO(b/381263619) there are more changes and tweaks required to match the new bouncer/shade specs +// Disabling for now but it will be fixed before the flag is fully ramped up. +@DisableFlags(Flags.FLAG_BOUNCER_UI_REVAMP) public class ScrimControllerTest extends SysuiTestCase { @Rule public Expect mExpect = Expect.create(); @@ -286,7 +291,8 @@ public class ScrimControllerTest extends SysuiTestCase { mKeyguardTransitionInteractor, mKeyguardInteractor, mKosmos.getTestDispatcher(), - mLinearLargeScreenShadeInterpolator); + mLinearLargeScreenShadeInterpolator, + new BlurConfig(0.0f, 0.0f)); mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible); mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront); mScrimController.setAnimatorListener(mAnimatorListener); @@ -1251,7 +1257,8 @@ public class ScrimControllerTest extends SysuiTestCase { mKeyguardTransitionInteractor, mKeyguardInteractor, mKosmos.getTestDispatcher(), - mLinearLargeScreenShadeInterpolator); + mLinearLargeScreenShadeInterpolator, + new BlurConfig(0.0f, 0.0f)); mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible); mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront); mScrimController.setAnimatorListener(mAnimatorListener); diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index fab7922f58e7..5d88f72b805b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -98,6 +98,7 @@ import android.view.View; import android.view.ViewTreeObserver; import android.view.WindowManager; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.test.filters.SmallTest; @@ -2894,6 +2895,12 @@ public class BubblesTest extends SysuiTestCase { @Override public void animateBubbleBarLocation(BubbleBarLocation location) { } + + @Override + public void onDragItemOverBubbleBarDragZone(@NonNull BubbleBarLocation location) {} + + @Override + public void onItemDraggedOutsideBubbleBarDropZone() {} } private static class FakeBubbleProperties implements BubbleProperties { diff --git a/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt b/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt index de4bbecaaf0e..42c509eeaa0b 100644 --- a/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt +++ b/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt @@ -16,16 +16,19 @@ package android.hardware.input +import android.hardware.input.InputGestureData.Trigger +import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_ALREADY_EXISTS +import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_DOES_NOT_EXIST +import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS import android.hardware.input.InputManager.InputDeviceListener import android.view.InputDevice import android.view.KeyCharacterMap import android.view.KeyCharacterMap.VIRTUAL_KEYBOARD import android.view.KeyEvent -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.whenever import org.mockito.ArgumentMatchers.anyInt import org.mockito.invocation.InvocationOnMock +import org.mockito.kotlin.any +import org.mockito.kotlin.mock class FakeInputManager { @@ -49,36 +52,79 @@ class FakeInputManager { ) private var inputDeviceListener: InputDeviceListener? = null + private val customInputGestures: MutableMap<Trigger, InputGestureData> = mutableMapOf() + var addCustomInputGestureErrorCode = CUSTOM_INPUT_GESTURE_RESULT_ERROR_ALREADY_EXISTS + + val inputManager: InputManager = mock { + on { getCustomInputGestures(any()) }.then { customInputGestures.values.toList() } + + on { addCustomInputGesture(any()) } + .then { + val inputGestureData = it.getArgument<InputGestureData>(0) + val trigger = inputGestureData.trigger + + if (customInputGestures.containsKey(trigger)) { + addCustomInputGestureErrorCode + } else { + customInputGestures[trigger] = inputGestureData + CUSTOM_INPUT_GESTURE_RESULT_SUCCESS + } + } + + on { removeCustomInputGesture(any()) } + .then { + val inputGestureData = it.getArgument<InputGestureData>(0) + val trigger = inputGestureData.trigger + + if (customInputGestures.containsKey(trigger)) { + customInputGestures.remove(trigger) + CUSTOM_INPUT_GESTURE_RESULT_SUCCESS + } else { + CUSTOM_INPUT_GESTURE_RESULT_ERROR_DOES_NOT_EXIST + } + } + + on { removeAllCustomInputGestures(any()) }.then { customInputGestures.clear() } - val inputManager = - mock<InputManager> { - whenever(getInputDevice(anyInt())).thenAnswer { invocation -> + on { getInputGesture(any()) } + .then { + val trigger = it.getArgument<Trigger>(0) + customInputGestures[trigger] + } + + on { getInputDevice(anyInt()) } + .thenAnswer { invocation -> val deviceId = invocation.arguments[0] as Int return@thenAnswer devices[deviceId] } - whenever(inputDeviceIds).thenAnswer { + on { inputDeviceIds } + .thenAnswer { return@thenAnswer devices.keys.toIntArray() } - fun setDeviceEnabled(invocation: InvocationOnMock, enabled: Boolean) { - val deviceId = invocation.arguments[0] as Int - val device = devices[deviceId] ?: return - devices[deviceId] = device.copy(enabled = enabled) - } + fun setDeviceEnabled(invocation: InvocationOnMock, enabled: Boolean) { + val deviceId = invocation.arguments[0] as Int + val device = devices[deviceId] ?: return + devices[deviceId] = device.copy(enabled = enabled) + } - whenever(disableInputDevice(anyInt())).thenAnswer { invocation -> - setDeviceEnabled(invocation, enabled = false) - } - whenever(enableInputDevice(anyInt())).thenAnswer { invocation -> - setDeviceEnabled(invocation, enabled = true) - } - whenever(deviceHasKeys(any(), any())).thenAnswer { invocation -> + on { disableInputDevice(anyInt()) } + .thenAnswer { invocation -> setDeviceEnabled(invocation, enabled = false) } + on { enableInputDevice(anyInt()) } + .thenAnswer { invocation -> setDeviceEnabled(invocation, enabled = true) } + on { deviceHasKeys(any(), any()) } + .thenAnswer { invocation -> val deviceId = invocation.arguments[0] as Int val keyCodes = invocation.arguments[1] as IntArray val supportedKeyCodes = supportedKeyCodesByDeviceId[deviceId]!! return@thenAnswer keyCodes.map { supportedKeyCodes.contains(it) }.toBooleanArray() } - } + } + + fun resetCustomInputGestures() { + customInputGestures.clear() + addCustomInputGestureErrorCode = CUSTOM_INPUT_GESTURE_RESULT_ERROR_ALREADY_EXISTS + } fun addPhysicalKeyboardIfNotPresent(deviceId: Int, enabled: Boolean = true) { if (devices.containsKey(deviceId)) { @@ -97,7 +143,7 @@ class FakeInputManager { vendorId: Int = 0, productId: Int = 0, isFullKeyboard: Boolean = true, - enabled: Boolean = true + enabled: Boolean = true, ) { check(id > 0) { "Physical keyboard ids have to be > 0" } addKeyboard(id, vendorId, productId, isFullKeyboard, enabled) @@ -113,7 +159,7 @@ class FakeInputManager { vendorId: Int = 0, productId: Int = 0, isFullKeyboard: Boolean = true, - enabled: Boolean = true + enabled: Boolean = true, ) { val keyboardType = if (isFullKeyboard) InputDevice.KEYBOARD_TYPE_ALPHABETIC @@ -152,7 +198,7 @@ class FakeInputManager { id: Int = getId(), type: Int = keyboardType, sources: Int = getSources(), - enabled: Boolean = isEnabled + enabled: Boolean = isEnabled, ) = InputDevice.Builder() .setId(id) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeFocusedDisplayRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeFocusedDisplayRepository.kt index 83df5d874ad6..ad9370f7ac84 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeFocusedDisplayRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeFocusedDisplayRepository.kt @@ -33,7 +33,7 @@ class FakeFocusedDisplayRepository @Inject constructor() : FocusedDisplayReposit override val focusedDisplayId: StateFlow<Int> get() = flow.asStateFlow() - suspend fun emit(focusedDisplay: Int) = flow.emit(focusedDisplay) + suspend fun setDisplayId(focusedDisplay: Int) = flow.emit(focusedDisplay) } @Module diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt index 93e7f2e588b0..83f4e8f5aa49 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt @@ -25,7 +25,7 @@ import com.android.systemui.keyboard.data.repository.keyboardRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope -import com.android.systemui.recents.OverviewProxyService +import com.android.systemui.recents.LauncherProxyService import com.android.systemui.touchpad.data.repository.touchpadRepository import com.android.systemui.user.data.repository.userRepository import org.mockito.kotlin.mock @@ -43,12 +43,12 @@ var Kosmos.keyboardTouchpadEduInteractor by userRepository, ), tutorialRepository = tutorialSchedulerRepository, - overviewProxyService = mockOverviewProxyService, + launcherProxyService = mockLauncherProxyService, metricsLogger = mockEduMetricsLogger, clock = fakeEduClock, ) } var Kosmos.mockEduMetricsLogger by Kosmos.Fixture { mock<ContextualEducationMetricsLogger>() } -var Kosmos.mockOverviewProxyService by Kosmos.Fixture { mock<OverviewProxyService>() } +var Kosmos.mockLauncherProxyService by Kosmos.Fixture { mock<LauncherProxyService>() } var Kosmos.mockEduInputManager by Kosmos.Fixture { mock<InputManager>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt index 0192fa47b434..739f6c2af2b4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt @@ -79,7 +79,7 @@ var Kosmos.shortcutHelperInputShortcutsSource: KeyboardShortcutGroupsSource by var Kosmos.shortcutHelperCurrentAppShortcutsSource: KeyboardShortcutGroupsSource by Kosmos.Fixture { CurrentAppShortcutsSource(windowManager) } -val Kosmos.shortcutHelperAccessibilityShortcutsSource: KeyboardShortcutGroupsSource by +var Kosmos.shortcutHelperAccessibilityShortcutsSource: KeyboardShortcutGroupsSource by Kosmos.Fixture { AccessibilityShortcutsSource(mainResources) } val Kosmos.shortcutHelperExclusions by diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 8489d8380041..8ea80081a871 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.data.repository import android.graphics.Point +import android.graphics.RectF import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.shared.model.BiometricUnlockMode import com.android.systemui.keyguard.shared.model.BiometricUnlockModel @@ -129,6 +130,10 @@ class FakeKeyguardRepository @Inject constructor() : KeyguardRepository { override val notificationStackAbsoluteBottom: StateFlow<Float> get() = _notificationStackAbsoluteBottom.asStateFlow() + private val _wallpaperFocalAreaBounds = MutableStateFlow(RectF(0f, 0f, 0f, 0f)) + override val wallpaperFocalAreaBounds: StateFlow<RectF> + get() = _wallpaperFocalAreaBounds.asStateFlow() + private val _isKeyguardEnabled = MutableStateFlow(true) override val isKeyguardEnabled: StateFlow<Boolean> = _isKeyguardEnabled.asStateFlow() @@ -287,6 +292,10 @@ class FakeKeyguardRepository @Inject constructor() : KeyguardRepository { _notificationStackAbsoluteBottom.value = bottom } + override fun setWallpaperFocalAreaBounds(bounds: RectF) { + _wallpaperFocalAreaBounds.value = bounds + } + override fun setCanIgnoreAuthAndReturnToGone(canWake: Boolean) { _canIgnoreAuthAndReturnToGone.value = canWake } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt index f4791003c828..026f8f97d2ae 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt @@ -124,8 +124,8 @@ class FakeKeyguardTransitionRepository( /** * Sends TransitionSteps between [from] and [to], calling [runCurrent] after each step. * - * By default, sends steps through FINISHED (STARTED, RUNNING, FINISHED) but can be halted part - * way using [throughTransitionState]. + * By default, sends steps through FINISHED (STARTED, RUNNING @0.5f, RUNNING @1f, FINISHED) but + * can be halted part way using [throughTransitionState]. */ suspend fun sendTransitionSteps( from: KeyguardState, @@ -137,6 +137,25 @@ class FakeKeyguardTransitionRepository( } /** + * Sends a STARTED step between [from] and [to], followed by two RUNNING steps at value + * [throughValue] / 2 and [throughValue], calling [runCurrent] after each step. + */ + suspend fun sendTransitionStepsThroughRunning( + from: KeyguardState, + to: KeyguardState, + testScope: TestScope, + throughValue: Float = 1f, + ) { + sendTransitionSteps( + from, + to, + testScope.testScheduler, + TransitionState.RUNNING, + throughValue, + ) + } + + /** * Sends the provided [step] and makes sure that all previous [TransitionState]'s are sent when * [fillInSteps] is true. e.g. when a step FINISHED is provided, a step with STARTED and RUNNING * is also sent. @@ -178,14 +197,15 @@ class FakeKeyguardTransitionRepository( /** * Sends TransitionSteps between [from] and [to], calling [runCurrent] after each step. * - * By default, sends steps through FINISHED (STARTED, RUNNING, FINISHED) but can be halted part - * way using [throughTransitionState]. + * By default, sends steps through FINISHED (STARTED, RUNNING @0.5f, RUNNING @1f, FINISHED) but + * can be halted part way using [throughTransitionState]. */ suspend fun sendTransitionSteps( from: KeyguardState, to: KeyguardState, testScheduler: TestCoroutineScheduler, throughTransitionState: TransitionState = TransitionState.FINISHED, + throughTransitionValue: Float = 1f, ) { val lastStep = _transitions.replayCache.lastOrNull() if (lastStep != null && lastStep.transitionState != TransitionState.FINISHED) { @@ -216,13 +236,14 @@ class FakeKeyguardTransitionRepository( throughTransitionState == TransitionState.RUNNING || throughTransitionState == TransitionState.FINISHED ) { + // Send two steps to better simulate RUNNING transitions. sendTransitionStep( step = TransitionStep( transitionState = TransitionState.RUNNING, from = from, to = to, - value = 0.5f, + value = throughTransitionValue / 2f, ) ) testScheduler.runCurrent() @@ -233,7 +254,7 @@ class FakeKeyguardTransitionRepository( transitionState = TransitionState.RUNNING, from = from, to = to, - value = 1f, + value = throughTransitionValue, ) ) testScheduler.runCurrent() diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/WallpaperFocalAreaInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/WallpaperFocalAreaInteractorKosmos.kt new file mode 100644 index 000000000000..8fd6f62b315f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/WallpaperFocalAreaInteractorKosmos.kt @@ -0,0 +1,39 @@ +/* + * 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.keyguard.domain.interactor + +import android.content.applicationContext +import com.android.systemui.keyguard.data.repository.keyguardClockRepository +import com.android.systemui.keyguard.data.repository.keyguardRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.shade.data.repository.shadeRepository +import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor +import com.android.systemui.wallpapers.data.repository.wallpaperRepository + +val Kosmos.wallpaperFocalAreaInteractor by + Kosmos.Fixture { + WallpaperFocalAreaInteractor( + applicationScope = applicationCoroutineScope, + context = applicationContext, + keyguardRepository = keyguardRepository, + shadeRepository = shadeRepository, + activeNotificationsInteractor = activeNotificationsInteractor, + keyguardClockRepository = keyguardClockRepository, + wallpaperRepository = wallpaperRepository, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelFactoryKosmos.kt index 2de0e8f76a4b..16d3fdc26613 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelFactoryKosmos.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 The Android Open Source Project + * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,16 +14,17 @@ * limitations under the License. */ -package com.android.systemui.volume.dialog.sliders.ui.viewmodel +package com.android.systemui.keyguard.ui.viewmodel +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.applicationCoroutineScope -import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSliderInputEventsInteractor +import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor -val Kosmos.volumeDialogSliderInputEventsViewModel by +val Kosmos.keyguardMediaViewModelFactory by Kosmos.Fixture { - VolumeDialogSliderInputEventsViewModel( - applicationCoroutineScope, - volumeDialogSliderInputEventsInteractor, - ) + object : KeyguardMediaViewModel.Factory { + override fun create(): KeyguardMediaViewModel { + return KeyguardMediaViewModel(mediaCarouselInteractor, keyguardInteractor) + } + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt index 40b8e0e62b03..37df05b68f9e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt @@ -20,6 +20,7 @@ import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.keyguard.domain.interactor.pulseExpansionInteractor +import com.android.systemui.keyguard.domain.interactor.wallpaperFocalAreaInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.applicationCoroutineScope @@ -87,5 +88,6 @@ val Kosmos.keyguardRootViewModel by Fixture { screenOffAnimationController = screenOffAnimationController, aodBurnInViewModel = aodBurnInViewModel, shadeInteractor = shadeInteractor, + wallpaperFocalAreaInteractor = wallpaperFocalAreaInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModelKosmos.kt index 004f97d95673..c97c4e3ba302 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModelKosmos.kt @@ -25,5 +25,6 @@ val Kosmos.occludedToPrimaryBouncerTransitionViewModel by Fixture { OccludedToPrimaryBouncerTransitionViewModel( animationFlow = keyguardTransitionAnimationFlow, blurConfig = blurConfig, + shadeDependentFlows = shadeDependentFlows, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModelKosmos.kt index 2256c10eebc9..ed5dd454a087 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModelKosmos.kt @@ -25,5 +25,6 @@ val Kosmos.primaryBouncerToOccludedTransitionViewModel by Fixture { PrimaryBouncerToOccludedTransitionViewModel( animationFlow = keyguardTransitionAnimationFlow, blurConfig = blurConfig, + shadeDependentFlows = shadeDependentFlows, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt index 9d73ae3f176f..a1e7d5cede13 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt @@ -40,7 +40,8 @@ class FakeVolumeDialogController(private val audioManager: AudioManager) : Volum private val callbacks = CopyOnWriteArraySet<VolumeDialogController.Callbacks>() private var hasVibrator: Boolean = true - private var state = VolumeDialogController.State() + var state = VolumeDialogController.State() + private set override fun setActiveStream(stream: Int) { updateState { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryKosmos.kt index aaef27d257c5..d9a348d93533 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryKosmos.kt @@ -16,12 +16,15 @@ package com.android.systemui.shade.data.repository +import com.android.systemui.display.data.repository.FakeFocusedDisplayRepository import com.android.systemui.display.data.repository.displayRepository import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.shade.display.AnyExternalShadeDisplayPolicy import com.android.systemui.shade.display.DefaultDisplayShadePolicy +import com.android.systemui.shade.display.FakeShadeDisplayPolicy +import com.android.systemui.shade.display.FocusShadeDisplayPolicy import com.android.systemui.shade.display.ShadeDisplayPolicy import com.android.systemui.shade.display.ShadeExpansionIntent import com.android.systemui.shade.display.StatusBarTouchShadeDisplayPolicy @@ -46,8 +49,6 @@ val Kosmos.statusBarTouchShadeDisplayPolicy: StatusBarTouchShadeDisplayPolicy by StatusBarTouchShadeDisplayPolicy( displayRepository = displayRepository, backgroundScope = testScope.backgroundScope, - keyguardRepository = keyguardRepository, - shadeOnDefaultDisplayWhenLocked = false, shadeInteractor = { shadeInteractor }, notificationElement = { notificationElement }, qsShadeElement = { qsElement }, @@ -55,13 +56,15 @@ val Kosmos.statusBarTouchShadeDisplayPolicy: StatusBarTouchShadeDisplayPolicy by } val Kosmos.shadeExpansionIntent: ShadeExpansionIntent by Kosmos.Fixture { statusBarTouchShadeDisplayPolicy } -val Kosmos.shadeDisplaysRepository: MutableShadeDisplaysRepository by +val Kosmos.shadeDisplaysRepository: ShadeDisplaysRepository by Kosmos.Fixture { ShadeDisplaysRepositoryImpl( bgScope = testScope.backgroundScope, globalSettings = fakeGlobalSettings, policies = shadeDisplayPolicies, defaultPolicy = defaultShadeDisplayPolicy, + shadeOnDefaultDisplayWhenLocked = true, + keyguardRepository = keyguardRepository, ) } @@ -71,8 +74,16 @@ val Kosmos.shadeDisplayPolicies: Set<ShadeDisplayPolicy> by defaultShadeDisplayPolicy, anyExternalShadeDisplayPolicy, statusBarTouchShadeDisplayPolicy, + FakeShadeDisplayPolicy, ) } val Kosmos.fakeShadeDisplaysRepository: FakeShadeDisplayRepository by Kosmos.Fixture { FakeShadeDisplayRepository() } +val Kosmos.fakeFocusedDisplayRepository: FakeFocusedDisplayRepository by + Kosmos.Fixture { FakeFocusedDisplayRepository() } + +val Kosmos.focusShadeDisplayPolicy: FocusShadeDisplayPolicy by + Kosmos.Fixture { + FocusShadeDisplayPolicy(focusedDisplayRepository = fakeFocusedDisplayRepository) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorKosmos.kt index 7892e962d63d..1b50094ec0b7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorKosmos.kt @@ -16,14 +16,57 @@ package com.android.systemui.shade.domain.interactor +import android.provider.Settings import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testScope +import com.android.systemui.shade.data.repository.fakeShadeRepository import com.android.systemui.shade.data.repository.shadeRepository +import com.android.systemui.shared.settings.data.repository.secureSettingsRepository +import kotlinx.coroutines.launch val Kosmos.shadeModeInteractor by Fixture { ShadeModeInteractorImpl( applicationScope = applicationCoroutineScope, repository = shadeRepository, + secureSettingsRepository = secureSettingsRepository, ) } + +// TODO(b/391578667): Make this user-aware once supported by FakeSecureSettingsRepository. +/** + * Enables the Dual Shade setting, and (optionally) sets the shade layout to be wide (`true`) + * or narrow (`false`). + * + * In a wide layout, notifications and quick settings shades each take up only half the screen + * width. In a narrow layout, they each take up the entire screen width. + */ +fun Kosmos.enableDualShade(wideLayout: Boolean? = null) { + testScope.launch { + secureSettingsRepository.setInt(Settings.Secure.DUAL_SHADE, 1) + + if (wideLayout != null) { + fakeShadeRepository.setShadeLayoutWide(wideLayout) + } + } +} + +// TODO(b/391578667): Make this user-aware once supported by FakeSecureSettingsRepository. +fun Kosmos.disableDualShade() { + testScope.launch { secureSettingsRepository.setInt(Settings.Secure.DUAL_SHADE, 0) } +} + +fun Kosmos.enableSingleShade() { + testScope.launch { + disableDualShade() + fakeShadeRepository.setShadeLayoutWide(false) + } +} + +fun Kosmos.enableSplitShade() { + testScope.launch { + disableDualShade() + fakeShadeRepository.setShadeLayoutWide(true) + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java index 6cd6594c3404..c6daed1aa58f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java @@ -61,6 +61,7 @@ public class RankingBuilder { private boolean mIsBubble = false; private int mProposedImportance = IMPORTANCE_UNSPECIFIED; private boolean mSensitiveContent = false; + private String mSummarization = null; public RankingBuilder() { } @@ -92,6 +93,7 @@ public class RankingBuilder { mIsBubble = ranking.isBubble(); mProposedImportance = ranking.getProposedImportance(); mSensitiveContent = ranking.hasSensitiveContent(); + mSummarization = ranking.getSummarization(); } public Ranking build() { @@ -122,7 +124,8 @@ public class RankingBuilder { mRankingAdjustment, mIsBubble, mProposedImportance, - mSensitiveContent); + mSensitiveContent, + mSummarization); return ranking; } @@ -262,6 +265,11 @@ public class RankingBuilder { return this; } + public RankingBuilder setSummarization(String summary) { + mSummarization = summary; + return this; + } + private static <E> ArrayList<E> copyList(List<E> list) { if (list == null) { return null; diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt index 36fa82f82f0d..4ca044d60f3f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt @@ -32,10 +32,8 @@ import com.android.systemui.volume.dialog.data.repository.volumeDialogVisibility import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType import com.android.systemui.volume.dialog.sliders.domain.model.volumeDialogSliderType import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogOverscrollViewBinder -import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderHapticsViewBinder import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderViewBinder import com.android.systemui.volume.dialog.sliders.ui.volumeDialogOverscrollViewBinder -import com.android.systemui.volume.dialog.sliders.ui.volumeDialogSliderHapticsViewBinder import com.android.systemui.volume.dialog.sliders.ui.volumeDialogSliderViewBinder import com.android.systemui.volume.mediaControllerRepository import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.mediaControllerInteractor @@ -65,9 +63,6 @@ fun Kosmos.volumeDialogSliderComponent(type: VolumeDialogSliderType): VolumeDial override fun sliderViewBinder(): VolumeDialogSliderViewBinder = localKosmos.volumeDialogSliderViewBinder - override fun sliderHapticsViewBinder(): VolumeDialogSliderHapticsViewBinder = - localKosmos.volumeDialogSliderHapticsViewBinder - override fun overscrollViewBinder(): VolumeDialogOverscrollViewBinder = localKosmos.volumeDialogOverscrollViewBinder } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt index c6db717e004f..484a7cc30152 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt @@ -16,14 +16,16 @@ package com.android.systemui.volume.dialog.sliders.ui +import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory import com.android.systemui.kosmos.Kosmos -import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSliderInputEventsViewModel +import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogOverscrollViewModel import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSliderViewModel val Kosmos.volumeDialogSliderViewBinder by Kosmos.Fixture { VolumeDialogSliderViewBinder( volumeDialogSliderViewModel, - volumeDialogSliderInputEventsViewModel, + volumeDialogOverscrollViewModel, + sliderHapticsViewModelFactory, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModelKosmos.kt index b26081c40c38..90bbb28ff519 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModelKosmos.kt @@ -21,6 +21,7 @@ import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.util.time.systemClock import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor import com.android.systemui.volume.dialog.shared.volumeDialogLogger +import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSliderInputEventsInteractor import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSliderInteractor import com.android.systemui.volume.dialog.sliders.domain.model.volumeDialogSliderType @@ -29,6 +30,7 @@ val Kosmos.volumeDialogSliderViewModel by VolumeDialogSliderViewModel( sliderType = volumeDialogSliderType, interactor = volumeDialogSliderInteractor, + inputEventsInteractor = volumeDialogSliderInputEventsInteractor, visibilityInteractor = volumeDialogVisibilityInteractor, coroutineScope = applicationCoroutineScope, volumeDialogSliderIconProvider = volumeDialogSliderIconProvider, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryKosmos.kt index ddb9a3ffee6d..f0c0d30e6db4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryKosmos.kt @@ -19,7 +19,6 @@ package com.android.systemui.wallpapers.data.repository import android.content.applicationContext import com.android.app.wallpaperManager import com.android.systemui.broadcast.broadcastDispatcher -import com.android.systemui.keyguard.data.repository.keyguardClockRepository import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture @@ -34,8 +33,7 @@ val Kosmos.wallpaperRepository by Fixture { bgDispatcher = testDispatcher, broadcastDispatcher = broadcastDispatcher, userRepository = userRepository, - wallpaperManager = wallpaperManager, - keyguardClockRepository = keyguardClockRepository, keyguardRepository = keyguardRepository, + wallpaperManager = wallpaperManager, ) } diff --git a/packages/Vcn/framework-b/framework-vcn-jarjar-rules.txt b/packages/Vcn/framework-b/framework-vcn-jarjar-rules.txt index 7e27b24f749c..33287b7f3449 100644 --- a/packages/Vcn/framework-b/framework-vcn-jarjar-rules.txt +++ b/packages/Vcn/framework-b/framework-vcn-jarjar-rules.txt @@ -1,2 +1,3 @@ rule android.net.vcn.persistablebundleutils.** android.net.connectivity.android.net.vcn.persistablebundleutils.@1 -rule android.net.vcn.util.** android.net.connectivity.android.net.vcn.util.@1
\ No newline at end of file +rule android.net.vcn.util.** android.net.connectivity.android.net.vcn.util.@1 +rule android.util.IndentingPrintWriter android.net.connectivity.android.util.IndentingPrintWriter
\ No newline at end of file diff --git a/packages/Vcn/service-b/src/com/android/server/ConnectivityServiceInitializerB.java b/packages/Vcn/service-b/src/com/android/server/ConnectivityServiceInitializerB.java index 81c7edf4adf1..b9dcc6160d68 100644 --- a/packages/Vcn/service-b/src/com/android/server/ConnectivityServiceInitializerB.java +++ b/packages/Vcn/service-b/src/com/android/server/ConnectivityServiceInitializerB.java @@ -32,8 +32,7 @@ import com.android.tools.r8.keepanno.annotations.UsedByReflection; // Without this annotation, this class will be treated as unused class and be removed during build // time. @UsedByReflection(kind = KeepItemKind.CLASS_AND_METHODS) -// TODO(b/374174952): Replace VANILLA_ICE_CREAM with BAKLAVA after Android B finalization -@TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@TargetApi(Build.VERSION_CODES.BAKLAVA) public final class ConnectivityServiceInitializerB extends SystemService { private static final String TAG = ConnectivityServiceInitializerB.class.getSimpleName(); private final VcnManagementService mVcnManagementService; diff --git a/packages/Vcn/service-b/src/com/android/server/VcnManagementService.java b/packages/Vcn/service-b/src/com/android/server/VcnManagementService.java index c9a99d729e91..8edd63dc341f 100644 --- a/packages/Vcn/service-b/src/com/android/server/VcnManagementService.java +++ b/packages/Vcn/service-b/src/com/android/server/VcnManagementService.java @@ -165,8 +165,7 @@ import java.util.concurrent.TimeUnit; * @hide */ // TODO(b/180451994): ensure all incoming + outgoing calls have a cleared calling identity -// TODO(b/374174952): Replace VANILLA_ICE_CREAM with BAKLAVA after Android B finalization -@TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@TargetApi(Build.VERSION_CODES.BAKLAVA) public class VcnManagementService extends IVcnManagementService.Stub { @NonNull private static final String TAG = VcnManagementService.class.getSimpleName(); @NonNull private static final String CONTEXT_ATTRIBUTION_TAG = "VCN"; diff --git a/packages/Vcn/service-b/src/com/android/server/vcn/TelephonySubscriptionTracker.java b/packages/Vcn/service-b/src/com/android/server/vcn/TelephonySubscriptionTracker.java index b04e25dff276..cedb2d16808f 100644 --- a/packages/Vcn/service-b/src/com/android/server/vcn/TelephonySubscriptionTracker.java +++ b/packages/Vcn/service-b/src/com/android/server/vcn/TelephonySubscriptionTracker.java @@ -79,8 +79,7 @@ import java.util.Set; * * @hide */ -// TODO(b/374174952): Replace VANILLA_ICE_CREAM with BAKLAVA after Android B finalization -@TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@TargetApi(Build.VERSION_CODES.BAKLAVA) public class TelephonySubscriptionTracker extends BroadcastReceiver { @NonNull private static final String TAG = TelephonySubscriptionTracker.class.getSimpleName(); private static final boolean LOG_DBG = false; // STOPSHIP if true diff --git a/packages/Vcn/service-b/src/com/android/server/vcn/Vcn.java b/packages/Vcn/service-b/src/com/android/server/vcn/Vcn.java index 97f86b1bff5b..0f8b2885cf4d 100644 --- a/packages/Vcn/service-b/src/com/android/server/vcn/Vcn.java +++ b/packages/Vcn/service-b/src/com/android/server/vcn/Vcn.java @@ -77,8 +77,7 @@ import java.util.Set; * * @hide */ -// TODO(b/374174952): Replace VANILLA_ICE_CREAM with BAKLAVA after Android B finalization -@TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@TargetApi(Build.VERSION_CODES.BAKLAVA) public class Vcn extends Handler { private static final String TAG = Vcn.class.getSimpleName(); diff --git a/packages/Vcn/service-b/src/com/android/server/vcn/VcnGatewayConnection.java b/packages/Vcn/service-b/src/com/android/server/vcn/VcnGatewayConnection.java index 300b80f942ef..da411174f95c 100644 --- a/packages/Vcn/service-b/src/com/android/server/vcn/VcnGatewayConnection.java +++ b/packages/Vcn/service-b/src/com/android/server/vcn/VcnGatewayConnection.java @@ -174,8 +174,7 @@ import java.util.function.Consumer; * * @hide */ -// TODO(b/374174952): Replace VANILLA_ICE_CREAM with BAKLAVA after Android B finalization -@TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@TargetApi(Build.VERSION_CODES.BAKLAVA) public class VcnGatewayConnection extends StateMachine { private static final String TAG = VcnGatewayConnection.class.getSimpleName(); diff --git a/packages/Vcn/service-b/src/com/android/server/vcn/VcnNetworkProvider.java b/packages/Vcn/service-b/src/com/android/server/vcn/VcnNetworkProvider.java index 38fcf09145d9..bc815eb27454 100644 --- a/packages/Vcn/service-b/src/com/android/server/vcn/VcnNetworkProvider.java +++ b/packages/Vcn/service-b/src/com/android/server/vcn/VcnNetworkProvider.java @@ -58,8 +58,7 @@ import java.util.concurrent.Executor; */ // TODO(b/388919146): Implement a more generic solution to prevent concurrent modifications on // mListeners and mRequests -// TODO(b/374174952): Replace VANILLA_ICE_CREAM with BAKLAVA after Android B finalization -@TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@TargetApi(Build.VERSION_CODES.BAKLAVA) public class VcnNetworkProvider extends NetworkProvider { private static final String TAG = VcnNetworkProvider.class.getSimpleName(); diff --git a/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java b/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java index c8c645f1276d..aff7068b6c4f 100644 --- a/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java +++ b/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java @@ -61,8 +61,7 @@ import java.util.concurrent.TimeUnit; * * <p>This class is flag gated by "network_metric_monitor" and "ipsec_tramsform_state" */ -// TODO(b/374174952) Replace VANILLA_ICE_CREAM with BAKLAVA after Android B finalization -@TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@TargetApi(Build.VERSION_CODES.BAKLAVA) public class IpSecPacketLossDetector extends NetworkMetricMonitor { private static final String TAG = IpSecPacketLossDetector.class.getSimpleName(); diff --git a/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/NetworkMetricMonitor.java b/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/NetworkMetricMonitor.java index 55829a5fe978..fc9c7ac8a335 100644 --- a/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/NetworkMetricMonitor.java +++ b/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/NetworkMetricMonitor.java @@ -44,8 +44,7 @@ import java.util.concurrent.Executor; * * <p>This class is flag gated by "network_metric_monitor" */ -// TODO(b/374174952): Replace VANILLA_ICE_CREAM with BAKLAVA after Android B finalization -@TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@TargetApi(Build.VERSION_CODES.BAKLAVA) public abstract class NetworkMetricMonitor implements AutoCloseable { private static final String TAG = NetworkMetricMonitor.class.getSimpleName(); diff --git a/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java b/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java index 705141f3f1b4..7cb3257193a5 100644 --- a/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java +++ b/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java @@ -52,8 +52,7 @@ import java.util.Map; import java.util.Set; /** @hide */ -// TODO(b/374174952): Replace VANILLA_ICE_CREAM with BAKLAVA after Android B finalization -@TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@TargetApi(Build.VERSION_CODES.BAKLAVA) class NetworkPriorityClassifier { @NonNull private static final String TAG = NetworkPriorityClassifier.class.getSimpleName(); /** diff --git a/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/UnderlyingNetworkController.java b/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/UnderlyingNetworkController.java index bc552e7e6afd..37ec0e8f40dc 100644 --- a/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/UnderlyingNetworkController.java +++ b/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/UnderlyingNetworkController.java @@ -75,8 +75,7 @@ import java.util.TreeSet; * * @hide */ -// TODO(b/374174952): Replace VANILLA_ICE_CREAM with BAKLAVA after Android B finalization -@TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@TargetApi(Build.VERSION_CODES.BAKLAVA) public class UnderlyingNetworkController { @NonNull private static final String TAG = UnderlyingNetworkController.class.getSimpleName(); diff --git a/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java b/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java index 776931bad73b..164b59f6f0cd 100644 --- a/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java +++ b/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java @@ -52,8 +52,7 @@ import java.util.concurrent.TimeUnit; * * @hide */ -// TODO(b/374174952): Replace VANILLA_ICE_CREAM with BAKLAVA after Android B finalization -@TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@TargetApi(Build.VERSION_CODES.BAKLAVA) public class UnderlyingNetworkEvaluator { private static final String TAG = UnderlyingNetworkEvaluator.class.getSimpleName(); diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp index 8e998426685b..65550f2b4273 100644 --- a/ravenwood/Android.bp +++ b/ravenwood/Android.bp @@ -175,9 +175,9 @@ java_library { } java_device_for_host { - name: "ravenwood-junit-impl-for-ravenizer", + name: "ravenwood-junit-for-ravenizer", libs: [ - "ravenwood-junit-impl", + "ravenwood-junit", ], visibility: [":__subpackages__"], } @@ -661,6 +661,9 @@ android_ravenwood_libgroup { // StatsD "framework-statsd.ravenwood", + // Graphics + "framework-graphics.ravenwood", + // Provide runtime versions of utils linked in below "junit", "truth", diff --git a/ravenwood/Framework.bp b/ravenwood/Framework.bp index 99fc31b258e9..71496b0d5766 100644 --- a/ravenwood/Framework.bp +++ b/ravenwood/Framework.bp @@ -399,3 +399,55 @@ java_genrule { "framework-statsd.ravenwood.jar", ], } + +////////////////////// +// framework-graphics +////////////////////// + +java_genrule { + name: "framework-graphics.ravenwood-base", + tools: ["hoststubgen"], + cmd: "$(location hoststubgen) " + + "@$(location :ravenwood-standard-options) " + + + "--debug-log $(location framework-graphics.log) " + + "--stats-file $(location framework-graphics_stats.csv) " + + "--supported-api-list-file $(location framework-graphics_apis.csv) " + + "--gen-keep-all-file $(location framework-graphics_keep_all.txt) " + + "--gen-input-dump-file $(location framework-graphics_dump.txt) " + + + "--out-impl-jar $(location ravenwood.jar) " + + "--in-jar $(location :framework-graphics.impl{.jar}) " + + + "--policy-override-file $(location :ravenwood-common-policies) ", + srcs: [ + ":framework-graphics.impl{.jar}", + + ":ravenwood-common-policies", + ":ravenwood-standard-options", + ], + out: [ + "ravenwood.jar", + + // Following files are created just as FYI. + "framework-graphics_keep_all.txt", + "framework-graphics_dump.txt", + + "framework-graphics.log", + "framework-graphics_stats.csv", + "framework-graphics_apis.csv", + ], + visibility: ["//visibility:private"], +} + +java_genrule { + name: "framework-graphics.ravenwood", + defaults: ["ravenwood-internal-only-visibility-genrule"], + cmd: "cp $(in) $(out)", + srcs: [ + ":framework-graphics.ravenwood-base{ravenwood.jar}", + ], + out: [ + "framework-graphics.ravenwood.jar", + ], +} diff --git a/ravenwood/scripts/pta-framework.sh b/ravenwood/scripts/pta-framework.sh index 224ab59e2e09..46c2c01c8ee8 100755 --- a/ravenwood/scripts/pta-framework.sh +++ b/ravenwood/scripts/pta-framework.sh @@ -79,6 +79,7 @@ run_pta() { $extra_args if ! [[ -f $OUT_SCRIPT ]] ; then + echo "No files need updating." # no operations generated. exit 0 fi @@ -88,4 +89,4 @@ run_pta() { return 0 } -run_pta "$extra_args"
\ No newline at end of file +run_pta "$extra_args" diff --git a/ravenwood/texts/ravenwood-framework-policies.txt b/ravenwood/texts/ravenwood-framework-policies.txt index 4033782c607e..fff9e6ad41d5 100644 --- a/ravenwood/texts/ravenwood-framework-policies.txt +++ b/ravenwood/texts/ravenwood-framework-policies.txt @@ -62,4 +62,4 @@ class android.text.ClipboardManager keep # no-pta # Just enough to allow ResourcesManager to run class android.hardware.display.DisplayManagerGlobal keep # no-pta - method getInstance ()Landroid/hardware/display/DisplayManagerGlobal; ignore + method getInstance ()Landroid/hardware/display/DisplayManagerGlobal; ignore # no-pta diff --git a/ravenwood/texts/ravenwood-standard-options.txt b/ravenwood/texts/ravenwood-standard-options.txt index 27223d8b72ff..91fd9283aff2 100644 --- a/ravenwood/texts/ravenwood-standard-options.txt +++ b/ravenwood/texts/ravenwood-standard-options.txt @@ -31,6 +31,9 @@ --remove-annotation android.ravenwood.annotation.RavenwoodRemove +--ignore-annotation + android.ravenwood.annotation.RavenwoodIgnore + --substitute-annotation android.ravenwood.annotation.RavenwoodReplace diff --git a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/Annotations.kt b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/Annotations.kt index 4a11259a8ef7..ef1cb5dfca89 100644 --- a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/Annotations.kt +++ b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/Annotations.kt @@ -45,7 +45,8 @@ class Annotations { "@android.ravenwood.annotation.RavenwoodRedirect" FilterPolicy.Throw -> "@android.ravenwood.annotation.RavenwoodThrow" - FilterPolicy.Ignore -> null // Ignore has no annotation. (because it's not very safe.) + FilterPolicy.Ignore -> + "@android.ravenwood.annotation.RavenwoodIgnore" FilterPolicy.Remove -> "@android.ravenwood.annotation.RavenwoodRemove" } diff --git a/ravenwood/tools/ravenizer/Android.bp b/ravenwood/tools/ravenizer/Android.bp index 2892d0778ec6..a52a04b44f2d 100644 --- a/ravenwood/tools/ravenizer/Android.bp +++ b/ravenwood/tools/ravenizer/Android.bp @@ -19,7 +19,7 @@ java_binary_host { "ow2-asm-tree", "ow2-asm-util", "junit", - "ravenwood-junit-impl-for-ravenizer", + "ravenwood-junit-for-ravenizer", ], visibility: ["//visibility:public"], } diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java index 75c629b77700..fda57d6bb986 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java @@ -45,6 +45,7 @@ import android.view.MotionEvent.PointerProperties; import android.view.accessibility.AccessibilityEvent; import com.android.server.LocalServices; +import com.android.server.accessibility.autoclick.AutoclickController; import com.android.server.accessibility.gestures.TouchExplorer; import com.android.server.accessibility.magnification.FullScreenMagnificationController; import com.android.server.accessibility.magnification.FullScreenMagnificationGestureHandler; diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 875b655fe3d2..8e0a7785c597 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -607,7 +607,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mLock, mContext, new MagnificationScaleProvider(mContext), - Executors.newSingleThreadExecutor() + Executors.newSingleThreadExecutor(), + mContext.getMainLooper() ); mMagnificationProcessor = new MagnificationProcessor(mMagnificationController); mCaptioningManagerImpl = new CaptioningManagerImpl(mContext); @@ -5084,39 +5085,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub final List<String> permittedServices = dpm.getPermittedAccessibilityServices(userId); // permittedServices null means all accessibility services are allowed. - boolean allowed = permittedServices == null || permittedServices.contains(packageName); - if (allowed) { - if (android.permission.flags.Flags.enhancedConfirmationModeApisEnabled() - && android.security.Flags.extendEcmToAllSettings()) { - try { - final EnhancedConfirmationManager userContextEcm = - mContext.createContextAsUser(UserHandle.of(userId), /* flags = */ 0) - .getSystemService(EnhancedConfirmationManager.class); - if (userContextEcm != null) { - return !userContextEcm.isRestricted(packageName, - AppOpsManager.OPSTR_BIND_ACCESSIBILITY_SERVICE); - } - return false; - } catch (PackageManager.NameNotFoundException e) { - Log.e(LOG_TAG, "Exception when retrieving package:" + packageName, e); - return false; - } - } else { - try { - final int mode = mContext.getSystemService(AppOpsManager.class) - .noteOpNoThrow(AppOpsManager.OP_ACCESS_RESTRICTED_SETTINGS, - uid, packageName); - final boolean ecmEnabled = mContext.getResources().getBoolean( - com.android.internal.R.bool.config_enhancedConfirmationModeEnabled); - return !ecmEnabled || mode == AppOpsManager.MODE_ALLOWED - || mode == AppOpsManager.MODE_DEFAULT; - } catch (Exception e) { - // Fallback in case if app ops is not available in testing. - return false; - } - } - } - return false; + return permittedServices == null || permittedServices.contains(packageName); } finally { Binder.restoreCallingIdentity(identity); } diff --git a/services/accessibility/java/com/android/server/accessibility/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java index 8b758d29a2ac..1bc9c783df76 100644 --- a/services/accessibility/java/com/android/server/accessibility/AutoclickController.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java @@ -14,11 +14,14 @@ * limitations under the License. */ -package com.android.server.accessibility; +package com.android.server.accessibility.autoclick; -import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; +import static android.view.MotionEvent.BUTTON_PRIMARY; +import static android.view.accessibility.AccessibilityManager.AUTOCLICK_CURSOR_AREA_SIZE_DEFAULT; +import static android.view.accessibility.AccessibilityManager.AUTOCLICK_DELAY_DEFAULT; +import static android.view.accessibility.AccessibilityManager.AUTOCLICK_IGNORE_MINOR_CURSOR_MOVEMENT_DEFAULT; -import static com.android.server.accessibility.AutoclickIndicatorView.SHOW_INDICATOR_DELAY_TIME; +import static com.android.server.accessibility.autoclick.AutoclickIndicatorView.SHOW_INDICATOR_DELAY_TIME; import android.accessibilityservice.AccessibilityTrace; import android.annotation.NonNull; @@ -26,7 +29,6 @@ import android.annotation.Nullable; import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; -import android.graphics.PixelFormat; import android.net.Uri; import android.os.Handler; import android.os.SystemClock; @@ -37,10 +39,14 @@ import android.view.MotionEvent; import android.view.MotionEvent.PointerCoords; import android.view.MotionEvent.PointerProperties; import android.view.WindowManager; -import android.view.accessibility.AccessibilityManager; import androidx.annotation.VisibleForTesting; +import com.android.internal.accessibility.util.AccessibilityUtils; +import com.android.server.accessibility.AccessibilityTraceManager; +import com.android.server.accessibility.BaseEventStreamTransformation; +import com.android.server.accessibility.Flags; + /** * Implements "Automatically click on mouse stop" feature. * @@ -96,8 +102,7 @@ public class AutoclickController extends BaseEventStreamTransformation { initiateAutoclickIndicator(handler); } - mClickScheduler = - new ClickScheduler(handler, AccessibilityManager.AUTOCLICK_DELAY_DEFAULT); + mClickScheduler = new ClickScheduler(handler, AUTOCLICK_DELAY_DEFAULT); mAutoclickSettingsObserver = new AutoclickSettingsObserver(mUserId, handler); mAutoclickSettingsObserver.start( mContext.getContentResolver(), @@ -117,21 +122,8 @@ public class AutoclickController extends BaseEventStreamTransformation { mAutoclickIndicatorScheduler = new AutoclickIndicatorScheduler(handler); mAutoclickIndicatorView = new AutoclickIndicatorView(mContext); - final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(); - layoutParams.type = WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY; - layoutParams.flags = - WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE - | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; - layoutParams.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; - layoutParams.setFitInsetsTypes(0); - layoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; - layoutParams.format = PixelFormat.TRANSLUCENT; - layoutParams.setTitle(AutoclickIndicatorView.class.getSimpleName()); - layoutParams.inputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL; - mWindowManager = mContext.getSystemService(WindowManager.class); - mWindowManager.addView(mAutoclickIndicatorView, layoutParams); + mWindowManager.addView(mAutoclickIndicatorView, mAutoclickIndicatorView.getLayoutParams()); } @Override @@ -209,6 +201,11 @@ public class AutoclickController extends BaseEventStreamTransformation { private final Uri mAutoclickCursorAreaSizeSettingUri = Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_AUTOCLICK_CURSOR_AREA_SIZE); + /** URI used to identify ignore minor cursor movement setting with content resolver. */ + private final Uri mAutoclickIgnoreMinorCursorMovementSettingUri = + Settings.Secure.getUriFor( + Settings.Secure.ACCESSIBILITY_AUTOCLICK_IGNORE_MINOR_CURSOR_MOVEMENT); + private ContentResolver mContentResolver; private ClickScheduler mClickScheduler; private AutoclickIndicatorScheduler mAutoclickIndicatorScheduler; @@ -247,18 +244,32 @@ public class AutoclickController extends BaseEventStreamTransformation { mContentResolver = contentResolver; mClickScheduler = clickScheduler; mAutoclickIndicatorScheduler = autoclickIndicatorScheduler; - mContentResolver.registerContentObserver(mAutoclickDelaySettingUri, false, this, + mContentResolver.registerContentObserver( + mAutoclickDelaySettingUri, + /* notifyForDescendants= */ false, + /* observer= */ this, mUserId); // Initialize mClickScheduler's initial delay value. - onChange(true, mAutoclickDelaySettingUri); + onChange(/* selfChange= */ true, mAutoclickDelaySettingUri); if (Flags.enableAutoclickIndicator()) { // Register observer to listen to cursor area size setting change. mContentResolver.registerContentObserver( - mAutoclickCursorAreaSizeSettingUri, false, this, mUserId); + mAutoclickCursorAreaSizeSettingUri, + /* notifyForDescendants= */ false, + /* observer= */ this, + mUserId); // Initialize mAutoclickIndicatorView's initial size. - onChange(true, mAutoclickCursorAreaSizeSettingUri); + onChange(/* selfChange= */ true, mAutoclickCursorAreaSizeSettingUri); + + // Register observer to listen to ignore minor cursor movement setting change. + mContentResolver.registerContentObserver( + mAutoclickIgnoreMinorCursorMovementSettingUri, + /* notifyForDescendants= */ false, + /* observer= */ this, + mUserId); + onChange(/* selfChange= */ true, mAutoclickIgnoreMinorCursorMovementSettingUri); } } @@ -279,21 +290,41 @@ public class AutoclickController extends BaseEventStreamTransformation { @Override public void onChange(boolean selfChange, Uri uri) { if (mAutoclickDelaySettingUri.equals(uri)) { - int delay = Settings.Secure.getIntForUser( - mContentResolver, Settings.Secure.ACCESSIBILITY_AUTOCLICK_DELAY, - AccessibilityManager.AUTOCLICK_DELAY_DEFAULT, mUserId); - mClickScheduler.updateDelay(delay); - } - if (Flags.enableAutoclickIndicator() - && mAutoclickCursorAreaSizeSettingUri.equals(uri)) { - int size = + int delay = Settings.Secure.getIntForUser( mContentResolver, - Settings.Secure.ACCESSIBILITY_AUTOCLICK_CURSOR_AREA_SIZE, - AccessibilityManager.AUTOCLICK_CURSOR_AREA_SIZE_DEFAULT, + Settings.Secure.ACCESSIBILITY_AUTOCLICK_DELAY, + AUTOCLICK_DELAY_DEFAULT, mUserId); - if (mAutoclickIndicatorScheduler != null) { - mAutoclickIndicatorScheduler.updateCursorAreaSize(size); + mClickScheduler.updateDelay(delay); + } + + if (Flags.enableAutoclickIndicator()) { + if (mAutoclickCursorAreaSizeSettingUri.equals(uri)) { + int size = + Settings.Secure.getIntForUser( + mContentResolver, + Settings.Secure.ACCESSIBILITY_AUTOCLICK_CURSOR_AREA_SIZE, + AUTOCLICK_CURSOR_AREA_SIZE_DEFAULT, + mUserId); + if (mAutoclickIndicatorScheduler != null) { + mAutoclickIndicatorScheduler.updateCursorAreaSize(size); + } + mClickScheduler.updateMovementSlope(size); + } + + if (mAutoclickIgnoreMinorCursorMovementSettingUri.equals(uri)) { + boolean ignoreMinorCursorMovement = + Settings.Secure.getIntForUser( + mContentResolver, + Settings.Secure + .ACCESSIBILITY_AUTOCLICK_IGNORE_MINOR_CURSOR_MOVEMENT, + AUTOCLICK_IGNORE_MINOR_CURSOR_MOVEMENT_DEFAULT + ? AccessibilityUtils.State.ON + : AccessibilityUtils.State.OFF, + mUserId) + == AccessibilityUtils.State.ON; + mClickScheduler.setIgnoreMinorCursorMovement(ignoreMinorCursorMovement); } } } @@ -365,11 +396,16 @@ public class AutoclickController extends BaseEventStreamTransformation { @VisibleForTesting final class ClickScheduler implements Runnable { /** - * Minimal distance pointer has to move relative to anchor in order for movement not to be - * discarded as noise. Anchor is the position of the last MOVE event that was not considered - * noise. + * Default minimal distance pointer has to move relative to anchor in order for movement not + * to be discarded as noise. Anchor is the position of the last MOVE event that was not + * considered noise. */ - private static final double MOVEMENT_SLOPE = 20f; + private static final double DEFAULT_MOVEMENT_SLOPE = 20f; + + private double mMovementSlope = DEFAULT_MOVEMENT_SLOPE; + + /** Whether the minor cursor movement should be ignored. */ + private boolean mIgnoreMinorCursorMovement = AUTOCLICK_IGNORE_MINOR_CURSOR_MOVEMENT_DEFAULT; /** Whether there is pending click. */ private boolean mActive; @@ -553,7 +589,19 @@ public class AutoclickController extends BaseEventStreamTransformation { float deltaX = mAnchorCoords.x - event.getX(pointerIndex); float deltaY = mAnchorCoords.y - event.getY(pointerIndex); double delta = Math.hypot(deltaX, deltaY); - return delta > MOVEMENT_SLOPE; + double slope = + ((Flags.enableAutoclickIndicator() && mIgnoreMinorCursorMovement) + ? mMovementSlope + : DEFAULT_MOVEMENT_SLOPE); + return delta > slope; + } + + public void setIgnoreMinorCursorMovement(boolean ignoreMinorCursorMovement) { + mIgnoreMinorCursorMovement = ignoreMinorCursorMovement; + } + + private void updateMovementSlope(double slope) { + mMovementSlope = slope; } /** @@ -581,18 +629,30 @@ public class AutoclickController extends BaseEventStreamTransformation { final long now = SystemClock.uptimeMillis(); - MotionEvent downEvent = MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN, 1, - mTempPointerProperties, mTempPointerCoords, mMetaState, - MotionEvent.BUTTON_PRIMARY, 1.0f, 1.0f, mLastMotionEvent.getDeviceId(), 0, - mLastMotionEvent.getSource(), mLastMotionEvent.getFlags()); + MotionEvent downEvent = + MotionEvent.obtain( + /* downTime= */ now, + /* eventTime= */ now, + MotionEvent.ACTION_DOWN, + /* pointerCount= */ 1, + mTempPointerProperties, + mTempPointerCoords, + mMetaState, + BUTTON_PRIMARY, + /* xPrecision= */ 1.0f, + /* yPrecision= */ 1.0f, + mLastMotionEvent.getDeviceId(), + /* edgeFlags= */ 0, + mLastMotionEvent.getSource(), + mLastMotionEvent.getFlags()); MotionEvent pressEvent = MotionEvent.obtain(downEvent); pressEvent.setAction(MotionEvent.ACTION_BUTTON_PRESS); - pressEvent.setActionButton(MotionEvent.BUTTON_PRIMARY); + pressEvent.setActionButton(BUTTON_PRIMARY); MotionEvent releaseEvent = MotionEvent.obtain(downEvent); releaseEvent.setAction(MotionEvent.ACTION_BUTTON_RELEASE); - releaseEvent.setActionButton(MotionEvent.BUTTON_PRIMARY); + releaseEvent.setActionButton(BUTTON_PRIMARY); releaseEvent.setButtonState(0); MotionEvent upEvent = MotionEvent.obtain(downEvent); diff --git a/services/accessibility/java/com/android/server/accessibility/AutoclickIndicatorView.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickIndicatorView.java index f87dcdb200bb..557e1b2afcd5 100644 --- a/services/accessibility/java/com/android/server/accessibility/AutoclickIndicatorView.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickIndicatorView.java @@ -14,17 +14,21 @@ * limitations under the License. */ -package com.android.server.accessibility; +package com.android.server.accessibility.autoclick; +import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; import static android.view.accessibility.AccessibilityManager.AUTOCLICK_CURSOR_AREA_SIZE_DEFAULT; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; +import android.graphics.PixelFormat; import android.graphics.RectF; import android.util.DisplayMetrics; import android.view.View; +import android.view.WindowInsets; +import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; import android.view.animation.LinearInterpolator; @@ -81,6 +85,26 @@ public class AutoclickIndicatorView extends View { mRingRect = new RectF(); } + /** + * Retrieves the layout params for AutoclickIndicatorView, used when it's added to the Window + * Manager. + */ + public final WindowManager.LayoutParams getLayoutParams() { + final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(); + layoutParams.type = WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY; + layoutParams.flags = + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; + layoutParams.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; + layoutParams.setFitInsetsTypes(WindowInsets.Type.statusBars()); + layoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + layoutParams.format = PixelFormat.TRANSLUCENT; + layoutParams.setTitle(AutoclickIndicatorView.class.getSimpleName()); + layoutParams.inputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL; + return layoutParams; + } + @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java index 75ec8ea88ace..486f1f449691 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java @@ -36,6 +36,8 @@ import android.graphics.PointF; import android.graphics.Rect; import android.graphics.Region; import android.hardware.display.DisplayManager; +import android.os.Handler; +import android.os.Looper; import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings; @@ -53,6 +55,7 @@ import android.view.accessibility.MagnificationAnimationCallback; import com.android.internal.accessibility.util.AccessibilityStatsLogUtils; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.function.pooled.PooledLambda; import com.android.server.LocalServices; import com.android.server.accessibility.AccessibilityManagerService; import com.android.server.wm.WindowManagerInternal; @@ -111,6 +114,20 @@ public class MagnificationController implements MagnificationConnectionManager.C private final Executor mBackgroundExecutor; + private final Handler mHandler; + private @PanDirection int mActivePanDirection = PAN_DIRECTION_DOWN; + private int mActivePanDisplay = Display.INVALID_DISPLAY; + private boolean mRepeatKeysEnabled = true; + + private @ZoomDirection int mActiveZoomDirection = ZOOM_DIRECTION_IN; + private int mActiveZoomDisplay = Display.INVALID_DISPLAY; + + // TODO(b/355499907): Get initial repeat interval from repeat keys settings. + @VisibleForTesting + public static final int INITIAL_KEYBOARD_REPEAT_INTERVAL_MS = 500; + @VisibleForTesting + public static final int KEYBOARD_REPEAT_INTERVAL_MS = 60; + @GuardedBy("mLock") private final SparseIntArray mCurrentMagnificationModeArray = new SparseIntArray(); @GuardedBy("mLock") @@ -287,12 +304,13 @@ public class MagnificationController implements MagnificationConnectionManager.C public MagnificationController(AccessibilityManagerService ams, Object lock, Context context, MagnificationScaleProvider scaleProvider, - Executor backgroundExecutor) { + Executor backgroundExecutor, Looper looper) { mAms = ams; mLock = lock; mContext = context; mScaleProvider = scaleProvider; mBackgroundExecutor = backgroundExecutor; + mHandler = new Handler(looper); LocalServices.getService(WindowManagerInternal.class) .getAccessibilityController().setUiChangesForAccessibilityCallbacks(this); mSupportWindowMagnification = context.getPackageManager().hasSystemFeature( @@ -303,14 +321,20 @@ public class MagnificationController implements MagnificationConnectionManager.C mAlwaysOnMagnificationFeatureFlag = new AlwaysOnMagnificationFeatureFlag(context); mAlwaysOnMagnificationFeatureFlag.addOnChangedListener( mBackgroundExecutor, mAms::updateAlwaysOnMagnification); + + // TODO(b/355499907): Add an observer for repeat keys enabled changes, + // rather than initializing once at startup. + mRepeatKeysEnabled = Settings.Secure.getIntForUser( + mContext.getContentResolver(), Settings.Secure.KEY_REPEAT_ENABLED, 1, + UserHandle.USER_CURRENT) != 0; } @VisibleForTesting public MagnificationController(AccessibilityManagerService ams, Object lock, Context context, FullScreenMagnificationController fullScreenMagnificationController, MagnificationConnectionManager magnificationConnectionManager, - MagnificationScaleProvider scaleProvider, Executor backgroundExecutor) { - this(ams, lock, context, scaleProvider, backgroundExecutor); + MagnificationScaleProvider scaleProvider, Executor backgroundExecutor, Looper looper) { + this(ams, lock, context, scaleProvider, backgroundExecutor, looper); mFullScreenMagnificationController = fullScreenMagnificationController; mMagnificationConnectionManager = magnificationConnectionManager; } @@ -354,27 +378,60 @@ public class MagnificationController implements MagnificationConnectionManager.C // pan diagonally) by decreasing diagonal movement by sqrt(2) to make it appear the same // speed as non-diagonal movement. panMagnificationByStep(displayId, direction); + mActivePanDirection = direction; + mActivePanDisplay = displayId; + if (mRepeatKeysEnabled) { + mHandler.sendMessageDelayed( + PooledLambda.obtainMessage(MagnificationController::maybeContinuePan, this), + INITIAL_KEYBOARD_REPEAT_INTERVAL_MS); + } } @Override public void onPanMagnificationStop(int displayId, @MagnificationController.PanDirection int direction) { - // TODO(b/388847283): Handle held key gestures, which can be used - // for continuous scaling and panning, until they are released. - + if (direction == mActivePanDirection) { + mActivePanDisplay = Display.INVALID_DISPLAY; + } } @Override public void onScaleMagnificationStart(int displayId, @MagnificationController.ZoomDirection int direction) { scaleMagnificationByStep(displayId, direction); + mActiveZoomDirection = direction; + mActiveZoomDisplay = displayId; + if (mRepeatKeysEnabled) { + mHandler.sendMessageDelayed( + PooledLambda.obtainMessage(MagnificationController::maybeContinueZoom, this), + INITIAL_KEYBOARD_REPEAT_INTERVAL_MS); + } } @Override public void onScaleMagnificationStop(int displayId, @MagnificationController.ZoomDirection int direction) { - // TODO(b/388847283): Handle held key gestures, which can be used - // for continuous scaling and panning, until they are released. + if (direction == mActiveZoomDirection) { + mActiveZoomDisplay = Display.INVALID_DISPLAY; + } + } + + private void maybeContinuePan() { + if (mActivePanDisplay != Display.INVALID_DISPLAY) { + panMagnificationByStep(mActivePanDisplay, mActivePanDirection); + mHandler.sendMessageDelayed( + PooledLambda.obtainMessage(MagnificationController::maybeContinuePan, this), + KEYBOARD_REPEAT_INTERVAL_MS); + } + } + + private void maybeContinueZoom() { + if (mActiveZoomDisplay != Display.INVALID_DISPLAY) { + scaleMagnificationByStep(mActiveZoomDisplay, mActiveZoomDirection); + mHandler.sendMessageDelayed( + PooledLambda.obtainMessage(MagnificationController::maybeContinueZoom, this), + KEYBOARD_REPEAT_INTERVAL_MS); + } } private void handleUserInteractionChanged(int displayId, int mode) { diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index a37b2b926c9c..02a8f6218468 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -612,7 +612,7 @@ public class CompanionDeviceManagerService extends SystemService { @Override public void enablePermissionsSync(int associationId) { - if (UserHandle.getAppId(Binder.getCallingUid()) == SYSTEM_UID) { + if (UserHandle.getAppId(Binder.getCallingUid()) != SYSTEM_UID) { throw new SecurityException("Caller must be system UID"); } mSystemDataTransferProcessor.enablePermissionsSync(associationId); @@ -620,7 +620,7 @@ public class CompanionDeviceManagerService extends SystemService { @Override public void disablePermissionsSync(int associationId) { - if (UserHandle.getAppId(Binder.getCallingUid()) == SYSTEM_UID) { + if (UserHandle.getAppId(Binder.getCallingUid()) != SYSTEM_UID) { throw new SecurityException("Caller must be system UID"); } mSystemDataTransferProcessor.disablePermissionsSync(associationId); @@ -628,7 +628,7 @@ public class CompanionDeviceManagerService extends SystemService { @Override public PermissionSyncRequest getPermissionSyncRequest(int associationId) { - if (UserHandle.getAppId(Binder.getCallingUid()) == SYSTEM_UID) { + if (UserHandle.getAppId(Binder.getCallingUid()) != SYSTEM_UID) { throw new SecurityException("Caller must be system UID"); } return mSystemDataTransferProcessor.getPermissionSyncRequest(associationId); @@ -704,7 +704,7 @@ public class CompanionDeviceManagerService extends SystemService { @Override public byte[] getBackupPayload(int userId) { - if (UserHandle.getAppId(Binder.getCallingUid()) == SYSTEM_UID) { + if (UserHandle.getAppId(Binder.getCallingUid()) != SYSTEM_UID) { throw new SecurityException("Caller must be system"); } return mBackupRestoreProcessor.getBackupPayload(userId); @@ -712,7 +712,7 @@ public class CompanionDeviceManagerService extends SystemService { @Override public void applyRestoredPayload(byte[] payload, int userId) { - if (UserHandle.getAppId(Binder.getCallingUid()) == SYSTEM_UID) { + if (UserHandle.getAppId(Binder.getCallingUid()) != SYSTEM_UID) { throw new SecurityException("Caller must be system"); } mBackupRestoreProcessor.applyRestoredPayload(payload, userId); diff --git a/services/core/java/com/android/server/MasterClearReceiver.java b/services/core/java/com/android/server/MasterClearReceiver.java index 2b30c013235c..6f2ecd82a79c 100644 --- a/services/core/java/com/android/server/MasterClearReceiver.java +++ b/services/core/java/com/android/server/MasterClearReceiver.java @@ -130,7 +130,7 @@ public class MasterClearReceiver extends BroadcastReceiver { if (mWipeExternalStorage) { // thr will be started at the end of this task. Slog.i(TAG, "Wiping external storage on async task"); - new WipeDataTask(context, thr).execute(); + new WipeDataTask(context, thr).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } else { Slog.i(TAG, "NOT wiping external storage; starting thread " + thr.getName()); thr.start(); diff --git a/services/core/java/com/android/server/am/BroadcastController.java b/services/core/java/com/android/server/am/BroadcastController.java index bfacfbba4e22..bec5db79ff9d 100644 --- a/services/core/java/com/android/server/am/BroadcastController.java +++ b/services/core/java/com/android/server/am/BroadcastController.java @@ -317,7 +317,7 @@ class BroadcastController { Slog.w(TAG, "registerReceiverWithFeature: no app for " + caller); return null; } - if (callerApp.info.uid != SYSTEM_UID + if (!UserHandle.isCore(callerApp.info.uid) && !callerApp.getPkgList().containsKey(callerPackage)) { throw new SecurityException("Given caller package " + callerPackage + " is not running in process " + callerApp); diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java index 0b7890167c08..2c0366e6a6db 100644 --- a/services/core/java/com/android/server/am/PendingIntentRecord.java +++ b/services/core/java/com/android/server/am/PendingIntentRecord.java @@ -24,11 +24,14 @@ import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_COMPAT; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE; +import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED; +import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED; import static android.os.Process.ROOT_UID; import static android.os.Process.SYSTEM_UID; import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM; import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME; +import static com.android.window.flags.Flags.balClearAllowlistDuration; import android.annotation.IntDef; import android.annotation.Nullable; @@ -327,10 +330,11 @@ public final class PendingIntentRecord extends IIntentSender.Stub { mAllowBgActivityStartsForActivitySender.remove(token); mAllowBgActivityStartsForBroadcastSender.remove(token); mAllowBgActivityStartsForServiceSender.remove(token); - if (mAllowlistDuration != null) { - mAllowlistDuration.remove(token); - if (mAllowlistDuration.isEmpty()) { - mAllowlistDuration = null; + if (mAllowlistDuration != null && balClearAllowlistDuration()) { + TempAllowListDuration duration = mAllowlistDuration.get(token); + if (duration != null + && duration.type == TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED) { + duration.type = TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED; } } } diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index 8e09e3b8e112..833599810210 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -604,7 +604,7 @@ public class AppOpsService extends IAppOpsService.Stub { } } - /** Returned from {@link #verifyAndGetBypass(int, String, String, String, boolean)}. */ + /** Returned from {@link #verifyAndGetBypass(int, String, String, int, String, boolean)}. */ private static final class PackageVerificationResult { final RestrictionBypass bypass; @@ -3087,10 +3087,10 @@ public class AppOpsService extends IAppOpsService.Stub { public int checkPackage(int uid, String packageName) { Objects.requireNonNull(packageName); try { - verifyAndGetBypass(uid, packageName, null, null, true); + verifyAndGetBypass(uid, packageName, null, Process.INVALID_UID, null, true); // When the caller is the system, it's possible that the packageName is the special // one (e.g., "root") which isn't actually existed. - if (resolveUid(packageName) == uid + if (resolveNonAppUid(packageName) == uid || (isPackageExisted(packageName) && !filterAppAccessUnlocked(packageName, UserHandle.getUserId(uid)))) { return AppOpsManager.MODE_ALLOWED; @@ -3306,7 +3306,7 @@ public class AppOpsService extends IAppOpsService.Stub { boolean shouldCollectMessage, int notedCount) { PackageVerificationResult pvr; try { - pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyPackageName); + pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyUid, proxyPackageName); if (!pvr.isAttributionTagValid) { attributionTag = null; } @@ -3930,7 +3930,7 @@ public class AppOpsService extends IAppOpsService.Stub { // Test if the proxied operation will succeed before starting the proxy operation final SyncNotedAppOp testProxiedOp = startOperationDryRun(code, proxiedUid, resolvedProxiedPackageName, proxiedAttributionTag, - proxiedVirtualDeviceId, resolvedProxyPackageName, proxiedFlags, + proxiedVirtualDeviceId, proxyUid, resolvedProxyPackageName, proxiedFlags, startIfModeDefault); if (!shouldStartForMode(testProxiedOp.getOpMode(), startIfModeDefault)) { @@ -3970,7 +3970,7 @@ public class AppOpsService extends IAppOpsService.Stub { int attributionChainId) { PackageVerificationResult pvr; try { - pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyPackageName); + pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyUid, proxyPackageName); if (!pvr.isAttributionTagValid) { attributionTag = null; } @@ -4097,11 +4097,11 @@ public class AppOpsService extends IAppOpsService.Stub { */ private SyncNotedAppOp startOperationDryRun(int code, int uid, @NonNull String packageName, @Nullable String attributionTag, int virtualDeviceId, - String proxyPackageName, @OpFlags int flags, + int proxyUid, String proxyPackageName, @OpFlags int flags, boolean startIfModeDefault) { PackageVerificationResult pvr; try { - pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyPackageName); + pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyUid, proxyPackageName); if (!pvr.isAttributionTagValid) { attributionTag = null; } @@ -4656,13 +4656,17 @@ public class AppOpsService extends IAppOpsService.Stub { private boolean isSpecialPackage(int callingUid, @Nullable String packageName) { final String resolvedPackage = AppOpsManager.resolvePackageName(callingUid, packageName); return callingUid == Process.SYSTEM_UID - || resolveUid(resolvedPackage) != Process.INVALID_UID; + || resolveNonAppUid(resolvedPackage) != Process.INVALID_UID; } private boolean isCallerAndAttributionTrusted(@NonNull AttributionSource attributionSource) { if (attributionSource.getUid() != Binder.getCallingUid() && attributionSource.isTrusted(mContext)) { - return true; + // if there is a next attribution source, it must be trusted, as well. + if (attributionSource.getNext() == null + || attributionSource.getNext().isTrusted(mContext)) { + return true; + } } return mContext.checkPermission(android.Manifest.permission.UPDATE_APP_OPS_STATS, Binder.getCallingPid(), Binder.getCallingUid(), null) @@ -4757,19 +4761,20 @@ public class AppOpsService extends IAppOpsService.Stub { } /** - * @see #verifyAndGetBypass(int, String, String, String, boolean) + * @see #verifyAndGetBypass(int, String, String, int, String, boolean) */ private @NonNull PackageVerificationResult verifyAndGetBypass(int uid, String packageName, @Nullable String attributionTag) { - return verifyAndGetBypass(uid, packageName, attributionTag, null); + return verifyAndGetBypass(uid, packageName, attributionTag, Process.INVALID_UID, null); } /** - * @see #verifyAndGetBypass(int, String, String, String, boolean) + * @see #verifyAndGetBypass(int, String, String, int, String, boolean) */ private @NonNull PackageVerificationResult verifyAndGetBypass(int uid, String packageName, - @Nullable String attributionTag, @Nullable String proxyPackageName) { - return verifyAndGetBypass(uid, packageName, attributionTag, proxyPackageName, false); + @Nullable String attributionTag, int proxyUid, @Nullable String proxyPackageName) { + return verifyAndGetBypass(uid, packageName, attributionTag, proxyUid, proxyPackageName, + false); } /** @@ -4780,14 +4785,15 @@ public class AppOpsService extends IAppOpsService.Stub { * @param uid The uid the package belongs to * @param packageName The package the might belong to the uid * @param attributionTag attribution tag or {@code null} if no need to verify - * @param proxyPackageName The proxy package, from which the attribution tag is to be pulled + * @param proxyUid The proxy uid, from which the attribution tag is to be pulled + * @param proxyPackageName The proxy package, from which the attribution tag may be pulled * @param suppressErrorLogs Whether to print to logcat about nonmatching parameters * * @return PackageVerificationResult containing {@link RestrictionBypass} and whether the * attribution tag is valid */ private @NonNull PackageVerificationResult verifyAndGetBypass(int uid, String packageName, - @Nullable String attributionTag, @Nullable String proxyPackageName, + @Nullable String attributionTag, int proxyUid, @Nullable String proxyPackageName, boolean suppressErrorLogs) { if (uid == Process.ROOT_UID) { // For backwards compatibility, don't check package name for root UID. @@ -4831,34 +4837,47 @@ public class AppOpsService extends IAppOpsService.Stub { int callingUid = Binder.getCallingUid(); - // Allow any attribution tag for resolvable uids - int pkgUid; + // Allow any attribution tag for resolvable, non-app uids + int nonAppUid; if (Objects.equals(packageName, "com.android.shell")) { // Special case for the shell which is a package but should be able // to bypass app attribution tag restrictions. - pkgUid = Process.SHELL_UID; + nonAppUid = Process.SHELL_UID; } else { - pkgUid = resolveUid(packageName); + nonAppUid = resolveNonAppUid(packageName); } - if (pkgUid != Process.INVALID_UID) { - if (pkgUid != UserHandle.getAppId(uid)) { + if (nonAppUid != Process.INVALID_UID) { + if (nonAppUid != UserHandle.getAppId(uid)) { if (!suppressErrorLogs) { Slog.e(TAG, "Bad call made by uid " + callingUid + ". " - + "Package \"" + packageName + "\" does not belong to uid " + uid - + "."); + + "Package \"" + packageName + "\" does not belong to uid " + uid + + "."); + } + String otherUidMessage = + DEBUG ? " but it is really " + nonAppUid : " but it is not"; + throw new SecurityException("Specified package \"" + packageName + + "\" under uid " + UserHandle.getAppId(uid) + otherUidMessage); + } + // We only allow bypassing the attribution tag verification if the proxy is a + // system app (or is null), in order to prevent abusive apps clogging the appops + // system with unlimited attribution tags via proxy calls. + boolean proxyIsSystemAppOrNull = true; + if (proxyPackageName != null) { + int proxyAppId = UserHandle.getAppId(proxyUid); + if (proxyAppId >= Process.FIRST_APPLICATION_UID) { + proxyIsSystemAppOrNull = + mPackageManagerInternal.isSystemPackage(proxyPackageName); } - String otherUidMessage = DEBUG ? " but it is really " + pkgUid : " but it is not"; - throw new SecurityException("Specified package \"" + packageName + "\" under uid " - + UserHandle.getAppId(uid) + otherUidMessage); } return new PackageVerificationResult(RestrictionBypass.UNRESTRICTED, - /* isAttributionTagValid */ true); + /* isAttributionTagValid */ proxyIsSystemAppOrNull); } int userId = UserHandle.getUserId(uid); RestrictionBypass bypass = null; boolean isAttributionTagValid = false; + int pkgUid = nonAppUid; final long ident = Binder.clearCallingIdentity(); try { PackageManagerInternal pmInt = LocalServices.getService(PackageManagerInternal.class); @@ -5649,7 +5668,7 @@ public class AppOpsService extends IAppOpsService.Stub { if (nonpackageUid != -1) { packageName = null; } else { - packageUid = resolveUid(packageName); + packageUid = resolveNonAppUid(packageName); if (packageUid < 0) { packageUid = AppGlobals.getPackageManager().getPackageUid(packageName, PackageManager.MATCH_UNINSTALLED_PACKAGES, userId); @@ -6749,7 +6768,13 @@ public class AppOpsService extends IAppOpsService.Stub { if (restricted && attrOp.isRunning()) { attrOp.pause(); } else if (attrOp.isPaused()) { - attrOp.resume(); + RestrictionBypass bypass = verifyAndGetBypass(uid, ops.packageName, attrOp.tag) + .bypass; + if (!isOpRestrictedLocked(uid, code, ops.packageName, attrOp.tag, + Context.DEVICE_ID_DEFAULT, bypass, false)) { + // Only resume if there are no other restrictions remaining on this op + attrOp.resume(); + } } } } @@ -7198,7 +7223,7 @@ public class AppOpsService extends IAppOpsService.Stub { } } - private static int resolveUid(String packageName) { + private static int resolveNonAppUid(String packageName) { if (packageName == null) { return Process.INVALID_UID; } diff --git a/services/core/java/com/android/server/audio/AudioManagerShellCommand.java b/services/core/java/com/android/server/audio/AudioManagerShellCommand.java index 030ce12f5063..fece7a899f0a 100644 --- a/services/core/java/com/android/server/audio/AudioManagerShellCommand.java +++ b/services/core/java/com/android/server/audio/AudioManagerShellCommand.java @@ -65,6 +65,12 @@ class AudioManagerShellCommand extends ShellCommand { return setRingerMode(); case "set-volume": return setVolume(); + case "get-min-volume": + return getMinVolume(); + case "get-max-volume": + return getMaxVolume(); + case "get-stream-volume": + return getStreamVolume(); case "set-device-volume": return setDeviceVolume(); case "adj-mute": @@ -106,6 +112,12 @@ class AudioManagerShellCommand extends ShellCommand { pw.println(" Sets the Ringer mode to one of NORMAL|SILENT|VIBRATE"); pw.println(" set-volume STREAM_TYPE VOLUME_INDEX"); pw.println(" Sets the volume for STREAM_TYPE to VOLUME_INDEX"); + pw.println(" get-min-volume STREAM_TYPE"); + pw.println(" Gets the min volume for STREAM_TYPE"); + pw.println(" get-max-volume STREAM_TYPE"); + pw.println(" Gets the max volume for STREAM_TYPE"); + pw.println(" get-stream-volume STREAM_TYPE"); + pw.println(" Gets the volume for STREAM_TYPE"); pw.println(" set-device-volume STREAM_TYPE VOLUME_INDEX NATIVE_DEVICE_TYPE"); pw.println(" Sets for NATIVE_DEVICE_TYPE the STREAM_TYPE volume to VOLUME_INDEX"); pw.println(" adj-mute STREAM_TYPE"); @@ -296,6 +308,33 @@ class AudioManagerShellCommand extends ShellCommand { return 0; } + private int getMinVolume() { + final Context context = mService.mContext; + final AudioManager am = context.getSystemService(AudioManager.class); + final int stream = readIntArg(); + final int result = am.getStreamMinVolume(stream); + getOutPrintWriter().println("AudioManager.getStreamMinVolume(" + stream + ") -> " + result); + return 0; + } + + private int getMaxVolume() { + final Context context = mService.mContext; + final AudioManager am = context.getSystemService(AudioManager.class); + final int stream = readIntArg(); + final int result = am.getStreamMaxVolume(stream); + getOutPrintWriter().println("AudioManager.getStreamMaxVolume(" + stream + ") -> " + result); + return 0; + } + + private int getStreamVolume() { + final Context context = mService.mContext; + final AudioManager am = context.getSystemService(AudioManager.class); + final int stream = readIntArg(); + final int result = am.getStreamVolume(stream); + getOutPrintWriter().println("AudioManager.getStreamVolume(" + stream + ") -> " + result); + return 0; + } + private int setDeviceVolume() { final Context context = mService.mContext; final AudioDeviceVolumeManager advm = (AudioDeviceVolumeManager) context.getSystemService( diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 709c13bc9704..320dd8f188c0 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -47,9 +47,10 @@ import static android.media.AudioManager.RINGER_MODE_NORMAL; import static android.media.AudioManager.RINGER_MODE_SILENT; import static android.media.AudioManager.RINGER_MODE_VIBRATE; import static android.media.AudioManager.STREAM_SYSTEM; -import static android.media.IAudioManagerNative.HardeningType; import static android.media.audio.Flags.autoPublicVolumeApiHardening; import static android.media.audio.Flags.automaticBtDeviceType; +import static android.media.audio.Flags.cacheGetStreamMinMaxVolume; +import static android.media.audio.Flags.cacheGetStreamVolume; import static android.media.audio.Flags.concurrentAudioRecordBypassPermission; import static android.media.audio.Flags.featureSpatialAudioHeadtrackingLowLatency; import static android.media.audio.Flags.focusFreezeTestApi; @@ -898,6 +899,16 @@ public class AudioService extends IAudioService.Stub public void permissionUpdateBarrier() { AudioService.this.permissionUpdateBarrier(); } + + /** + * Update mute state event for port + * @param portId Port id to update + * @param event the mute event containing info about the mute + */ + @Override + public void portMuteEvent(int portId, int event) { + mPlaybackMonitor.portMuteEvent(portId, event, Binder.getCallingUid()); + } }; // List of binder death handlers for setMode() client processes. @@ -1900,6 +1911,12 @@ public class AudioService extends IAudioService.Stub mSpatializerHelper.onRoutingUpdated(); } checkMuteAwaitConnection(); + if (cacheGetStreamVolume()) { + if (DEBUG_VOL) { + Log.d(TAG, "Clear volume cache after routing update"); + } + AudioManager.clearVolumeCache(AudioManager.VOLUME_CACHING_API); + } } //----------------------------------------------------------------- @@ -4976,6 +4993,10 @@ public class AudioService extends IAudioService.Stub + ringMyCar()); pw.println("\tandroid.media.audio.Flags.concurrentAudioRecordBypassPermission:" + concurrentAudioRecordBypassPermission()); + pw.println("\tandroid.media.audio.Flags.cacheGetStreamMinMaxVolume:" + + cacheGetStreamMinMaxVolume()); + pw.println("\tandroid.media.audio.Flags.cacheGetStreamVolume:" + + cacheGetStreamVolume()); } private void dumpAudioMode(PrintWriter pw) { @@ -7042,6 +7063,13 @@ public class AudioService extends IAudioService.Stub streamState.mIsMuted = false; } } + if (cacheGetStreamVolume()) { + if (DEBUG_VOL) { + Log.d(TAG, + "Clear volume cache after possibly changing mute in readAudioSettings"); + } + AudioManager.clearVolumeCache(AudioManager.VOLUME_CACHING_API); + } } readVolumeGroupsSettings(userSwitch); @@ -9262,11 +9290,23 @@ public class AudioService extends IAudioService.Stub public void put(int key, int value) { super.put(key, value); record("put", key, value); + if (cacheGetStreamVolume()) { + if (DEBUG_VOL) { + Log.d(TAG, "Clear volume cache after update index map"); + } + AudioManager.clearVolumeCache(AudioManager.VOLUME_CACHING_API); + } } @Override public void setValueAt(int index, int value) { super.setValueAt(index, value); record("setValueAt", keyAt(index), value); + if (cacheGetStreamVolume()) { + if (DEBUG_VOL) { + Log.d(TAG, "Clear volume cache after update index map"); + } + AudioManager.clearVolumeCache(AudioManager.VOLUME_CACHING_API); + } } // Record all changes in the VolumeStreamState @@ -9366,6 +9406,12 @@ public class AudioService extends IAudioService.Stub mIndexMinNoPerm = mIndexMin; } } + if (cacheGetStreamMinMaxVolume() && mStreamType == AudioSystem.STREAM_VOICE_CALL) { + if (DEBUG_VOL) { + Log.d(TAG, "Clear min volume cache from updateIndexFactors"); + } + AudioManager.clearVolumeCache(AudioManager.VOLUME_MIN_CACHING_API); + } final int status = AudioSystem.initStreamVolume( mStreamType, indexMinVolCurve, indexMaxVolCurve); @@ -9403,11 +9449,19 @@ public class AudioService extends IAudioService.Stub * @param index minimum index expressed in "UI units", i.e. no 10x factor */ public void updateNoPermMinIndex(int index) { + boolean changedNoPermMinIndex = + cacheGetStreamMinMaxVolume() && (index * 10) != mIndexMinNoPerm; mIndexMinNoPerm = index * 10; if (mIndexMinNoPerm < mIndexMin) { Log.e(TAG, "Invalid mIndexMinNoPerm for stream " + mStreamType); mIndexMinNoPerm = mIndexMin; } + if (changedNoPermMinIndex) { + if (DEBUG_VOL) { + Log.d(TAG, "Clear min volume cache from updateNoPermMinIndex"); + } + AudioManager.clearVolumeCache(AudioManager.VOLUME_MIN_CACHING_API); + } } /** @@ -9982,8 +10036,9 @@ public class AudioService extends IAudioService.Stub * @return true if the mute state was changed */ public boolean mute(boolean state, boolean apply, String src) { + boolean changed; synchronized (VolumeStreamState.class) { - boolean changed = state != mIsMuted; + changed = state != mIsMuted; if (changed) { sMuteLogger.enqueue( new AudioServiceEvents.StreamMuteEvent(mStreamType, state, src)); @@ -10001,8 +10056,16 @@ public class AudioService extends IAudioService.Stub doMute(); } } - return changed; } + + if (cacheGetStreamVolume() && changed) { + if (DEBUG_VOL) { + Log.d(TAG, "Clear volume cache after changing mute state"); + } + AudioManager.clearVolumeCache(AudioManager.VOLUME_CACHING_API); + } + + return changed; } public void doMute() { diff --git a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java index e2e06b63c7d6..57b5febf4df0 100644 --- a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java +++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java @@ -435,6 +435,49 @@ public final class PlaybackActivityMonitor /** * Update event for port * @param portId Port id to update + * @param event the mute event containing info about the mute + * @param binderUid Calling binder uid + */ + public void portMuteEvent(int portId, @PlayerMuteEvent int event, int binderUid) { + if (!UserHandle.isCore(binderUid)) { + Log.e(TAG, "Forbidden operation from uid " + binderUid); + return; + } + + synchronized (mPlayerLock) { + int piid; + if (portToPiidSimplification()) { + int idxOfPiid = mPiidToPortId.indexOfValue(portId); + if (idxOfPiid < 0) { + Log.w(TAG, "No piid assigned for invalid/internal port id " + portId); + return; + } + piid = mPiidToPortId.keyAt(idxOfPiid); + } else { + piid = mPortIdToPiid.get(portId, PLAYER_PIID_INVALID); + if (piid == PLAYER_PIID_INVALID) { + Log.w(TAG, "No piid assigned for invalid/internal port id " + portId); + return; + } + } + final AudioPlaybackConfiguration apc = mPlayers.get(piid); + if (apc == null) { + Log.w(TAG, "No AudioPlaybackConfiguration assigned for piid " + piid); + return; + } + + if (apc.getPlayerType() + == AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL) { + // FIXME SoundPool not ready for state reporting + return; + } + mEventHandler.sendMessage( + mEventHandler.obtainMessage(MSG_IIL_UPDATE_PLAYER_MUTED_EVENT, piid, event, null)); + } + } + /** + * Update event for port + * @param portId Port id to update * @param event The new port event * @param extras The values associated with this event * @param binderUid Calling binder uid @@ -479,15 +522,10 @@ public final class PlaybackActivityMonitor return; } - if (event == AudioPlaybackConfiguration.PLAYER_UPDATE_MUTED) { - mEventHandler.sendMessage( - mEventHandler.obtainMessage(MSG_IIL_UPDATE_PLAYER_MUTED_EVENT, piid, - portId, - extras)); - } else if (event == AudioPlaybackConfiguration.PLAYER_UPDATE_FORMAT) { + if (event == AudioPlaybackConfiguration.PLAYER_UPDATE_FORMAT) { mEventHandler.sendMessage( mEventHandler.obtainMessage(MSG_IIL_UPDATE_PLAYER_FORMAT, piid, - portId, + -1, extras)); } } @@ -1695,9 +1733,7 @@ public final class PlaybackActivityMonitor * event for player getting muted * args: * msg.arg1: piid - * msg.arg2: port id - * msg.obj: extras describing the mute reason - * type: PersistableBundle + * msg.arg2: mute reason */ private static final int MSG_IIL_UPDATE_PLAYER_MUTED_EVENT = 2; @@ -1705,7 +1741,6 @@ public final class PlaybackActivityMonitor * event for player reporting playback format and spatialization status * args: * msg.arg1: piid - * msg.arg2: port id * msg.obj: extras describing the sample rate, channel mask, spatialized * type: PersistableBundle */ @@ -1729,17 +1764,9 @@ public final class PlaybackActivityMonitor break; case MSG_IIL_UPDATE_PLAYER_MUTED_EVENT: - // TODO: replace PersistableBundle with own struct - PersistableBundle extras = (PersistableBundle) msg.obj; - if (extras == null) { - Log.w(TAG, "Received mute event with no extras"); - break; - } - @PlayerMuteEvent int eventValue = extras.getInt(EXTRA_PLAYER_EVENT_MUTE); - synchronized (mPlayerLock) { int piid = msg.arg1; - + @PlayerMuteEvent int eventValue = msg.arg2; int[] eventValues = new int[1]; eventValues[0] = eventValue; diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java index d435144b28c6..b9ce8c93dbde 100644 --- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java +++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java @@ -21,6 +21,7 @@ import android.os.Build; import android.os.SystemProperties; import android.text.TextUtils; import android.util.Slog; +import android.window.DesktopExperienceFlags; import com.android.server.display.feature.flags.Flags; import com.android.server.display.utils.DebugUtils; @@ -250,7 +251,7 @@ public class DisplayManagerFlags { ); private final FlagState mEnableDisplayContentModeManagementFlagState = new FlagState( Flags.FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT, - Flags::enableDisplayContentModeManagement + DesktopExperienceFlags.ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT::isTrue ); private final FlagState mSubscribeGranularDisplayEvents = new FlagState( diff --git a/services/core/java/com/android/server/display/plugin/PluginManager.java b/services/core/java/com/android/server/display/plugin/PluginManager.java index d4099975cafa..cb0a4574361a 100644 --- a/services/core/java/com/android/server/display/plugin/PluginManager.java +++ b/services/core/java/com/android/server/display/plugin/PluginManager.java @@ -74,15 +74,17 @@ public class PluginManager { /** * Adds change listener for particular plugin type */ - public <T> void subscribe(PluginType<T> type, PluginChangeListener<T> listener) { - mPluginStorage.addListener(type, listener); + public <T> void subscribe(PluginType<T> type, String uniqueDisplayId, + PluginChangeListener<T> listener) { + mPluginStorage.addListener(type, uniqueDisplayId, listener); } /** * Removes change listener */ - public <T> void unsubscribe(PluginType<T> type, PluginChangeListener<T> listener) { - mPluginStorage.removeListener(type, listener); + public <T> void unsubscribe(PluginType<T> type, String uniqueDisplayId, + PluginChangeListener<T> listener) { + mPluginStorage.removeListener(type, uniqueDisplayId, listener); } /** diff --git a/services/core/java/com/android/server/display/plugin/PluginStorage.java b/services/core/java/com/android/server/display/plugin/PluginStorage.java index dd3415fb614d..5102c2709329 100644 --- a/services/core/java/com/android/server/display/plugin/PluginStorage.java +++ b/services/core/java/com/android/server/display/plugin/PluginStorage.java @@ -20,10 +20,13 @@ import android.annotation.Nullable; import android.util.Slog; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.tools.r8.keepanno.annotations.KeepForApi; import java.io.PrintWriter; import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -35,42 +38,97 @@ import java.util.Set; public class PluginStorage { private static final String TAG = "PluginStorage"; + // Special ID used to indicate that given value is to be applied globally, rather than to a + // specific display. If both GLOBAL and specific display values are present - specific display + // value is selected. + @VisibleForTesting + static final String GLOBAL_ID = "GLOBAL"; + private final Object mLock = new Object(); @GuardedBy("mLock") - private final Map<PluginType<?>, Object> mValues = new HashMap<>(); + private final Map<PluginType<?>, ValuesContainer<?>> mValues = new HashMap<>(); @GuardedBy("mLock") private final Map<PluginType<?>, ListenersContainer<?>> mListeners = new HashMap<>(); @GuardedBy("mLock") - private final PluginEventStorage mPluginEventStorage = new PluginEventStorage(); + private final Map<String, PluginEventStorage> mPluginEventStorages = new HashMap<>(); + + /** + * Updates value in storage and forwards it to corresponding listeners for all displays + * that does not have display specific value. + * Should be called by OEM Plugin implementation in order to communicate with Framework + */ + @KeepForApi + public <T> void updateGlobalValue(PluginType<T> type, @Nullable T value) { + updateValue(type, GLOBAL_ID, value); + } /** - * Updates value in storage and forwards it to corresponding listeners. - * Should be called by OEM Plugin implementation in order to provide communicate with Framework + * Updates value in storage and forwards it to corresponding listeners for specific display. + * Should be called by OEM Plugin implementation in order to communicate with Framework + * @param type - plugin type, that need to be updated + * @param uniqueDisplayId - uniqueDisplayId that this type/value should be applied to + * @param value - plugin value for particular type and display */ @KeepForApi - public <T> void updateValue(PluginType<T> type, @Nullable T value) { - Slog.d(TAG, "updateValue, type=" + type.mName + "; value=" + value); + public <T> void updateValue(PluginType<T> type, String uniqueDisplayId, @Nullable T value) { + Slog.d(TAG, "updateValue, type=" + type.mName + "; value=" + value + + "; displayId=" + uniqueDisplayId); Set<PluginManager.PluginChangeListener<T>> localListeners; + T valueToNotify; synchronized (mLock) { - mValues.put(type, value); - mPluginEventStorage.onValueUpdated(type); - ListenersContainer<T> container = getListenersContainerForTypeLocked(type); - localListeners = new LinkedHashSet<>(container.mListeners); + ValuesContainer<T> valuesByType = getValuesContainerLocked(type); + valuesByType.updateValueLocked(uniqueDisplayId, value); + // if value was set to null, we might need to notify with GLOBAL value instead + valueToNotify = valuesByType.getValueLocked(uniqueDisplayId); + + PluginEventStorage storage = mPluginEventStorages.computeIfAbsent(uniqueDisplayId, + d -> new PluginEventStorage()); + storage.onValueUpdated(type); + + localListeners = getListenersForUpdateLocked(type, uniqueDisplayId); } Slog.d(TAG, "updateValue, notifying listeners=" + localListeners); - localListeners.forEach(l -> l.onChanged(value)); + localListeners.forEach(l -> l.onChanged(valueToNotify)); + } + + @GuardedBy("mLock") + private <T> Set<PluginManager.PluginChangeListener<T>> getListenersForUpdateLocked( + PluginType<T> type, String uniqueDisplayId) { + ListenersContainer<T> listenersContainer = getListenersContainerLocked(type); + Set<PluginManager.PluginChangeListener<T>> localListeners = new LinkedHashSet<>(); + // if GLOBAL value change we need to notify only listeners for displays that does not + // have display specific value + if (GLOBAL_ID.equals(uniqueDisplayId)) { + ValuesContainer<T> valuesContainer = getValuesContainerLocked(type); + Set<String> excludedDisplayIds = valuesContainer.getNonGlobalDisplaysLocked(); + listenersContainer.mListeners.forEach((localDisplayId, listeners) -> { + if (!excludedDisplayIds.contains(localDisplayId)) { + localListeners.addAll(listeners); + } + }); + } else { + localListeners.addAll( + listenersContainer.mListeners.getOrDefault(uniqueDisplayId, Set.of())); + } + return localListeners; } /** * Adds listener for PluginType. If storage already has value for this type, listener will * be notified immediately. */ - <T> void addListener(PluginType<T> type, PluginManager.PluginChangeListener<T> listener) { + <T> void addListener(PluginType<T> type, String uniqueDisplayId, + PluginManager.PluginChangeListener<T> listener) { + if (GLOBAL_ID.equals(uniqueDisplayId)) { + Slog.d(TAG, "addListener ignored for GLOBAL_ID, type=" + type.mName); + return; + } T value = null; synchronized (mLock) { - ListenersContainer<T> container = getListenersContainerForTypeLocked(type); - if (container.mListeners.add(listener)) { - value = getValueForTypeLocked(type); + ListenersContainer<T> container = getListenersContainerLocked(type); + if (container.addListenerLocked(uniqueDisplayId, listener)) { + ValuesContainer<T> valuesContainer = getValuesContainerLocked(type); + value = valuesContainer.getValueLocked(uniqueDisplayId); } } if (value != null) { @@ -81,10 +139,15 @@ public class PluginStorage { /** * Removes listener */ - <T> void removeListener(PluginType<T> type, PluginManager.PluginChangeListener<T> listener) { + <T> void removeListener(PluginType<T> type, String uniqueDisplayId, + PluginManager.PluginChangeListener<T> listener) { + if (GLOBAL_ID.equals(uniqueDisplayId)) { + Slog.d(TAG, "removeListener ignored for GLOBAL_ID, type=" + type.mName); + return; + } synchronized (mLock) { - ListenersContainer<T> container = getListenersContainerForTypeLocked(type); - container.mListeners.remove(listener); + ListenersContainer<T> container = getListenersContainerLocked(type); + container.removeListenerLocked(uniqueDisplayId, listener); } } @@ -92,53 +155,106 @@ public class PluginStorage { * Print the object's state and debug information into the given stream. */ void dump(PrintWriter pw) { - Map<PluginType<?>, Object> localValues; + Map<PluginType<?>, Map<String, Object>> localValues = new HashMap<>(); @SuppressWarnings("rawtypes") - Map<PluginType, Set> localListeners = new HashMap<>(); - List<PluginEventStorage.TimeFrame> timeFrames; + Map<PluginType, Map<String, Set>> localListeners = new HashMap<>(); + Map<String, List<PluginEventStorage.TimeFrame>> timeFrames = new HashMap<>(); synchronized (mLock) { - timeFrames = mPluginEventStorage.getTimeFrames(); - localValues = new HashMap<>(mValues); - mListeners.forEach((type, container) -> localListeners.put(type, container.mListeners)); + mPluginEventStorages.forEach((displayId, storage) -> { + timeFrames.put(displayId, storage.getTimeFrames()); + }); + mValues.forEach((type, valueContainer) -> { + localValues.put(type, new HashMap<>(valueContainer.mValues)); + }); + mListeners.forEach((type, container) -> { + localListeners.put(type, new HashMap<>(container.mListeners)); + }); } pw.println("PluginStorage:"); pw.println("values=" + localValues); pw.println("listeners=" + localListeners); pw.println("PluginEventStorage:"); - for (PluginEventStorage.TimeFrame timeFrame: timeFrames) { - timeFrame.dump(pw); + for (Map.Entry<String, List<PluginEventStorage.TimeFrame>> timeFrameEntry : + timeFrames.entrySet()) { + pw.println("TimeFrames for displayId=" + timeFrameEntry.getKey()); + for (PluginEventStorage.TimeFrame timeFrame : timeFrameEntry.getValue()) { + timeFrame.dump(pw); + } } } @GuardedBy("mLock") @SuppressWarnings("unchecked") - private <T> T getValueForTypeLocked(PluginType<T> type) { - Object value = mValues.get(type); - if (value == null) { - return null; - } else if (type.mType == value.getClass()) { - return (T) value; + private <T> ListenersContainer<T> getListenersContainerLocked(PluginType<T> type) { + ListenersContainer<?> container = mListeners.get(type); + if (container == null) { + ListenersContainer<T> lc = new ListenersContainer<>(); + mListeners.put(type, lc); + return lc; } else { - Slog.d(TAG, "getValueForType: unexpected value type=" + value.getClass().getName() - + ", expected=" + type.mType.getName()); - return null; + return (ListenersContainer<T>) container; } } @GuardedBy("mLock") @SuppressWarnings("unchecked") - private <T> ListenersContainer<T> getListenersContainerForTypeLocked(PluginType<T> type) { - ListenersContainer<?> container = mListeners.get(type); + private <T> ValuesContainer<T> getValuesContainerLocked(PluginType<T> type) { + ValuesContainer<?> container = mValues.get(type); if (container == null) { - ListenersContainer<T> lc = new ListenersContainer<>(); - mListeners.put(type, lc); - return lc; + ValuesContainer<T> vc = new ValuesContainer<>(); + mValues.put(type, vc); + return vc; } else { - return (ListenersContainer<T>) container; + return (ValuesContainer<T>) container; } } private static final class ListenersContainer<T> { - private final Set<PluginManager.PluginChangeListener<T>> mListeners = new LinkedHashSet<>(); + private final Map<String, Set<PluginManager.PluginChangeListener<T>>> mListeners = + new LinkedHashMap<>(); + + private boolean addListenerLocked( + String uniqueDisplayId, PluginManager.PluginChangeListener<T> listener) { + Set<PluginManager.PluginChangeListener<T>> listenersForDisplay = + mListeners.computeIfAbsent(uniqueDisplayId, k -> new LinkedHashSet<>()); + return listenersForDisplay.add(listener); + } + + private void removeListenerLocked(String uniqueDisplayId, + PluginManager.PluginChangeListener<T> listener) { + Set<PluginManager.PluginChangeListener<T>> listenersForDisplay = mListeners.get( + uniqueDisplayId); + if (listenersForDisplay == null) { + return; + } + + listenersForDisplay.remove(listener); + + if (listenersForDisplay.isEmpty()) { + mListeners.remove(uniqueDisplayId); + } + } + } + + private static final class ValuesContainer<T> { + private final Map<String, T> mValues = new HashMap<>(); + + private void updateValueLocked(String uniqueDisplayId, @Nullable T value) { + if (value == null) { + mValues.remove(uniqueDisplayId); + } else { + mValues.put(uniqueDisplayId, value); + } + } + + private Set<String> getNonGlobalDisplaysLocked() { + Set<String> keys = new HashSet<>(mValues.keySet()); + keys.remove(GLOBAL_ID); + return keys; + } + + private @Nullable T getValueLocked(String displayId) { + return mValues.getOrDefault(displayId, mValues.get(GLOBAL_ID)); + } } } diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index c0dbfa546a94..89f0d0edbf2b 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -710,7 +710,9 @@ public class HdmiControlService extends SystemService { // Register ContentObserver to monitor the settings change. registerContentObserver(); } - mMhlController.setOption(OPTION_MHL_SERVICE_CONTROL, ENABLED); + if (mMhlController != null) { + mMhlController.setOption(OPTION_MHL_SERVICE_CONTROL, ENABLED); + } } @VisibleForTesting diff --git a/services/core/java/com/android/server/infra/OWNERS b/services/core/java/com/android/server/infra/OWNERS index 4fea05d295b6..0f0d382e28f8 100644 --- a/services/core/java/com/android/server/infra/OWNERS +++ b/services/core/java/com/android/server/infra/OWNERS @@ -1,3 +1,4 @@ # Bug component: 655446 srazdan@google.com +reemabajwa@google.com diff --git a/services/core/java/com/android/server/input/InputGestureManager.java b/services/core/java/com/android/server/input/InputGestureManager.java index fd755e3cefe2..32b36bfb50e5 100644 --- a/services/core/java/com/android/server/input/InputGestureManager.java +++ b/services/core/java/com/android/server/input/InputGestureManager.java @@ -23,7 +23,6 @@ import static com.android.hardware.input.Flags.enableVoiceAccessKeyGestures; import static com.android.hardware.input.Flags.keyboardA11yShortcutControl; import static com.android.server.flags.Flags.newBugreportKeyboardShortcut; import static com.android.window.flags.Flags.enableMoveToNextDisplayShortcut; -import static com.android.window.flags.Flags.enableTaskResizingKeyboardShortcuts; import android.annotation.NonNull; import android.annotation.Nullable; @@ -37,6 +36,7 @@ import android.os.SystemProperties; import android.util.IndentingPrintWriter; import android.util.SparseArray; import android.view.KeyEvent; +import android.window.DesktopModeFlags; import com.android.internal.annotations.GuardedBy; @@ -186,21 +186,11 @@ final class InputGestureManager { KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT ), createKeyGesture( - KeyEvent.KEYCODE_DPAD_LEFT, - KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON, - KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT - ), - createKeyGesture( KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON, KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT ), createKeyGesture( - KeyEvent.KEYCODE_DPAD_RIGHT, - KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON, - KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT - ), - createKeyGesture( KeyEvent.KEYCODE_SLASH, KeyEvent.META_META_ON, KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER @@ -243,7 +233,7 @@ final class InputGestureManager { KeyEvent.META_META_ON | KeyEvent.META_ALT_ON, KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS)); } - if (enableTaskResizingKeyboardShortcuts()) { + if (DesktopModeFlags.ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS.isTrue()) { systemShortcuts.add(createKeyGesture( KeyEvent.KEYCODE_LEFT_BRACKET, KeyEvent.META_META_ON, diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java index b9352edf9230..ddace179348c 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java @@ -42,6 +42,7 @@ import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import java.util.Collection; +import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -372,10 +373,12 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub /* package */ void onEndpointSessionOpenRequest( int sessionId, HubEndpointInfo initiator, String serviceDescriptor) { - boolean success = + Optional<Byte> error = onEndpointSessionOpenRequestInternal(sessionId, initiator, serviceDescriptor); - if (!success) { - cleanupSessionResources(sessionId); + if (error.isPresent()) { + halCloseEndpointSessionNoThrow(sessionId, error.get()); + onCloseEndpointSession(sessionId, error.get()); + // Resource cleanup is done in onCloseEndpointSession } } @@ -422,7 +425,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } } - private boolean onEndpointSessionOpenRequestInternal( + private Optional<Byte> onEndpointSessionOpenRequestInternal( int sessionId, HubEndpointInfo initiator, String serviceDescriptor) { if (!hasEndpointPermissions(initiator)) { Log.e( @@ -431,22 +434,21 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub + initiator + " doesn't have permission for " + mEndpointInfo); - halCloseEndpointSessionNoThrow(sessionId, Reason.PERMISSION_DENIED); - return false; + return Optional.of(Reason.PERMISSION_DENIED); } synchronized (mOpenSessionLock) { if (hasSessionId(sessionId)) { Log.e(TAG, "Existing session in onEndpointSessionOpenRequest: id=" + sessionId); - halCloseEndpointSessionNoThrow(sessionId, Reason.UNSPECIFIED); - return false; + return Optional.of(Reason.UNSPECIFIED); } mSessionInfoMap.put(sessionId, new SessionInfo(initiator, true)); } - return invokeCallback( + boolean success = invokeCallback( (consumer) -> consumer.onSessionOpenRequest(sessionId, initiator, serviceDescriptor)); + return success ? Optional.empty() : Optional.of(Reason.UNSPECIFIED); } private byte onMessageReceivedInternal(int sessionId, HubMessage message) { diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java index 011659a616d3..3eb38a7029e6 100644 --- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java +++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java @@ -17,6 +17,7 @@ package com.android.server.media; import static android.media.MediaRoute2Info.FEATURE_LIVE_AUDIO; +import static android.media.MediaRoute2Info.FEATURE_LIVE_VIDEO; import android.annotation.NonNull; import android.annotation.Nullable; @@ -217,6 +218,28 @@ import java.util.stream.Stream; } } + @Override + public void setRouteVolume(long requestId, String routeOriginalId, int volume) { + synchronized (mLock) { + var targetProviderProxyId = mOriginalRouteIdToProviderId.get(routeOriginalId); + var targetProviderProxyRecord = mProxyRecords.get(targetProviderProxyId); + // Holds the target route, if it's managed by a provider service. Holds null otherwise. + if (targetProviderProxyRecord != null) { + var serviceTargetRoute = + targetProviderProxyRecord.mNewOriginalIdToSourceOriginalIdMap.get( + routeOriginalId); + if (serviceTargetRoute != null) { + targetProviderProxyRecord.mProxy.setRouteVolume( + requestId, serviceTargetRoute, volume); + } else { + notifyRequestFailed( + requestId, MediaRoute2ProviderService.REASON_ROUTE_NOT_AVAILABLE); + } + } + } + super.setRouteVolume(requestId, routeOriginalId, volume); + } + /** * Returns the uid that corresponds to the given name and user handle, or {@link * Process#INVALID_UID} if a uid couldn't be found. @@ -463,11 +486,18 @@ import java.util.stream.Stream; } String id = asSystemRouteId(providerInfo.getUniqueId(), sourceRoute.getOriginalId()); - var newRoute = - new MediaRoute2Info.Builder(id, sourceRoute.getName()) - .addFeature(FEATURE_LIVE_AUDIO) - .build(); - routesMap.put(id, newRoute); + var newRouteBuilder = new MediaRoute2Info.Builder(id, sourceRoute); + if ((sourceRoute.getSupportedRoutingTypes() + & MediaRoute2Info.FLAG_ROUTING_TYPE_SYSTEM_AUDIO) + != 0) { + newRouteBuilder.addFeature(FEATURE_LIVE_AUDIO); + } + if ((sourceRoute.getSupportedRoutingTypes() + & MediaRoute2Info.FLAG_ROUTING_TYPE_SYSTEM_VIDEO) + != 0) { + newRouteBuilder.addFeature(FEATURE_LIVE_VIDEO); + } + routesMap.put(id, newRouteBuilder.build()); idMap.put(id, sourceRoute.getOriginalId()); } return new ProviderProxyRecord( diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java index c428f39fd9d0..34a6cb951d46 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -58,6 +58,7 @@ import android.media.projection.IMediaProjection; import android.media.projection.IMediaProjectionCallback; import android.media.projection.IMediaProjectionManager; import android.media.projection.IMediaProjectionWatcherCallback; +import android.media.projection.MediaProjectionEvent; import android.media.projection.MediaProjectionInfo; import android.media.projection.MediaProjectionManager; import android.media.projection.ReviewGrantedConsentResult; @@ -80,6 +81,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.DumpUtils; +import com.android.media.projection.flags.Flags; import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.Watchdog; @@ -177,9 +179,31 @@ public final class MediaProjectionManagerService extends SystemService private void maybeStopMediaProjection(int reason) { synchronized (mLock) { - if (!mMediaProjectionStopController.isExemptFromStopping(mProjectionGrant, reason)) { - Slog.d(TAG, "Content Recording: Stopping MediaProjection due to " - + MediaProjectionStopController.stopReasonToString(reason)); + if (mMediaProjectionStopController.isExemptFromStopping(mProjectionGrant, reason)) { + return; + } + + if (Flags.showStopDialogPostCallEnd() + && mMediaProjectionStopController.isStopReasonCallEnd(reason)) { + MediaProjectionEvent event = + new MediaProjectionEvent( + MediaProjectionEvent + .PROJECTION_STARTED_DURING_CALL_AND_ACTIVE_POST_CALL, + System.currentTimeMillis()); + Slog.d( + TAG, + "Scheduling event: " + + event.getEventType() + + " for reason: " + + MediaProjectionStopController.stopReasonToString(reason)); + + // Post the PROJECTION_STARTED_DURING_CALL_AND_ACTIVE_POST_CALL event with a delay. + mHandler.postDelayed(() -> dispatchEvent(event), 500); + } else { + Slog.d( + TAG, + "Stopping MediaProjection due to reason: " + + MediaProjectionStopController.stopReasonToString(reason)); mProjectionGrant.stop(StopReason.STOP_DEVICE_LOCKED); } } @@ -388,6 +412,24 @@ public final class MediaProjectionManagerService extends SystemService mCallbackDelegate.dispatchSession(projectionInfo, session); } + private void dispatchEvent(@NonNull MediaProjectionEvent event) { + if (!Flags.showStopDialogPostCallEnd()) { + Slog.d( + TAG, + "Event dispatch skipped. Reason: Flag showStopDialogPostCallEnd " + + "is disabled. Event details: " + + event); + return; + } + MediaProjectionInfo projectionInfo; + ContentRecordingSession session; + synchronized (mLock) { + projectionInfo = mProjectionGrant != null ? mProjectionGrant.getProjectionInfo() : null; + session = mProjectionGrant != null ? mProjectionGrant.mSession : null; + } + mCallbackDelegate.dispatchEvent(event, projectionInfo, session); + } + /** * Returns {@code true} when updating the current mirroring session on WM succeeded, and * {@code false} otherwise. @@ -1467,6 +1509,25 @@ public final class MediaProjectionManagerService extends SystemService } } + private void dispatchEvent( + @NonNull MediaProjectionEvent event, + @Nullable MediaProjectionInfo info, + @Nullable ContentRecordingSession session) { + if (!Flags.showStopDialogPostCallEnd()) { + Slog.d( + TAG, + "Event dispatch skipped. Reason: Flag showStopDialogPostCallEnd " + + "is disabled. Event details: " + + event); + return; + } + synchronized (mLock) { + for (IMediaProjectionWatcherCallback callback : mWatcherCallbacks.values()) { + mHandler.post(new WatcherEventCallback(callback, event, info, session)); + } + } + } + public void dispatchSession( @NonNull MediaProjectionInfo projectionInfo, @Nullable ContentRecordingSession session) { @@ -1593,6 +1654,41 @@ public final class MediaProjectionManagerService extends SystemService } } + private static final class WatcherEventCallback implements Runnable { + private final IMediaProjectionWatcherCallback mCallback; + private final MediaProjectionEvent mEvent; + private final MediaProjectionInfo mProjectionInfo; + private final ContentRecordingSession mSession; + + WatcherEventCallback( + @NonNull IMediaProjectionWatcherCallback callback, + @NonNull MediaProjectionEvent event, + @Nullable MediaProjectionInfo projectionInfo, + @Nullable ContentRecordingSession session) { + mCallback = callback; + mEvent = event; + mProjectionInfo = projectionInfo; + mSession = session; + } + + @Override + public void run() { + if (!Flags.showStopDialogPostCallEnd()) { + Slog.d( + TAG, + "Not running WatcherEventCallback. Reason: Flag " + + "showStopDialogPostCallEnd is disabled. " + ); + return; + } + try { + mCallback.onMediaProjectionEvent(mEvent, mProjectionInfo, mSession); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to notify MediaProjectionEvent change", e); + } + } + } + private static final class WatcherSessionCallback implements Runnable { private final IMediaProjectionWatcherCallback mCallback; private final MediaProjectionInfo mProjectionInfo; diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionStopController.java b/services/core/java/com/android/server/media/projection/MediaProjectionStopController.java index c018e6bc1dc7..18f2f48b80a3 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionStopController.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionStopController.java @@ -28,6 +28,7 @@ import android.content.Context; import android.content.pm.PackageManager; import android.os.Binder; import android.os.SystemClock; +import android.os.UserHandle; import android.provider.Settings; import android.telecom.TelecomManager; import android.telephony.TelephonyCallback; @@ -38,6 +39,7 @@ import android.view.Display; import com.android.internal.annotations.VisibleForTesting; import com.android.server.SystemConfig; +import java.util.List; import java.util.function.Consumer; /** @@ -60,21 +62,35 @@ public class MediaProjectionStopController { private final TelephonyManager mTelephonyManager; private final AppOpsManager mAppOpsManager; private final PackageManager mPackageManager; - private final RoleManager mRoleManager; + private final RoleHolderProvider mRoleHolderProvider; private final ContentResolver mContentResolver; private boolean mIsInCall; private long mLastCallStartTimeMillis; + + @VisibleForTesting + interface RoleHolderProvider { + List<String> getRoleHoldersAsUser(String roleName, UserHandle user); + } + public MediaProjectionStopController(Context context, Consumer<Integer> stopReasonConsumer) { + this(context, stopReasonConsumer, + (roleName, user) -> context.getSystemService(RoleManager.class) + .getRoleHoldersAsUser(roleName, user)); + } + + @VisibleForTesting + MediaProjectionStopController(Context context, Consumer<Integer> stopReasonConsumer, + RoleHolderProvider roleHolderProvider) { mStopReasonConsumer = stopReasonConsumer; mKeyguardManager = context.getSystemService(KeyguardManager.class); mTelecomManager = context.getSystemService(TelecomManager.class); mTelephonyManager = context.getSystemService(TelephonyManager.class); mAppOpsManager = context.getSystemService(AppOpsManager.class); mPackageManager = context.getPackageManager(); - mRoleManager = context.getSystemService(RoleManager.class); mContentResolver = context.getContentResolver(); + mRoleHolderProvider = roleHolderProvider; } /** @@ -95,6 +111,11 @@ public class MediaProjectionStopController { } } + /** Checks if the given stop reason corresponds to a call ending. */ + public boolean isStopReasonCallEnd(int stopReason) { + return stopReason == STOP_REASON_CALL_END; + } + /** * Checks whether the given projection grant is exempt from stopping restrictions. */ @@ -141,8 +162,9 @@ public class MediaProjectionStopController { Slog.v(TAG, "Continuing MediaProjection for package with OP_PROJECT_MEDIA AppOp "); return true; } - if (mRoleManager.getRoleHoldersAsUser(AssociationRequest.DEVICE_PROFILE_APP_STREAMING, - projectionGrant.userHandle).contains(projectionGrant.packageName)) { + if (mRoleHolderProvider.getRoleHoldersAsUser( + AssociationRequest.DEVICE_PROFILE_APP_STREAMING, projectionGrant.userHandle) + .contains(projectionGrant.packageName)) { Slog.v(TAG, "Continuing MediaProjection for package holding app streaming role."); return true; } @@ -172,10 +194,6 @@ public class MediaProjectionStopController { */ public boolean isStartForbidden( MediaProjectionManagerService.MediaProjection projectionGrant) { - if (!android.companion.virtualdevice.flags.Flags.mediaProjectionKeyguardRestrictions()) { - return false; - } - if (!mKeyguardManager.isKeyguardLocked()) { return false; } @@ -189,9 +207,6 @@ public class MediaProjectionStopController { @VisibleForTesting void onKeyguardLockedStateChanged(boolean isKeyguardLocked) { if (!isKeyguardLocked) return; - if (!android.companion.virtualdevice.flags.Flags.mediaProjectionKeyguardRestrictions()) { - return; - } mStopReasonConsumer.accept(STOP_REASON_KEYGUARD); } diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java index 2a5b779546d5..f6c94a7d9a5a 100644 --- a/services/core/java/com/android/server/media/quality/MediaQualityService.java +++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java @@ -29,11 +29,19 @@ import android.content.pm.PackageManager; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.hardware.tv.mediaquality.AmbientBacklightColorFormat; +import android.hardware.tv.mediaquality.DolbyAudioProcessing; +import android.hardware.tv.mediaquality.DtsVirtualX; import android.hardware.tv.mediaquality.IMediaQuality; +import android.hardware.tv.mediaquality.IPictureProfileAdjustmentListener; +import android.hardware.tv.mediaquality.IPictureProfileChangedListener; +import android.hardware.tv.mediaquality.ISoundProfileAdjustmentListener; +import android.hardware.tv.mediaquality.ISoundProfileChangedListener; +import android.hardware.tv.mediaquality.ParamCapability; import android.hardware.tv.mediaquality.PictureParameter; import android.hardware.tv.mediaquality.PictureParameters; import android.hardware.tv.mediaquality.SoundParameter; import android.hardware.tv.mediaquality.SoundParameters; +import android.hardware.tv.mediaquality.VendorParamCapability; import android.media.quality.AmbientBacklightEvent; import android.media.quality.AmbientBacklightMetadata; import android.media.quality.AmbientBacklightSettings; @@ -103,6 +111,10 @@ public class MediaQualityService extends SystemService { private final BiMap<Long, String> mPictureProfileTempIdMap; private final BiMap<Long, String> mSoundProfileTempIdMap; private IMediaQuality mMediaQuality; + private IPictureProfileAdjustmentListener mPpAdjustmentListener; + private ISoundProfileAdjustmentListener mSpAdjustmentListener; + private IPictureProfileChangedListener mPpChangedListener; + private ISoundProfileChangedListener mSpChangedListener; private final HalAmbientBacklightCallback mHalAmbientBacklightCallback; private final Map<String, AmbientBacklightCallbackRecord> mCallbackRecords = new HashMap<>(); private final PackageManager mPackageManager; @@ -138,18 +150,104 @@ public class MediaQualityService extends SystemService { @Override public void onStart() { IBinder binder = ServiceManager.getService(IMediaQuality.DESCRIPTOR + "/default"); - if (binder != null) { - Slogf.d(TAG, "binder is not null"); - mMediaQuality = IMediaQuality.Stub.asInterface(binder); - if (mMediaQuality != null) { - try { - mMediaQuality.setAmbientBacklightCallback(mHalAmbientBacklightCallback); - } catch (RemoteException e) { - Slog.e(TAG, "Failed to set ambient backlight detector callback", e); + if (binder == null) { + Slogf.d(TAG, "Binder is null"); + return; + } + Slogf.d(TAG, "Binder is not null"); + + mPpAdjustmentListener = new IPictureProfileAdjustmentListener.Stub() { + @Override + public void onPictureProfileAdjusted( + android.hardware.tv.mediaquality.PictureProfile pictureProfile) + throws RemoteException { + // TODO + } + + @Override + public void onParamCapabilityChanged(long pictureProfileId, ParamCapability[] caps) + throws RemoteException { + // TODO + } + + @Override + public void onVendorParamCapabilityChanged(long pictureProfileId, + VendorParamCapability[] caps) throws RemoteException { + // TODO + } + + @Override + public void requestPictureParameters(long pictureProfileId) throws RemoteException { + // TODO + } + + @Override + public void onStreamStatusChanged(long pictureProfileId, byte status) + throws RemoteException { + // TODO + } + + @Override + public int getInterfaceVersion() throws RemoteException { + return 0; + } + + @Override + public String getInterfaceHash() throws RemoteException { + return null; } + }; + mSpAdjustmentListener = new ISoundProfileAdjustmentListener.Stub() { + + @Override + public void onSoundProfileAdjusted( + android.hardware.tv.mediaquality.SoundProfile soundProfile) + throws RemoteException { + // TODO + } + + @Override + public void onParamCapabilityChanged(long soundProfileId, ParamCapability[] caps) + throws RemoteException { + // TODO + } + + @Override + public void onVendorParamCapabilityChanged(long soundProfileId, + VendorParamCapability[] caps) throws RemoteException { + // TODO + } + + @Override + public void requestSoundParameters(long soundProfileId) throws RemoteException { + // TODO + } + + @Override + public int getInterfaceVersion() throws RemoteException { + return 0; + } + + @Override + public String getInterfaceHash() throws RemoteException { + return null; + } + }; + + mMediaQuality = IMediaQuality.Stub.asInterface(binder); + if (mMediaQuality != null) { + try { + mMediaQuality.setAmbientBacklightCallback(mHalAmbientBacklightCallback); + mMediaQuality.setPictureProfileAdjustmentListener(mPpAdjustmentListener); + mMediaQuality.setSoundProfileAdjustmentListener(mSpAdjustmentListener); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to set ambient backlight detector callback", e); } } + mPpChangedListener = IPictureProfileChangedListener.Stub.asInterface(binder); + mSpChangedListener = ISoundProfileChangedListener.Stub.asInterface(binder); + publishBinderService(Context.MEDIA_QUALITY_SERVICE, new BinderService()); } @@ -185,6 +283,30 @@ public class MediaQualityService extends SystemService { return pp; } + private void notifyHalOnPictureProfileChange(Long dbId, PersistableBundle params) { + // TODO: only notify HAL when the profile is active / being used + try { + mPpChangedListener.onPictureProfileChanged(convertToHalPictureProfile(dbId, + params)); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to notify HAL on picture profile change.", e); + } + } + + private android.hardware.tv.mediaquality.PictureProfile convertToHalPictureProfile(Long id, + PersistableBundle params) { + PictureParameters pictureParameters = new PictureParameters(); + pictureParameters.pictureParameters = convertPersistableBundleToPictureParameterList( + params); + + android.hardware.tv.mediaquality.PictureProfile toReturn = + new android.hardware.tv.mediaquality.PictureProfile(); + toReturn.pictureProfileId = id; + toReturn.parameters = pictureParameters; + + return toReturn; + } + @Override public void updatePictureProfile(String id, PictureProfile pp, UserHandle user) { Long dbId = mPictureProfileTempIdMap.getKey(id); @@ -205,6 +327,7 @@ public class MediaQualityService extends SystemService { null, values); notifyOnPictureProfileUpdated(mPictureProfileTempIdMap.getValue(dbId), getPictureProfile(dbId), Binder.getCallingUid(), Binder.getCallingPid()); + notifyHalOnPictureProfileChange(dbId, pp.getParameters()); } private boolean hasPermissionToUpdatePictureProfile(Long dbId, PictureProfile toUpdate) { @@ -238,6 +361,7 @@ public class MediaQualityService extends SystemService { notifyOnPictureProfileRemoved(mPictureProfileTempIdMap.getValue(dbId), toDelete, Binder.getCallingUid(), Binder.getCallingPid()); mPictureProfileTempIdMap.remove(dbId); + notifyHalOnPictureProfileChange(dbId, null); } } @@ -357,6 +481,10 @@ public class MediaQualityService extends SystemService { private PictureParameter[] convertPersistableBundleToPictureParameterList( PersistableBundle params) { + if (params == null) { + return null; + } + List<PictureParameter> pictureParams = new ArrayList<>(); if (params.containsKey(PictureQuality.PARAMETER_BRIGHTNESS)) { pictureParams.add(PictureParameter.brightness(params.getLong( @@ -461,7 +589,7 @@ public class MediaQualityService extends SystemService { } if (params.containsKey(PictureQuality.PARAMETER_AUTO_SUPER_RESOLUTION_ENABLED)) { pictureParams.add(PictureParameter.autoSuperResolutionEnabled(params.getBoolean( - PictureQuality.PARAMETER_AUTO_SUPER_RESOLUTION_ENABLED))); + PictureQuality.PARAMETER_AUTO_SUPER_RESOLUTION_ENABLED))); } if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_RED_GAIN)) { pictureParams.add(PictureParameter.colorTemperatureRedGain(params.getInt( @@ -475,63 +603,210 @@ public class MediaQualityService extends SystemService { pictureParams.add(PictureParameter.colorTemperatureBlueGain(params.getInt( PictureQuality.PARAMETER_COLOR_TUNER_BLUE_GAIN))); } - - /** - * TODO: add conversion for following after adding to MediaQualityContract - * - * PictureParameter.levelRange - * PictureParameter.gamutMapping - * PictureParameter.pcMode - * PictureParameter.lowLatency - * PictureParameter.vrr - * PictureParameter.cvrr - * PictureParameter.hdmiRgbRange - * PictureParameter.colorSpace - * PictureParameter.panelInitMaxLuminceNits - * PictureParameter.panelInitMaxLuminceValid - * PictureParameter.gamma - * PictureParameter.colorTemperatureRedOffset - * PictureParameter.colorTemperatureGreenOffset - * PictureParameter.colorTemperatureBlueOffset - * PictureParameter.elevenPointRed - * PictureParameter.elevenPointGreen - * PictureParameter.elevenPointBlue - * PictureParameter.lowBlueLight - * PictureParameter.LdMode - * PictureParameter.osdRedGain - * PictureParameter.osdGreenGain - * PictureParameter.osdBlueGain - * PictureParameter.osdRedOffset - * PictureParameter.osdGreenOffset - * PictureParameter.osdBlueOffset - * PictureParameter.osdHue - * PictureParameter.osdSaturation - * PictureParameter.osdContrast - * PictureParameter.colorTunerSwitch - * PictureParameter.colorTunerHueRed - * PictureParameter.colorTunerHueGreen - * PictureParameter.colorTunerHueBlue - * PictureParameter.colorTunerHueCyan - * PictureParameter.colorTunerHueMagenta - * PictureParameter.colorTunerHueYellow - * PictureParameter.colorTunerHueFlesh - * PictureParameter.colorTunerSaturationRed - * PictureParameter.colorTunerSaturationGreen - * PictureParameter.colorTunerSaturationBlue - * PictureParameter.colorTunerSaturationCyan - * PictureParameter.colorTunerSaturationMagenta - * PictureParameter.colorTunerSaturationYellow - * PictureParameter.colorTunerSaturationFlesh - * PictureParameter.colorTunerLuminanceRed - * PictureParameter.colorTunerLuminanceGreen - * PictureParameter.colorTunerLuminanceBlue - * PictureParameter.colorTunerLuminanceCyan - * PictureParameter.colorTunerLuminanceMagenta - * PictureParameter.colorTunerLuminanceYellow - * PictureParameter.colorTunerLuminanceFlesh - * PictureParameter.activeProfile - * PictureParameter.pictureQualityEventType - */ + if (params.containsKey(PictureQuality.PARAMETER_LEVEL_RANGE)) { + pictureParams.add(PictureParameter.levelRange( + (byte) params.getInt(PictureQuality.PARAMETER_LEVEL_RANGE))); + } + if (params.containsKey(PictureQuality.PARAMETER_GAMUT_MAPPING)) { + pictureParams.add(PictureParameter.gamutMapping(params.getBoolean( + PictureQuality.PARAMETER_GAMUT_MAPPING))); + } + if (params.containsKey(PictureQuality.PARAMETER_PC_MODE)) { + pictureParams.add(PictureParameter.pcMode(params.getBoolean( + PictureQuality.PARAMETER_PC_MODE))); + } + if (params.containsKey(PictureQuality.PARAMETER_LOW_LATENCY)) { + pictureParams.add(PictureParameter.lowLatency(params.getBoolean( + PictureQuality.PARAMETER_LOW_LATENCY))); + } + if (params.containsKey(PictureQuality.PARAMETER_VRR)) { + pictureParams.add(PictureParameter.vrr(params.getBoolean( + PictureQuality.PARAMETER_VRR))); + } + if (params.containsKey(PictureQuality.PARAMETER_CVRR)) { + pictureParams.add(PictureParameter.cvrr(params.getBoolean( + PictureQuality.PARAMETER_CVRR))); + } + if (params.containsKey(PictureQuality.PARAMETER_HDMI_RGB_RANGE)) { + pictureParams.add(PictureParameter.hdmiRgbRange( + (byte) params.getInt(PictureQuality.PARAMETER_HDMI_RGB_RANGE))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_SPACE)) { + pictureParams.add(PictureParameter.colorSpace( + (byte) params.getInt(PictureQuality.PARAMETER_COLOR_SPACE))); + } + if (params.containsKey(PictureQuality.PARAMETER_PANEL_INIT_MAX_LUMINCE_NITS)) { + pictureParams.add(PictureParameter.panelInitMaxLuminceNits( + params.getInt(PictureQuality.PARAMETER_PANEL_INIT_MAX_LUMINCE_NITS))); + } + if (params.containsKey(PictureQuality.PARAMETER_PANEL_INIT_MAX_LUMINCE_VALID)) { + pictureParams.add(PictureParameter.panelInitMaxLuminceValid( + params.getBoolean(PictureQuality.PARAMETER_PANEL_INIT_MAX_LUMINCE_VALID))); + } + if (params.containsKey(PictureQuality.PARAMETER_GAMMA)) { + pictureParams.add(PictureParameter.gamma( + (byte) params.getInt(PictureQuality.PARAMETER_GAMMA))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TEMPERATURE_RED_OFFSET)) { + pictureParams.add(PictureParameter.colorTemperatureRedOffset(params.getInt( + PictureQuality.PARAMETER_COLOR_TEMPERATURE_RED_OFFSET))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TEMPERATURE_GREEN_OFFSET)) { + pictureParams.add(PictureParameter.colorTemperatureGreenOffset(params.getInt( + PictureQuality.PARAMETER_COLOR_TEMPERATURE_GREEN_OFFSET))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TEMPERATURE_BLUE_OFFSET)) { + pictureParams.add(PictureParameter.colorTemperatureBlueOffset(params.getInt( + PictureQuality.PARAMETER_COLOR_TEMPERATURE_BLUE_OFFSET))); + } + if (params.containsKey(PictureQuality.PARAMETER_ELEVEN_POINT_RED)) { + pictureParams.add(PictureParameter.elevenPointRed(params.getIntArray( + PictureQuality.PARAMETER_ELEVEN_POINT_RED))); + } + if (params.containsKey(PictureQuality.PARAMETER_ELEVEN_POINT_GREEN)) { + pictureParams.add(PictureParameter.elevenPointGreen(params.getIntArray( + PictureQuality.PARAMETER_ELEVEN_POINT_GREEN))); + } + if (params.containsKey(PictureQuality.PARAMETER_ELEVEN_POINT_BLUE)) { + pictureParams.add(PictureParameter.elevenPointBlue(params.getIntArray( + PictureQuality.PARAMETER_ELEVEN_POINT_BLUE))); + } + if (params.containsKey(PictureQuality.PARAMETER_LOW_BLUE_LIGHT)) { + pictureParams.add(PictureParameter.lowBlueLight( + (byte) params.getInt(PictureQuality.PARAMETER_LOW_BLUE_LIGHT))); + } + if (params.containsKey(PictureQuality.PARAMETER_LD_MODE)) { + pictureParams.add(PictureParameter.LdMode( + (byte) params.getInt(PictureQuality.PARAMETER_LD_MODE))); + } + if (params.containsKey(PictureQuality.PARAMETER_OSD_RED_GAIN)) { + pictureParams.add(PictureParameter.osdRedGain(params.getInt( + PictureQuality.PARAMETER_OSD_RED_GAIN))); + } + if (params.containsKey(PictureQuality.PARAMETER_OSD_GREEN_GAIN)) { + pictureParams.add(PictureParameter.osdGreenGain(params.getInt( + PictureQuality.PARAMETER_OSD_GREEN_GAIN))); + } + if (params.containsKey(PictureQuality.PARAMETER_OSD_BLUE_GAIN)) { + pictureParams.add(PictureParameter.osdBlueGain(params.getInt( + PictureQuality.PARAMETER_OSD_BLUE_GAIN))); + } + if (params.containsKey(PictureQuality.PARAMETER_OSD_RED_OFFSET)) { + pictureParams.add(PictureParameter.osdRedOffset(params.getInt( + PictureQuality.PARAMETER_OSD_RED_OFFSET))); + } + if (params.containsKey(PictureQuality.PARAMETER_OSD_GREEN_OFFSET)) { + pictureParams.add(PictureParameter.osdGreenOffset(params.getInt( + PictureQuality.PARAMETER_OSD_GREEN_OFFSET))); + } + if (params.containsKey(PictureQuality.PARAMETER_OSD_BLUE_OFFSET)) { + pictureParams.add(PictureParameter.osdBlueOffset(params.getInt( + PictureQuality.PARAMETER_OSD_BLUE_OFFSET))); + } + if (params.containsKey(PictureQuality.PARAMETER_OSD_HUE)) { + pictureParams.add(PictureParameter.osdHue(params.getInt( + PictureQuality.PARAMETER_OSD_HUE))); + } + if (params.containsKey(PictureQuality.PARAMETER_OSD_SATURATION)) { + pictureParams.add(PictureParameter.osdSaturation(params.getInt( + PictureQuality.PARAMETER_OSD_SATURATION))); + } + if (params.containsKey(PictureQuality.PARAMETER_OSD_CONTRAST)) { + pictureParams.add(PictureParameter.osdContrast(params.getInt( + PictureQuality.PARAMETER_OSD_CONTRAST))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SWITCH)) { + pictureParams.add(PictureParameter.colorTunerSwitch(params.getBoolean( + PictureQuality.PARAMETER_COLOR_TUNER_SWITCH))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_HUE_RED)) { + pictureParams.add(PictureParameter.colorTunerHueRed(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_HUE_RED))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_HUE_GREEN)) { + pictureParams.add(PictureParameter.colorTunerHueGreen(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_HUE_GREEN))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_HUE_BLUE)) { + pictureParams.add(PictureParameter.colorTunerHueBlue(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_HUE_BLUE))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_HUE_CYAN)) { + pictureParams.add(PictureParameter.colorTunerHueCyan(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_HUE_CYAN))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_HUE_MAGENTA)) { + pictureParams.add(PictureParameter.colorTunerHueMagenta(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_HUE_MAGENTA))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_HUE_YELLOW)) { + pictureParams.add(PictureParameter.colorTunerHueYellow(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_HUE_YELLOW))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_HUE_FLESH)) { + pictureParams.add(PictureParameter.colorTunerHueFlesh(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_HUE_FLESH))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_RED)) { + pictureParams.add(PictureParameter.colorTunerSaturationRed(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_RED))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_GREEN)) { + pictureParams.add(PictureParameter.colorTunerSaturationGreen(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_GREEN))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_BLUE)) { + pictureParams.add(PictureParameter.colorTunerSaturationBlue(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_BLUE))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_CYAN)) { + pictureParams.add(PictureParameter.colorTunerSaturationCyan(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_CYAN))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_MAGENTA)) { + pictureParams.add(PictureParameter.colorTunerSaturationMagenta(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_MAGENTA))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_YELLOW)) { + pictureParams.add(PictureParameter.colorTunerSaturationYellow(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_YELLOW))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_FLESH)) { + pictureParams.add(PictureParameter.colorTunerSaturationFlesh(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_FLESH))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_RED)) { + pictureParams.add(PictureParameter.colorTunerLuminanceRed(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_RED))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_GREEN)) { + pictureParams.add(PictureParameter.colorTunerLuminanceGreen(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_GREEN))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_BLUE)) { + pictureParams.add(PictureParameter.colorTunerLuminanceBlue(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_BLUE))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_CYAN)) { + pictureParams.add(PictureParameter.colorTunerLuminanceCyan(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_CYAN))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_MAGENTA)) { + pictureParams.add(PictureParameter.colorTunerLuminanceMagenta(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_MAGENTA))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_YELLOW)) { + pictureParams.add(PictureParameter.colorTunerLuminanceYellow(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_YELLOW))); + } + if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_FLESH)) { + pictureParams.add(PictureParameter.colorTunerLuminanceFlesh(params.getInt( + PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_FLESH))); + } + if (params.containsKey(PictureQuality.PARAMETER_PICTURE_QUALITY_EVENT_TYPE)) { + pictureParams.add(PictureParameter.pictureQualityEventType( + (byte) params.getInt(PictureQuality.PARAMETER_PICTURE_QUALITY_EVENT_TYPE))); + } return (PictureParameter[]) pictureParams.toArray(); } @@ -606,6 +881,28 @@ public class MediaQualityService extends SystemService { return sp; } + private void notifyHalOnSoundProfileChange(Long dbId, PersistableBundle params) { + // TODO: only notify HAL when the profile is active / being used + try { + mSpChangedListener.onSoundProfileChanged(convertToHalSoundProfile(dbId, params)); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to notify HAL on sound profile change.", e); + } + } + + private android.hardware.tv.mediaquality.SoundProfile convertToHalSoundProfile(Long id, + PersistableBundle params) { + SoundParameters soundParameters = new SoundParameters(); + soundParameters.soundParameters = convertPersistableBundleToSoundParameterList(params); + + android.hardware.tv.mediaquality.SoundProfile toReturn = + new android.hardware.tv.mediaquality.SoundProfile(); + toReturn.soundProfileId = id; + toReturn.parameters = soundParameters; + + return toReturn; + } + @Override public void updateSoundProfile(String id, SoundProfile sp, UserHandle user) { Long dbId = mSoundProfileTempIdMap.getKey(id); @@ -625,6 +922,7 @@ public class MediaQualityService extends SystemService { db.replace(mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, null, values); notifyOnSoundProfileUpdated(mSoundProfileTempIdMap.getValue(dbId), getSoundProfile(dbId), Binder.getCallingUid(), Binder.getCallingPid()); + notifyHalOnSoundProfileChange(dbId, sp.getParameters()); } private boolean hasPermissionToUpdateSoundProfile(Long dbId, SoundProfile sp) { @@ -657,6 +955,7 @@ public class MediaQualityService extends SystemService { notifyOnSoundProfileRemoved(mSoundProfileTempIdMap.getValue(dbId), toDelete, Binder.getCallingUid(), Binder.getCallingPid()); mSoundProfileTempIdMap.remove(dbId); + notifyHalOnSoundProfileChange(dbId, null); } } @@ -775,6 +1074,10 @@ public class MediaQualityService extends SystemService { private SoundParameter[] convertPersistableBundleToSoundParameterList( PersistableBundle params) { + //TODO: set EqualizerDetail + if (params == null) { + return null; + } List<SoundParameter> soundParams = new ArrayList<>(); if (params.containsKey(SoundQuality.PARAMETER_BALANCE)) { soundParams.add(SoundParameter.balance(params.getInt( @@ -811,15 +1114,50 @@ public class MediaQualityService extends SystemService { soundParams.add(SoundParameter.surroundSoundEnabled(params.getBoolean( SoundQuality.PARAMETER_DIGITAL_OUTPUT_DELAY_MILLIS))); } - //TODO: equalizerDetail - //TODO: downmixMode - //TODO: enhancedAudioReturnChannelEnabled - //TODO: dolbyAudioProcessing - //TODO: dolbyDialogueEnhancer - //TODO: dtsVirtualX - //TODO: digitalOutput - //TODO: activeProfile - //TODO: soundStyle + if (params.containsKey(SoundQuality.PARAMETER_EARC)) { + soundParams.add(SoundParameter.enhancedAudioReturnChannelEnabled(params.getBoolean( + SoundQuality.PARAMETER_EARC))); + } + if (params.containsKey(SoundQuality.PARAMETER_DOWN_MIX_MODE)) { + soundParams.add(SoundParameter.downmixMode((byte) params.getInt( + SoundQuality.PARAMETER_DOWN_MIX_MODE))); + } + if (params.containsKey(SoundQuality.PARAMETER_SOUND_STYLE)) { + soundParams.add(SoundParameter.soundStyle((byte) params.getInt( + SoundQuality.PARAMETER_SOUND_STYLE))); + } + if (params.containsKey(SoundQuality.PARAMETER_DIGITAL_OUTPUT_MODE)) { + soundParams.add(SoundParameter.digitalOutput((byte) params.getInt( + SoundQuality.PARAMETER_DIGITAL_OUTPUT_MODE))); + } + if (params.containsKey(SoundQuality.PARAMETER_DIALOGUE_ENHANCER)) { + soundParams.add(SoundParameter.dolbyDialogueEnhancer((byte) params.getInt( + SoundQuality.PARAMETER_DIALOGUE_ENHANCER))); + } + + DolbyAudioProcessing dab = new DolbyAudioProcessing(); + dab.soundMode = + (byte) params.getInt(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SOUND_MODE); + dab.volumeLeveler = + params.getBoolean(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_VOLUME_LEVELER); + dab.surroundVirtualizer = params.getBoolean( + SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SURROUND_VIRTUALIZER); + dab.dolbyAtmos = + params.getBoolean(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_DOLBY_ATMOS); + soundParams.add(SoundParameter.dolbyAudioProcessing(dab)); + + DtsVirtualX dts = new DtsVirtualX(); + dts.tbHdx = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TBHDX); + dts.limiter = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_LIMITER); + dts.truSurroundX = params.getBoolean( + SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_SURROUND_X); + dts.truVolumeHd = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_VOLUME_HD); + dts.dialogClarity = params.getBoolean( + SoundQuality.PARAMETER_DTS_VIRTUAL_X_DIALOG_CLARITY); + dts.definition = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_DEFINITION); + dts.height = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_HEIGHT); + soundParams.add(SoundParameter.dtsVirtualX(dts)); + return (SoundParameter[]) soundParams.toArray(); } @@ -1472,7 +1810,13 @@ public class MediaQualityService extends SystemService { RemoteCallbackList<IPictureProfileCallback> { @Override public void onCallbackDied(IPictureProfileCallback callback) { - //todo + synchronized ("mPictureProfileLock") { //TODO: Change to lock + for (int i = 0; i < mUserStates.size(); i++) { + int userId = mUserStates.keyAt(i); + UserState userState = getOrCreateUserStateLocked(userId); + userState.mPictureProfileCallbackPidUidMap.remove(callback); + } + } } } @@ -1480,7 +1824,13 @@ public class MediaQualityService extends SystemService { RemoteCallbackList<ISoundProfileCallback> { @Override public void onCallbackDied(ISoundProfileCallback callback) { - //todo + synchronized ("mSoundProfileLock") { //TODO: Change to lock + for (int i = 0; i < mUserStates.size(); i++) { + int userId = mUserStates.keyAt(i); + UserState userState = getOrCreateUserStateLocked(userId); + userState.mSoundProfileCallbackPidUidMap.remove(callback); + } + } } } diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java index 7de2815eba6b..1d376b4bdfd5 100644 --- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java +++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java @@ -3612,13 +3612,16 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { final long token = Binder.clearCallingIdentity(); try { config = mCarrierConfigManager.getConfigForSubId(subId); - tm = mContext.getSystemService(TelephonyManager.class); + tm = mContext.getSystemService(TelephonyManager.class).createForSubscriptionId(subId); } finally { Binder.restoreCallingIdentity(token); } - // First check: does caller have carrier privilege? - if (tm != null && tm.hasCarrierPrivileges(subId)) { + // First check: does callingPackage have carrier privilege? + // Note that we can't call TelephonyManager.hasCarrierPrivileges() which will check if + // ourself has carrier privileges + if (tm != null && (tm.checkCarrierPrivilegesForPackage(callingPackage) + == TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS)) { return; } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index daa1042bb255..b4a58ac72394 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -481,7 +481,8 @@ public class NotificationManagerService extends SystemService { Adjustment.KEY_SENSITIVE_CONTENT, Adjustment.KEY_RANKING_SCORE, Adjustment.KEY_NOT_CONVERSATION, - Adjustment.KEY_TYPE + Adjustment.KEY_TYPE, + Adjustment.KEY_SUMMARIZATION }; static final Integer[] DEFAULT_ALLOWED_ADJUSTMENT_KEY_TYPES = new Integer[] { @@ -631,6 +632,8 @@ public class NotificationManagerService extends SystemService { // Minium number of sparse groups for a package before autogrouping them private static final int AUTOGROUP_SPARSE_GROUPS_AT_COUNT = 3; + private static final Duration ZEN_BROADCAST_DELAY = Duration.ofMillis(250); + private IActivityManager mAm; private ActivityTaskManagerInternal mAtm; private ActivityManager mActivityManager; @@ -3169,6 +3172,24 @@ public class NotificationManagerService extends SystemService { sendRegisteredOnlyBroadcast(new Intent(action)); } + /** + * Schedules a broadcast to be sent to runtime receivers and DND-policy-access packages. The + * broadcast will be sent after {@link #ZEN_BROADCAST_DELAY}, unless a new broadcast is + * scheduled in the interim, in which case the previous one is dropped and the waiting period + * is <em>restarted</em>. + * + * <p>Note that this uses <em>equality of the {@link Intent#getAction}</em> as the criteria for + * deduplicating pending broadcasts, ignoring the extras and anything else. This is intentional + * so that e.g. rapidly changing some value A -> B -> C will only produce a broadcast for C + * (instead of every time because the extras are different). + */ + private void sendZenBroadcastWithDelay(Intent intent) { + String token = "zen_broadcast:" + intent.getAction(); + mHandler.removeCallbacksAndEqualMessages(token); + mHandler.postDelayed(() -> sendRegisteredOnlyBroadcast(intent), token, + ZEN_BROADCAST_DELAY.toMillis()); + } + private void sendRegisteredOnlyBroadcast(Intent baseIntent) { int[] userIds = mUmInternal.getProfileIds(mAmi.getCurrentUserId(), true); if (Flags.nmBinderPerfReduceZenBroadcasts()) { @@ -3362,14 +3383,25 @@ public class NotificationManagerService extends SystemService { @GuardedBy("mNotificationLock") private void updateEffectsSuppressorLocked() { + final long oldSuppressedEffects = mZenModeHelper.getSuppressedEffects(); final long updatedSuppressedEffects = calculateSuppressedEffects(); - if (updatedSuppressedEffects == mZenModeHelper.getSuppressedEffects()) return; + if (updatedSuppressedEffects == oldSuppressedEffects) return; + final List<ComponentName> suppressors = getSuppressors(); ZenLog.traceEffectsSuppressorChanged( - mEffectsSuppressors, suppressors, updatedSuppressedEffects); - mEffectsSuppressors = suppressors; + mEffectsSuppressors, suppressors, oldSuppressedEffects, updatedSuppressedEffects); mZenModeHelper.setSuppressedEffects(updatedSuppressedEffects); - sendRegisteredOnlyBroadcast(NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED); + + if (Flags.nmBinderPerfThrottleEffectsSuppressorBroadcast()) { + if (!suppressors.equals(mEffectsSuppressors)) { + mEffectsSuppressors = suppressors; + sendZenBroadcastWithDelay( + new Intent(NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED)); + } + } else { + mEffectsSuppressors = suppressors; + sendRegisteredOnlyBroadcast(NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED); + } } private void exitIdle() { @@ -3491,12 +3523,18 @@ public class NotificationManagerService extends SystemService { } private ArrayList<ComponentName> getSuppressors() { - ArrayList<ComponentName> names = new ArrayList<ComponentName>(); + ArrayList<ComponentName> names = new ArrayList<>(); for (int i = mListenersDisablingEffects.size() - 1; i >= 0; --i) { ArraySet<ComponentName> serviceInfoList = mListenersDisablingEffects.valueAt(i); for (ComponentName info : serviceInfoList) { - names.add(info); + if (Flags.nmBinderPerfThrottleEffectsSuppressorBroadcast()) { + if (!names.contains(info)) { + names.add(info); + } + } else { + names.add(info); + } } } @@ -5018,14 +5056,8 @@ public class NotificationManagerService extends SystemService { } @Override - public ParceledListSlice<NotificationChannel> getNotificationChannels( - String callingPkg, String targetPkg, int userId) { - return getOrCreateNotificationChannels(callingPkg, targetPkg, userId, false); - } - - @Override - public ParceledListSlice<NotificationChannel> getOrCreateNotificationChannels( - String callingPkg, String targetPkg, int userId, boolean createPrefsIfNeeded) { + public ParceledListSlice<NotificationChannel> getNotificationChannels(String callingPkg, + String targetPkg, int userId) { if (canNotifyAsPackage(callingPkg, targetPkg, userId) || isCallingUidSystem()) { int targetUid = -1; @@ -5035,8 +5067,7 @@ public class NotificationManagerService extends SystemService { /* ignore */ } return mPreferencesHelper.getNotificationChannels( - targetPkg, targetUid, false /* includeDeleted */, true, - createPrefsIfNeeded); + targetPkg, targetUid, false /* includeDeleted */, true); } throw new SecurityException("Pkg " + callingPkg + " cannot read channels for " + targetPkg + " in " + userId); @@ -7212,7 +7243,7 @@ public class NotificationManagerService extends SystemService { if (!mAssistants.isAdjustmentAllowed(potentialKey)) { toRemove.add(potentialKey); } - if (notificationClassification() && adjustments.containsKey(KEY_TYPE)) { + if (notificationClassification() && potentialKey.equals(KEY_TYPE)) { mAssistants.setNasUnsupportedDefaults(r.getSbn().getNormalizedUserId()); if (!mAssistants.isAdjustmentKeyTypeAllowed(adjustments.getInt(KEY_TYPE))) { toRemove.add(potentialKey); @@ -8467,6 +8498,9 @@ public class NotificationManagerService extends SystemService { (userId == USER_ALL) ? USER_SYSTEM : userId); Notification.addFieldsFromContext(ai, notification); + // can't be set by an app + notification.extras.remove(Notification.EXTRA_SUMMARIZED_CONTENT); + if (notification.isForegroundService() && fgsPolicy == NOT_FOREGROUND_SERVICE) { notification.flags &= ~FLAG_FOREGROUND_SERVICE; } @@ -10395,7 +10429,8 @@ public class NotificationManagerService extends SystemService { r.getRankingScore(), r.isConversation(), r.getProposedImportance(), - r.hasSensitiveContent()); + r.hasSensitiveContent(), + r.getSummarization()); extractorDataBefore.put(r.getKey(), extractorData); mRankingHelper.extractSignals(r); } @@ -11715,7 +11750,8 @@ public class NotificationManagerService extends SystemService { : (record.getRankingScore() > 0 ? RANKING_PROMOTED : RANKING_DEMOTED), record.getNotification().isBubbleNotification(), record.getProposedImportance(), - hasSensitiveContent + hasSensitiveContent, + record.getSummarization() ); rankings.add(ranking); } diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index 81af0d8a6d80..52101e336920 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -25,6 +25,7 @@ import static android.app.NotificationManager.IMPORTANCE_HIGH; import static android.app.NotificationManager.IMPORTANCE_LOW; import static android.app.NotificationManager.IMPORTANCE_MIN; import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; +import static android.service.notification.Adjustment.KEY_SUMMARIZATION; import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEUTRAL; import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_POSITIVE; @@ -225,6 +226,8 @@ public final class NotificationRecord { // type of the bundle if the notification was classified private @Adjustment.Types int mBundleType = Adjustment.TYPE_OTHER; + private String mSummarization = null; + public NotificationRecord(Context context, StatusBarNotification sbn, NotificationChannel channel) { this.sbn = sbn; @@ -589,6 +592,7 @@ public final class NotificationRecord { pw.println(prefix + "shortcut=" + notification.getShortcutId() + " found valid? " + (mShortcutInfo != null)); pw.println(prefix + "mUserVisOverride=" + getPackageVisibilityOverride()); + pw.println(prefix + "hasSummarization=" + (mSummarization != null)); } private void dumpNotification(PrintWriter pw, String prefix, Notification notification, @@ -811,6 +815,12 @@ public final class NotificationRecord { Adjustment.KEY_TYPE, mChannel.getId()); } + if ((android.app.Flags.nmSummarizationUi() || android.app.Flags.nmSummarization()) + && signals.containsKey(KEY_SUMMARIZATION)) { + mSummarization = signals.getString(KEY_SUMMARIZATION); + EventLogTags.writeNotificationAdjusted(getKey(), + KEY_SUMMARIZATION, Boolean.toString(mSummarization != null)); + } if (!signals.isEmpty() && adjustment.getIssuer() != null) { mAdjustmentIssuer = adjustment.getIssuer(); } @@ -983,6 +993,13 @@ public final class NotificationRecord { return null; } + public String getSummarization() { + if ((android.app.Flags.nmSummarizationUi() || android.app.Flags.nmSummarization())) { + return mSummarization; + } + return null; + } + public boolean setIntercepted(boolean intercept) { mIntercept = intercept; mInterceptSet = true; diff --git a/services/core/java/com/android/server/notification/NotificationRecordExtractorData.java b/services/core/java/com/android/server/notification/NotificationRecordExtractorData.java index 3f4f7d3bbc38..9315ddc0d5b0 100644 --- a/services/core/java/com/android/server/notification/NotificationRecordExtractorData.java +++ b/services/core/java/com/android/server/notification/NotificationRecordExtractorData.java @@ -47,6 +47,7 @@ public final class NotificationRecordExtractorData { private final boolean mIsConversation; private final int mProposedImportance; private final boolean mSensitiveContent; + private final String mSummarization; NotificationRecordExtractorData(int position, int visibility, boolean showBadge, boolean allowBubble, boolean isBubble, NotificationChannel channel, String groupKey, @@ -54,7 +55,8 @@ public final class NotificationRecordExtractorData { Integer userSentiment, Integer suppressVisually, ArrayList<Notification.Action> systemSmartActions, ArrayList<CharSequence> smartReplies, int importance, float rankingScore, - boolean isConversation, int proposedImportance, boolean sensitiveContent) { + boolean isConversation, int proposedImportance, boolean sensitiveContent, + String summarization) { mPosition = position; mVisibility = visibility; mShowBadge = showBadge; @@ -73,6 +75,7 @@ public final class NotificationRecordExtractorData { mIsConversation = isConversation; mProposedImportance = proposedImportance; mSensitiveContent = sensitiveContent; + mSummarization = summarization; } // Returns whether the provided NotificationRecord differs from the cached data in any way. @@ -93,7 +96,8 @@ public final class NotificationRecordExtractorData { || !Objects.equals(mSmartReplies, r.getSmartReplies()) || mImportance != r.getImportance() || mProposedImportance != r.getProposedImportance() - || mSensitiveContent != r.hasSensitiveContent(); + || mSensitiveContent != r.hasSensitiveContent() + || !Objects.equals(mSummarization, r.getSummarization()); } // Returns whether the NotificationRecord has a change from this data for which we should @@ -117,6 +121,7 @@ public final class NotificationRecordExtractorData { || !r.rankingScoreMatches(mRankingScore) || mIsConversation != r.isConversation() || mProposedImportance != r.getProposedImportance() - || mSensitiveContent != r.hasSensitiveContent(); + || mSensitiveContent != r.hasSensitiveContent() + || !Objects.equals(mSummarization, r.getSummarization()); } } diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 3b34dcd17705..14cc91b6305f 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -16,8 +16,8 @@ package com.android.server.notification; -import static android.app.Flags.notificationClassificationUi; import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW; +import static android.app.Flags.notificationClassificationUi; import static android.app.NotificationChannel.DEFAULT_CHANNEL_ID; import static android.app.NotificationChannel.NEWS_ID; import static android.app.NotificationChannel.PLACEHOLDER_CONVERSATION_ID; @@ -89,9 +89,10 @@ import android.util.SparseBooleanArray; import android.util.StatsEvent; import android.util.proto.ProtoOutputStream; +import androidx.annotation.VisibleForTesting; + import com.android.internal.R; import com.android.internal.annotations.GuardedBy; -import com.android.internal.annotations.VisibleForTesting; import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags; import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags; import com.android.internal.logging.MetricsLogger; @@ -1962,21 +1963,11 @@ public class PreferencesHelper implements RankingConfig { @Override public ParceledListSlice<NotificationChannel> getNotificationChannels(String pkg, int uid, boolean includeDeleted, boolean includeBundles) { - return getNotificationChannels(pkg, uid, includeDeleted, includeBundles, false); - } - - protected ParceledListSlice<NotificationChannel> getNotificationChannels(String pkg, int uid, - boolean includeDeleted, boolean includeBundles, boolean createPrefsIfNeeded) { - if (createPrefsIfNeeded && !android.app.Flags.nmBinderPerfCacheChannels()) { - Slog.wtf(TAG, - "getNotificationChannels called with createPrefsIfNeeded=true and flag off"); - createPrefsIfNeeded = false; - } Objects.requireNonNull(pkg); List<NotificationChannel> channels = new ArrayList<>(); synchronized (mLock) { PackagePreferences r; - if (createPrefsIfNeeded) { + if (android.app.Flags.nmBinderPerfCacheChannels()) { r = getOrCreatePackagePreferencesLocked(pkg, uid); } else { r = getPackagePreferencesLocked(pkg, uid); @@ -1997,6 +1988,18 @@ public class PreferencesHelper implements RankingConfig { } } + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + // Gets the entire list of notification channels for this package, with no filtering and + // without creating package preferences. For testing only, specifically to confirm the + // notification channels of a removed/deleted package. + protected List<NotificationChannel> getRemovedPkgNotificationChannels(String pkg, int uid) { + PackagePreferences r = getPackagePreferencesLocked(pkg, uid); + if (r == null || r.channels == null) { + return new ArrayList<>(); + } + return new ArrayList<>(r.channels.values()); + } + /** * Gets all notification channels associated with the given pkg and uid that can bypass dnd */ diff --git a/services/core/java/com/android/server/notification/ZenLog.java b/services/core/java/com/android/server/notification/ZenLog.java index 7e853d9d2d0b..49f93b8b7c16 100644 --- a/services/core/java/com/android/server/notification/ZenLog.java +++ b/services/core/java/com/android/server/notification/ZenLog.java @@ -140,8 +140,9 @@ public class ZenLog { } public static void traceEffectsSuppressorChanged(List<ComponentName> oldSuppressors, - List<ComponentName> newSuppressors, long suppressedEffects) { - append(TYPE_SUPPRESSOR_CHANGED, "suppressed effects:" + suppressedEffects + "," + List<ComponentName> newSuppressors, long oldSuppressedEffects, long suppressedEffects) { + append(TYPE_SUPPRESSOR_CHANGED, "suppressed effects:" + + oldSuppressedEffects + "->" + suppressedEffects + "," + componentListToString(oldSuppressors) + "->" + componentListToString(newSuppressors)); } diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig index 822ff48c831c..048f2b6b0cbc 100644 --- a/services/core/java/com/android/server/notification/flags.aconfig +++ b/services/core/java/com/android/server/notification/flags.aconfig @@ -182,6 +182,16 @@ flag { } flag { + name: "nm_binder_perf_throttle_effects_suppressor_broadcast" + namespace: "systemui" + description: "Delay sending the ACTION_EFFECTS_SUPPRESSOR_CHANGED broadcast if it changes too often" + bug: "371776935" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "fix_calling_uid_from_cps" namespace: "systemui" description: "Correctly checks zen rule ownership when a CPS notifies with a Condition" diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java index 0b58c759b284..f011d283c8bb 100644 --- a/services/core/java/com/android/server/pm/DexOptHelper.java +++ b/services/core/java/com/android/server/pm/DexOptHelper.java @@ -96,6 +96,9 @@ import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.function.Predicate; @@ -105,6 +108,11 @@ import java.util.function.Predicate; public final class DexOptHelper { private static final long SEVEN_DAYS_IN_MILLISECONDS = 7 * 24 * 60 * 60 * 1000; + @NonNull + private static final ThreadPoolExecutor sDexoptExecutor = + new ThreadPoolExecutor(1 /* corePoolSize */, 1 /* maximumPoolSize */, + 60 /* keepAliveTime */, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); + private static boolean sArtManagerLocalIsInitialized = false; private final PackageManagerService mPm; @@ -113,6 +121,11 @@ public final class DexOptHelper { // used, to make it available to the onDexoptDone callback. private volatile long mBootDexoptStartTime; + static { + // Recycle the thread if it's not used for `keepAliveTime`. + sDexoptExecutor.allowsCoreThreadTimeOut(); + } + DexOptHelper(PackageManagerService pm) { mPm = pm; } @@ -746,43 +759,11 @@ public final class DexOptHelper { */ static void performDexoptIfNeeded(InstallRequest installRequest, DexManager dexManager, Context context, PackageManagerTracedLock.RawLock installLock) { - // Construct the DexoptOptions early to see if we should skip running dexopt. - // - // Do not run PackageDexOptimizer through the local performDexOpt - // method because `pkg` may not be in `mPackages` yet. - // - // Also, don't fail application installs if the dexopt step fails. DexoptOptions dexoptOptions = getDexoptOptionsByInstallRequest(installRequest, dexManager); - // Check whether we need to dexopt the app. - // - // NOTE: it is IMPORTANT to call dexopt: - // - after doRename which will sync the package data from AndroidPackage and - // its corresponding ApplicationInfo. - // - after installNewPackageLIF or replacePackageLIF which will update result with the - // uid of the application (pkg.applicationInfo.uid). - // This update happens in place! - // - // We only need to dexopt if the package meets ALL of the following conditions: - // 1) it is not an instant app or if it is then dexopt is enabled via gservices. - // 2) it is not debuggable. - // 3) it is not on Incremental File System. - // - // Note that we do not dexopt instant apps by default. dexopt can take some time to - // complete, so we skip this step during installation. Instead, we'll take extra time - // the first time the instant app starts. It's preferred to do it this way to provide - // continuous progress to the useur instead of mysteriously blocking somewhere in the - // middle of running an instant app. The default behaviour can be overridden - // via gservices. - // - // Furthermore, dexopt may be skipped, depending on the install scenario and current - // state of the device. - // - // TODO(b/174695087): instantApp and onIncremental should be removed and their install - // path moved to SCENARIO_FAST. + boolean performDexopt = + DexOptHelper.shouldPerformDexopt(installRequest, dexoptOptions, context); - final boolean performDexopt = DexOptHelper.shouldPerformDexopt(installRequest, - dexoptOptions, context); if (performDexopt) { // dexopt can take long, and ArtService doesn't require installd, so we release // the lock here and re-acquire the lock after dexopt is finished. @@ -791,6 +772,7 @@ public final class DexOptHelper { } try { Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "dexopt"); + // Don't fail application installs if the dexopt step fails. DexoptResult dexOptResult = DexOptHelper.dexoptPackageUsingArtService( installRequest, dexoptOptions); installRequest.onDexoptFinished(dexOptResult); @@ -804,6 +786,41 @@ public final class DexOptHelper { } /** + * Same as above, but runs asynchronously. + */ + static CompletableFuture<Void> performDexoptIfNeededAsync(InstallRequest installRequest, + DexManager dexManager, Context context) { + // Construct the DexoptOptions early to see if we should skip running dexopt. + DexoptOptions dexoptOptions = getDexoptOptionsByInstallRequest(installRequest, dexManager); + boolean performDexopt = + DexOptHelper.shouldPerformDexopt(installRequest, dexoptOptions, context); + + if (performDexopt) { + return CompletableFuture + .runAsync(() -> { + try { + Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "dexopt"); + // Don't fail application installs if the dexopt step fails. + // TODO(jiakaiz): Make this async in ART Service. + DexoptResult dexOptResult = DexOptHelper.dexoptPackageUsingArtService( + installRequest, dexoptOptions); + installRequest.onDexoptFinished(dexOptResult); + } finally { + Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); + } + }, sDexoptExecutor) + .exceptionally((t) -> { + // This should never happen. A normal dexopt failure should result in a + // DexoptResult.DEXOPT_FAILED, not an exception. + Slog.wtf(TAG, "Dexopt encountered a fatal error", t); + return null; + }); + } else { + return CompletableFuture.completedFuture(null); + } + } + + /** * Use ArtService to perform dexopt by the given InstallRequest. */ static DexoptResult dexoptPackageUsingArtService(InstallRequest installRequest, @@ -840,6 +857,20 @@ public final class DexOptHelper { */ static boolean shouldPerformDexopt(InstallRequest installRequest, DexoptOptions dexoptOptions, Context context) { + // We only need to dexopt if the package meets ALL of the following conditions: + // 1) it is not an instant app or if it is then dexopt is enabled via gservices. + // 2) it is not debuggable. + // 3) it is not on Incremental File System. + // + // Note that we do not dexopt instant apps by default. dexopt can take some time to + // complete, so we skip this step during installation. Instead, we'll take extra time + // the first time the instant app starts. It's preferred to do it this way to provide + // continuous progress to the user instead of mysteriously blocking somewhere in the + // middle of running an instant app. The default behaviour can be overridden + // via gservices. + // + // Furthermore, dexopt may be skipped, depending on the install scenario and current + // state of the device. final boolean isApex = ((installRequest.getScanFlags() & SCAN_AS_APEX) != 0); final boolean instantApp = ((installRequest.getScanFlags() & SCAN_AS_INSTANT_APP) != 0); final PackageSetting ps = installRequest.getScannedPackageSetting(); diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 8eb5b6f11cb2..0c2782393879 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -1158,6 +1158,7 @@ final class InstallPackageHelper { return; } request.setKeepArtProfile(true); + // TODO(b/388159696): Use performDexoptIfNeededAsync. DexOptHelper.performDexoptIfNeeded(request, mDexManager, mContext, null); } } diff --git a/services/core/java/com/android/server/pm/UserManagerInternal.java b/services/core/java/com/android/server/pm/UserManagerInternal.java index 14b0fc81fdd2..c62aaebf673b 100644 --- a/services/core/java/com/android/server/pm/UserManagerInternal.java +++ b/services/core/java/com/android/server/pm/UserManagerInternal.java @@ -584,6 +584,12 @@ public abstract class UserManagerInternal { * Returns the user id of the main user, or {@link android.os.UserHandle#USER_NULL} if there is * no main user. * + * <p>NB: Features should ideally not limit functionality to the main user. Ideally, they + * should either work for all users or for all admin users. If a feature should only work for + * select users, its determination of which user should be done intelligently or be + * customizable. Not all devices support a main user, and the idea of singling out one user as + * special is contrary to overall multiuser goals. + * * @see UserManager#isMainUser() */ public abstract @UserIdInt int getMainUserId(); diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index a2c53e56b9c9..8cbccf5feead 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -3590,8 +3590,6 @@ public class UserManagerService extends IUserManager.Stub { } /** - * @hide - * * Returns who set a user restriction on a user. * Requires {@link android.Manifest.permission#MANAGE_USERS} permission. * @param restrictionKey the string key representing the restriction @@ -6275,9 +6273,6 @@ public class UserManagerService extends IUserManager.Stub { } } - /** - * @hide - */ @Override public @NonNull UserInfo createRestrictedProfileWithThrow( @Nullable String name, @UserIdInt int parentUserId) @@ -8504,7 +8499,6 @@ public class UserManagerService extends IUserManager.Stub { } /** - * @hide * Checks whether to show a notification for sounds (e.g., alarms, timers, etc.) from * background users. */ diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 7f511e1e2aa1..283979483e73 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -3801,10 +3801,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { true /* leftOrTop */); notifyKeyGestureCompleted(event, KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT); - } else if (event.isAltPressed()) { - setSplitscreenFocus(true /* leftOrTop */); - notifyKeyGestureCompleted(event, - KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT); } else { notifyKeyGestureCompleted(event, KeyGestureEvent.KEY_GESTURE_TYPE_BACK); @@ -3821,11 +3817,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { notifyKeyGestureCompleted(event, KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT); return true; - } else if (event.isAltPressed()) { - setSplitscreenFocus(false /* leftOrTop */); - notifyKeyGestureCompleted(event, - KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT); - return true; } } break; @@ -4241,9 +4232,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { case KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION: case KeyGestureEvent.KEY_GESTURE_TYPE_DESKTOP_MODE: case KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT: - case KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT: case KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT: - case KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT: case KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER: case KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_UP: case KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN: @@ -4379,22 +4368,12 @@ public class PhoneWindowManager implements WindowManagerPolicy { true /* leftOrTop */); } return true; - case KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT: - if (complete) { - setSplitscreenFocus(true /* leftOrTop */); - } - return true; case KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT: if (complete) { moveFocusedTaskToStageSplit(getTargetDisplayIdForKeyGestureEvent(event), false /* leftOrTop */); } return true; - case KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT: - if (complete) { - setSplitscreenFocus(false /* leftOrTop */); - } - return true; case KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER: if (complete) { toggleKeyboardShortcutsMenu(deviceId); @@ -5084,13 +5063,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } - private void setSplitscreenFocus(boolean leftOrTop) { - StatusBarManagerInternal statusbar = getStatusBarManagerInternal(); - if (statusbar != null) { - statusbar.setSplitscreenFocus(leftOrTop); - } - } - void launchHomeFromHotKey(int displayId) { launchHomeFromHotKey(displayId, true /* awakenFromDreams */, true /*respectKeyguard*/); } diff --git a/services/core/java/com/android/server/policy/SingleKeyGestureDetector.java b/services/core/java/com/android/server/policy/SingleKeyGestureDetector.java index 441d3eaf2348..142d919da455 100644 --- a/services/core/java/com/android/server/policy/SingleKeyGestureDetector.java +++ b/services/core/java/com/android/server/policy/SingleKeyGestureDetector.java @@ -24,6 +24,8 @@ import android.util.Log; import android.view.KeyEvent; import android.view.ViewConfiguration; +import com.android.hardware.input.Flags; + import java.io.PrintWriter; import java.util.ArrayList; @@ -355,6 +357,19 @@ public final class SingleKeyGestureDetector { } if (event.getKeyCode() == mActiveRule.mKeyCode) { + if (Flags.abortSlowMultiPress() + && (event.getEventTime() - mLastDownTime + >= mActiveRule.getLongPressTimeoutMs())) { + // In this case, we are either on a first long press (but long press behavior is not + // supported for this rule), or, on a non-first press that is at least as long as + // the long-press duration. Thus, we will cancel the multipress gesture. + if (DEBUG) { + Log.d(TAG, "The duration of the press is too slow. Resetting."); + } + reset(); + return false; + } + // key-up action should always be triggered if not processed by long press. MessageObject object = new MessageObject(mActiveRule, mActiveRule.mKeyCode, mKeyPressCounter, event); diff --git a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java index 977c6db66106..a5185a2139db 100644 --- a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java +++ b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java @@ -38,6 +38,7 @@ import com.android.internal.os.CpuScalingPolicies; import com.android.internal.os.MonotonicClock; import com.android.internal.os.PowerProfile; import com.android.internal.util.ArrayUtils; +import com.android.server.power.optimization.Flags; import com.android.server.power.stats.BatteryStatsImpl.BatteryStatsSession; import java.io.PrintWriter; @@ -351,7 +352,7 @@ public class BatteryUsageStatsProvider { accumulatedStats.endMonotonicTime = endMonotonicTime; accumulatedStats.builder.setStatsEndTimestamp(endWallClockTime); - accumulatedStats.builder.setStatsDuration(endWallClockTime - startMonotonicTime); + accumulatedStats.builder.setStatsDuration(endMonotonicTime - startMonotonicTime); mPowerAttributor.estimatePowerConsumption(accumulatedStats.builder, session.getHistory(), startMonotonicTime, endMonotonicTime); @@ -403,7 +404,10 @@ public class BatteryUsageStatsProvider { } if ((query.getFlags() & BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_INCLUDE_HISTORY) != 0) { - batteryUsageStatsBuilder.setBatteryHistory(session.getHistory().copy()); + batteryUsageStatsBuilder.setBatteryHistory(session.getHistory().copy(), + Flags.extendedBatteryHistoryContinuousCollectionEnabled() + ? query.getPreferredHistoryDurationMs() + : Long.MAX_VALUE); } mPowerAttributor.estimatePowerConsumption(batteryUsageStatsBuilder, session.getHistory(), diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java index 4827c9f414ad..0ed522805bef 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java @@ -56,12 +56,12 @@ public interface StatusBarManagerInternal { void toggleKeyboardShortcutsMenu(int deviceId); /** - * Used by InputMethodManagerService to notify the IME status. + * Sets the new IME window status. * - * @param displayId The display to which the IME is bound to. - * @param vis The IME visibility. - * @param backDisposition The IME back disposition. - * @param showImeSwitcher {@code true} when the IME switcher button should be shown. + * @param displayId The id of the display to which the IME is bound. + * @param vis The IME window visibility. + * @param backDisposition The IME back disposition mode. + * @param showImeSwitcher Whether the IME Switcher button should be shown. */ void setImeWindowStatus(int displayId, @ImeWindowVisibility int vis, @BackDispositionMode int backDisposition, boolean showImeSwitcher); diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerInternal.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerInternal.java index 25c07500b891..872ab595994b 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerInternal.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerInternal.java @@ -28,6 +28,9 @@ public abstract class WallpaperManagerInternal { */ public abstract void onDisplayReady(int displayId); + /** Notifies when display stop showing system decorations and wallpaper. */ + public abstract void onDisplayRemoveSystemDecorations(int displayId); + /** Notifies when the screen finished turning on and is visible to the user. */ public abstract void onScreenTurnedOn(int displayId); diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index a8deeeac311d..db530e728a1a 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -118,6 +118,7 @@ import android.service.wallpaper.WallpaperService; import android.system.ErrnoException; import android.system.Os; import android.text.TextUtils; +import android.util.ArraySet; import android.util.EventLog; import android.util.IntArray; import android.util.Slog; @@ -160,6 +161,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.function.Consumer; import java.util.function.Predicate; @@ -665,71 +667,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub @Override public void onDisplayRemoved(int displayId) { - synchronized (mLock) { - if (enableConnectedDisplaysWallpaper()) { - // There could be at most 2 wallpaper connections per display: - // 1. system & lock are the same: mLastWallpaper - // 2. system, lock are different: mLastWallpaper, mLastLockWallpaper - // 3. fallback used as both system & lock wallpaper: mFallbackWallpaper - // 4. fallback used as lock only wallpaper: mFallbackWallpaper, - // mLastWallpaper - // 5. fallback used as system only wallpaper: mFallbackWallpaper, - // mLastLockWallpaper - List<WallpaperData> pendingDisconnectWallpapers = new ArrayList<>(); - if (mLastWallpaper != null && mLastWallpaper.connection != null - && mLastWallpaper.connection.containsDisplay(displayId)) { - pendingDisconnectWallpapers.add(mLastWallpaper); - } - if (mLastLockWallpaper != null && mLastLockWallpaper.connection != null - && mLastLockWallpaper.connection.containsDisplay(displayId)) { - pendingDisconnectWallpapers.add(mLastLockWallpaper); - } - if (mFallbackWallpaper != null && mFallbackWallpaper.connection != null - && mFallbackWallpaper.connection.containsDisplay(displayId)) { - pendingDisconnectWallpapers.add(mFallbackWallpaper); - } - for (int i = 0; i < pendingDisconnectWallpapers.size(); i++) { - WallpaperData wallpaper = pendingDisconnectWallpapers.get(i); - DisplayConnector displayConnector = - wallpaper.connection.getDisplayConnectorOrCreate(displayId); - if (displayConnector == null) { - Slog.w(TAG, - "Fail to disconnect wallpaper upon display removal"); - return; - } - displayConnector.disconnectLocked(wallpaper.connection); - wallpaper.connection.removeDisplayConnector(displayId); - } - } else { - if (mLastWallpaper != null) { - WallpaperData targetWallpaper = null; - if (mLastWallpaper.connection != null - && mLastWallpaper.connection.containsDisplay(displayId)) { - targetWallpaper = mLastWallpaper; - } else if (mFallbackWallpaper != null - && mFallbackWallpaper.connection != null - && mFallbackWallpaper.connection.containsDisplay( - displayId)) { - targetWallpaper = mFallbackWallpaper; - } - if (targetWallpaper == null) return; - DisplayConnector connector = - targetWallpaper.connection.getDisplayConnectorOrCreate( - displayId); - if (connector == null) return; - connector.disconnectLocked(targetWallpaper.connection); - targetWallpaper.connection.removeDisplayConnector(displayId); - } - } - - mWallpaperDisplayHelper.removeDisplayData(displayId); - - for (int i = mColorsChangedListeners.size() - 1; i >= 0; i--) { - final SparseArray<RemoteCallbackList<IWallpaperManagerCallback>> callbacks = - mColorsChangedListeners.valueAt(i); - callbacks.delete(displayId); - } - } + onDisplayRemovedInternal(displayId); } @Override @@ -772,6 +710,12 @@ public class WallpaperManagerService extends IWallpaperManager.Stub private final ComponentName mImageWallpaper; /** + * Name of the component that is used when the user-selected wallpaper is incompatible with the + * display's resolution or aspect ratio. + */ + @Nullable private final ComponentName mFallbackWallpaperComponent; + + /** * Default image wallpaper shall never changed after system service started, caching it when we * first read the image file. */ @@ -799,12 +743,38 @@ public class WallpaperManagerService extends IWallpaperManager.Stub final WallpaperDisplayHelper mWallpaperDisplayHelper; final WallpaperCropper mWallpaperCropper; - private boolean supportsMultiDisplay(WallpaperConnection connection) { - if (connection != null) { - return connection.mInfo == null // This is image wallpaper - || connection.mInfo.supportsMultipleDisplays(); + // TODO(b/384519749): Remove this set after we introduce the aspect ratio check. + private final Set<Integer> mWallpaperCompatibleDisplaysForTest = new ArraySet<>(); + + private boolean isWallpaperCompatibleForDisplay(int displayId, WallpaperConnection connection) { + if (connection == null) { + return false; } - return false; + // Non image wallpaper. + if (connection.mInfo != null) { + return connection.mInfo.supportsMultipleDisplays(); + } + + // Image wallpaper + if (enableConnectedDisplaysWallpaper()) { + // TODO(b/384519749): check display's resolution and image wallpaper cropped image + // aspect ratio. + return displayId == DEFAULT_DISPLAY + || mWallpaperCompatibleDisplaysForTest.contains(displayId); + } + // When enableConnectedDisplaysWallpaper is off, we assume the image wallpaper supports all + // usable displays. + return true; + } + + @VisibleForTesting + void addWallpaperCompatibleDisplayForTest(int displayId) { + mWallpaperCompatibleDisplaysForTest.add(displayId); + } + + @VisibleForTesting + void removeWallpaperCompatibleDisplayForTest(int displayId) { + mWallpaperCompatibleDisplaysForTest.remove(displayId); } private void updateFallbackConnection() { @@ -815,7 +785,9 @@ public class WallpaperManagerService extends IWallpaperManager.Stub Slog.w(TAG, "Fallback wallpaper connection has not been created yet!!"); return; } - if (supportsMultiDisplay(systemConnection)) { + // TODO(b/384520326) Passing DEFAULT_DISPLAY temporarily before we revamp the + // multi-display supports. + if (isWallpaperCompatibleForDisplay(DEFAULT_DISPLAY, systemConnection)) { if (fallbackConnection.mDisplayConnector.size() != 0) { fallbackConnection.forEachDisplayConnector(connector -> { if (connector.mEngine != null) { @@ -990,16 +962,14 @@ public class WallpaperManagerService extends IWallpaperManager.Stub private void initDisplayState() { // Do not initialize fallback wallpaper if (!mWallpaper.equals(mFallbackWallpaper)) { - if (supportsMultiDisplay(this)) { - // The system wallpaper is image wallpaper or it can supports multiple displays. - appendConnectorWithCondition(display -> - mWallpaperDisplayHelper.isUsableDisplay(display, mClientUid)); - } else { - // The system wallpaper does not support multiple displays, so just attach it on - // default display. - mDisplayConnector.append(DEFAULT_DISPLAY, - new DisplayConnector(DEFAULT_DISPLAY)); - } + appendConnectorWithCondition(display -> { + final int displayId = display.getDisplayId(); + if (display.getDisplayId() == DEFAULT_DISPLAY) { + return true; + } + return mWallpaperDisplayHelper.isUsableDisplay(display, mClientUid) + && isWallpaperCompatibleForDisplay(displayId, /* connection= */ this); + }); } } @@ -1601,6 +1571,12 @@ public class WallpaperManagerService extends IWallpaperManager.Stub mShuttingDown = false; mImageWallpaper = ComponentName.unflattenFromString( context.getResources().getString(R.string.image_wallpaper_component)); + if (enableConnectedDisplaysWallpaper()) { + mFallbackWallpaperComponent = ComponentName.unflattenFromString( + context.getResources().getString(R.string.fallback_wallpaper_component)); + } else { + mFallbackWallpaperComponent = null; + } ComponentName defaultComponent = WallpaperManager.getCmfDefaultWallpaperComponent(context); mDefaultWallpaperComponent = defaultComponent == null ? mImageWallpaper : defaultComponent; mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class); @@ -1665,6 +1641,13 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } @Override + public void onDisplayRemoveSystemDecorations(int displayId) { + // The display mirroring starts. The handling logic is the same as when removing a + // display. + onDisplayRemovedInternal(displayId); + } + + @Override public void onScreenTurnedOn(int displayId) { notifyScreenTurnedOn(displayId); } @@ -3613,6 +3596,13 @@ public class WallpaperManagerService extends IWallpaperManager.Stub if (componentName != null && !componentName.equals(mImageWallpaper)) { // The requested component is not the static wallpaper service, so make sure it's // actually a wallpaper service. + if (mFallbackWallpaperComponent != null + && componentName.equals(mFallbackWallpaperComponent)) { + // The fallback wallpaper does not declare WallpaperService.SERVICE_INTERFACE + // action in its intent filter to prevent it from being listed in the wallpaper + // picker. And thus, use explicit intent to query the metadata. + intent = new Intent().setComponent(mFallbackWallpaperComponent); + } List<ResolveInfo> ris = mIPackageManager.queryIntentServices(intent, intent.resolveTypeIfNeeded(mContext.getContentResolver()), @@ -3971,7 +3961,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub for (int i = 0; i < wallpapers.size(); i++) { WallpaperData wallpaper = wallpapers.get(i); - if (supportsMultiDisplay(wallpaper.connection)) { + if (isWallpaperCompatibleForDisplay(displayId, wallpaper.connection)) { final DisplayConnector connector = wallpaper.connection.getDisplayConnectorOrCreate(displayId); if (connector != null) { @@ -3993,7 +3983,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } mFallbackWallpaper.mWhich = useFallbackWallpaperWhich; } else { - if (supportsMultiDisplay(mLastWallpaper.connection)) { + if (isWallpaperCompatibleForDisplay(displayId, mLastWallpaper.connection)) { final DisplayConnector connector = mLastWallpaper.connection.getDisplayConnectorOrCreate(displayId); if (connector == null) return; @@ -4016,6 +4006,78 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } } + // This method may be called even if the display is not being removed from the system. + // This can be called when the display is removed, or when the display system decorations are + // removed to start mirroring. + private void onDisplayRemovedInternal(int displayId) { + synchronized (mLock) { + if (enableConnectedDisplaysWallpaper()) { + // There could be at most 2 wallpaper connections per display: + // 1. system & lock are the same: mLastWallpaper + // 2. system, lock are different: mLastWallpaper, mLastLockWallpaper + // 3. fallback used as both system & lock wallpaper: mFallbackWallpaper + // 4. fallback used as lock only wallpaper: mFallbackWallpaper, + // mLastWallpaper + // 5. fallback used as system only wallpaper: mFallbackWallpaper, + // mLastLockWallpaper + List<WallpaperData> pendingDisconnectWallpapers = new ArrayList<>(); + if (mLastWallpaper != null && mLastWallpaper.connection != null + && mLastWallpaper.connection.containsDisplay(displayId)) { + pendingDisconnectWallpapers.add(mLastWallpaper); + } + if (mLastLockWallpaper != null && mLastLockWallpaper.connection != null + && mLastLockWallpaper.connection.containsDisplay(displayId)) { + pendingDisconnectWallpapers.add(mLastLockWallpaper); + } + if (mFallbackWallpaper != null && mFallbackWallpaper.connection != null + && mFallbackWallpaper.connection.containsDisplay(displayId)) { + pendingDisconnectWallpapers.add(mFallbackWallpaper); + } + for (int i = 0; i < pendingDisconnectWallpapers.size(); i++) { + WallpaperData wallpaper = pendingDisconnectWallpapers.get(i); + DisplayConnector displayConnector = + wallpaper.connection.getDisplayConnectorOrCreate(displayId); + if (displayConnector == null) { + Slog.w(TAG, + "Fail to disconnect wallpaper upon display removes system " + + "decorations"); + return; + } + displayConnector.disconnectLocked(wallpaper.connection); + wallpaper.connection.removeDisplayConnector(displayId); + } + } else { + if (mLastWallpaper != null) { + WallpaperData targetWallpaper = null; + if (mLastWallpaper.connection != null + && mLastWallpaper.connection.containsDisplay(displayId)) { + targetWallpaper = mLastWallpaper; + } else if (mFallbackWallpaper != null + && mFallbackWallpaper.connection != null + && mFallbackWallpaper.connection.containsDisplay( + displayId)) { + targetWallpaper = mFallbackWallpaper; + } + if (targetWallpaper == null) return; + DisplayConnector connector = + targetWallpaper.connection.getDisplayConnectorOrCreate( + displayId); + if (connector == null) return; + connector.disconnectLocked(targetWallpaper.connection); + targetWallpaper.connection.removeDisplayConnector(displayId); + } + } + + mWallpaperDisplayHelper.removeDisplayData(displayId); + + for (int i = mColorsChangedListeners.size() - 1; i >= 0; i--) { + final SparseArray<RemoteCallbackList<IWallpaperManagerCallback>> callbacks = + mColorsChangedListeners.valueAt(i); + callbacks.delete(displayId); + } + } + } + void saveSettingsLocked(int userId) { TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG); t.traceBegin("WPMS.saveSettingsLocked-" + userId); @@ -4100,8 +4162,14 @@ public class WallpaperManagerService extends IWallpaperManager.Stub mFallbackWallpaper.allowBackup = false; mFallbackWallpaper.wallpaperId = makeWallpaperIdLocked(); mFallbackWallpaper.mBindSource = BindSource.INITIALIZE_FALLBACK; - bindWallpaperComponentLocked(mDefaultWallpaperComponent, true, false, - mFallbackWallpaper, null); + if (mFallbackWallpaperComponent == null) { + bindWallpaperComponentLocked(mDefaultWallpaperComponent, true, false, + mFallbackWallpaper, null); + } else { + mFallbackWallpaper.mWhich = FLAG_SYSTEM | FLAG_LOCK; + bindWallpaperComponentLocked(mFallbackWallpaperComponent, true, false, + mFallbackWallpaper, null); + } } } diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java index dd769173fb34..12f553426c80 100644 --- a/services/core/java/com/android/server/wm/AccessibilityController.java +++ b/services/core/java/com/android/server/wm/AccessibilityController.java @@ -58,7 +58,6 @@ import android.graphics.Matrix; import android.graphics.Path; import android.graphics.Point; import android.graphics.Rect; -import android.graphics.RectF; import android.graphics.Region; import android.os.Binder; import android.os.Build; @@ -69,7 +68,6 @@ import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.SystemClock; -import android.util.ArraySet; import android.util.Pair; import android.util.Slog; import android.util.SparseArray; @@ -79,7 +77,6 @@ import android.view.Display; import android.view.MagnificationSpec; import android.view.Surface; import android.view.ViewConfiguration; -import android.view.WindowInfo; import android.view.WindowManager; import android.view.WindowManager.TransitionFlags; import android.view.WindowManager.TransitionType; @@ -557,9 +554,6 @@ final class AccessibilityController { private static final boolean DEBUG_WINDOW_TRANSITIONS = false; private static final boolean DEBUG_DISPLAY_SIZE = false; - private static final boolean DEBUG_LAYERS = false; - private static final boolean DEBUG_RECTANGLE_REQUESTED = false; - private static final boolean DEBUG_VIEWPORT_WINDOW = false; private final Rect mTempRect1 = new Rect(); private final Rect mTempRect2 = new Rect(); @@ -579,8 +573,6 @@ final class AccessibilityController { private final MagnificationCallbacks mCallbacks; private final UserContextChangedNotifier mUserContextChangedNotifier; - private final long mLongAnimationDuration; - private boolean mIsFullscreenMagnificationActivated = false; private final Region mMagnificationRegion = new Region(); private final Region mOldMagnificationRegion = new Region(); @@ -593,7 +585,6 @@ final class AccessibilityController { private final Point mScreenSize = new Point(); private final SparseArray<WindowState> mTempWindowStates = new SparseArray<WindowState>(); - private final RectF mTempRectF = new RectF(); private final Matrix mTempMatrix = new Matrix(); DisplayMagnifier(WindowManagerService windowManagerService, @@ -609,8 +600,6 @@ final class AccessibilityController { mUserContextChangedNotifier = new UserContextChangedNotifier(mHandler); mAccessibilityTracing = AccessibilityController.getAccessibilityControllerInternal(mService); - mLongAnimationDuration = mDisplayContext.getResources().getInteger( - com.android.internal.R.integer.config_longAnimTime); if (mDisplayContext.getResources().getConfiguration().isScreenRound()) { mCircularPath = new Path(); @@ -840,10 +829,6 @@ final class AccessibilityController { outMagnificationRegion.set(mMagnificationRegion); } - boolean isMagnifying() { - return mMagnificationSpec.scale > 1.0f; - } - void destroy() { if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) { mAccessibilityTracing.logTrace(LOG_TAG + ".destroy", FLAGS_MAGNIFICATION_CALLBACK); @@ -1172,12 +1157,6 @@ final class AccessibilityController { private static final boolean DEBUG = false; - private final Set<IBinder> mTempBinderSet = new ArraySet<>(); - - private final Region mTempRegion = new Region(); - - private final Region mTempRegion2 = new Region(); - private final WindowManagerService mService; private final Handler mHandler; @@ -1243,11 +1222,10 @@ final class AccessibilityController { Slog.i(LOG_TAG, "computeChangedWindows()"); } - List<WindowInfo> windows = null; final List<AccessibilityWindow> visibleWindows = new ArrayList<>(); final Point screenSize = new Point(); final int topFocusedDisplayId; - IBinder topFocusedWindowToken = null; + final IBinder topFocusedWindowToken; synchronized (mService.mGlobalLock) { final WindowState topFocusedWindowState = getTopFocusWindow(); diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 1d12c561f118..89b46bc4eba4 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -46,6 +46,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.app.WindowConfiguration.activityTypeToString; +import static android.app.WindowConfiguration.isFloating; import static android.app.admin.DevicePolicyResources.Drawables.Source.PROFILE_SWITCH_ANIMATION; import static android.app.admin.DevicePolicyResources.Drawables.Style.OUTLINE; import static android.app.admin.DevicePolicyResources.Drawables.WORK_PROFILE_ICON; @@ -3231,8 +3232,8 @@ final class ActivityRecord extends WindowToken { * Returns {@code true} if the fixed orientation, aspect ratio, resizability of the application * can be ignored. */ - static boolean canBeUniversalResizeable(ApplicationInfo appInfo, WindowManagerService wms, - boolean isLargeScreen, boolean forActivity) { + static boolean canBeUniversalResizeable(@NonNull ApplicationInfo appInfo, + WindowManagerService wms, boolean isLargeScreen, boolean forActivity) { if (appInfo.category == ApplicationInfo.CATEGORY_GAME) { return false; } @@ -8376,6 +8377,7 @@ final class ActivityRecord extends WindowToken { mConfigurationSeq = Math.max(++mConfigurationSeq, 1); getResolvedOverrideConfiguration().seq = mConfigurationSeq; + // TODO(b/392069771): Move to AppCompatSandboxingPolicy. // Sandbox max bounds by setting it to the activity bounds, if activity is letterboxed, or // has or will have mAppCompatDisplayInsets for size compat. Also forces an activity to be // sandboxed or not depending upon the configuration settings. @@ -8404,6 +8406,20 @@ final class ActivityRecord extends WindowToken { resolvedConfig.windowConfiguration.setMaxBounds(mTmpBounds); } + // Sandbox activity bounds in freeform to app bounds to force app to display within the + // container. This prevents UI cropping when activities can draw below insets which are + // normally excluded from appBounds before targetSDK < 35 + // (see ConfigurationContainer#applySizeOverrideIfNeeded). + if (isFloating(parentWindowingMode)) { + Rect appBounds = resolvedConfig.windowConfiguration.getAppBounds(); + if (appBounds == null || appBounds.isEmpty()) { + // When there is no override bounds, the activity will inherit the bounds from + // parent. + appBounds = mResolveConfigHint.mParentAppBoundsOverride; + } + resolvedConfig.windowConfiguration.setBounds(appBounds); + } + applySizeOverrideIfNeeded( mDisplayContent, info.applicationInfo, diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index c7d4467a6e98..81c7807311dd 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -71,8 +71,6 @@ import static com.android.server.wm.ActivityRecord.State.RESUMED; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_PERMISSIONS_REVIEW; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_RESULTS; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_USER_LEAVING; -import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_CONFIGURATION; -import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_FOCUS; import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_RESULTS; import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_USER_LEAVING; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; @@ -91,9 +89,7 @@ import static com.android.server.wm.TaskFragment.EMBEDDING_DISALLOWED_NEW_TASK; import static com.android.server.wm.TaskFragment.EMBEDDING_DISALLOWED_UNTRUSTED_HOST; import static com.android.server.wm.WindowContainer.POSITION_TOP; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; -import static com.android.window.flags.Flags.balDontBringExistingBackgroundTaskStackToFg; -import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; @@ -157,8 +153,6 @@ import com.android.server.wm.TaskFragment.EmbeddingCheckResult; import com.android.wm.shell.Flags; import java.io.PrintWriter; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.text.DateFormat; import java.util.Date; import java.util.function.Supplier; @@ -172,8 +166,6 @@ import java.util.function.Supplier; class ActivityStarter { private static final String TAG = TAG_WITH_CLASS_NAME ? "ActivityStarter" : TAG_ATM; private static final String TAG_RESULTS = TAG + POSTFIX_RESULTS; - private static final String TAG_FOCUS = TAG + POSTFIX_FOCUS; - private static final String TAG_CONFIGURATION = TAG + POSTFIX_CONFIGURATION; private static final String TAG_USER_LEAVING = TAG + POSTFIX_USER_LEAVING; private static final int INVALID_LAUNCH_MODE = -1; @@ -255,26 +247,7 @@ class ActivityStarter { private boolean mIsTaskCleared; private boolean mMovedToFront; private boolean mNoAnimation; - - // TODO mAvoidMoveToFront before V is changed from a boolean to a int code mCanMoveToFrontCode - // for the purpose of attribution of new BAL V feature. This should be reverted back to the - // boolean flag post V. - @IntDef(prefix = {"MOVE_TO_FRONT_"}, value = { - MOVE_TO_FRONT_ALLOWED, - MOVE_TO_FRONT_AVOID_PI_ONLY_CREATOR_ALLOWS, - MOVE_TO_FRONT_AVOID_LEGACY, - }) - @Retention(RetentionPolicy.SOURCE) - public @interface MoveToFrontCode {} - - // Allows a task move to front. - private static final int MOVE_TO_FRONT_ALLOWED = 0; - // Avoid a task move to front because the Pending Intent that starts the activity only - // its creator has the BAL privilege, its sender does not. - private static final int MOVE_TO_FRONT_AVOID_PI_ONLY_CREATOR_ALLOWS = 1; - // Avoid a task move to front because of all other legacy reasons. - private static final int MOVE_TO_FRONT_AVOID_LEGACY = 2; - private @MoveToFrontCode int mCanMoveToFrontCode = MOVE_TO_FRONT_ALLOWED; + private boolean mAvoidMoveToFront; private boolean mFrozeTaskList; private boolean mTransientLaunch; // The task which was above the targetTask before starting this activity. null if the targetTask @@ -771,7 +744,7 @@ class ActivityStarter { mIsTaskCleared = starter.mIsTaskCleared; mMovedToFront = starter.mMovedToFront; mNoAnimation = starter.mNoAnimation; - mCanMoveToFrontCode = starter.mCanMoveToFrontCode; + mAvoidMoveToFront = starter.mAvoidMoveToFront; mFrozeTaskList = starter.mFrozeTaskList; mVoiceSession = starter.mVoiceSession; @@ -1711,14 +1684,6 @@ class ActivityStarter { return result; } - private boolean avoidMoveToFront() { - return mCanMoveToFrontCode != MOVE_TO_FRONT_ALLOWED; - } - - private boolean avoidMoveToFrontPIOnlyCreatorAllows() { - return mCanMoveToFrontCode == MOVE_TO_FRONT_AVOID_PI_ONLY_CREATOR_ALLOWS; - } - /** * If the start result is success, ensure that the configuration of the started activity matches * the current display. Otherwise clean up unassociated containers to avoid leakage. @@ -1768,7 +1733,7 @@ class ActivityStarter { startedActivityRootTask.setAlwaysOnTop(true); } - if (isIndependentLaunch && !mDoResume && avoidMoveToFront() && !mTransientLaunch + if (isIndependentLaunch && !mDoResume && mAvoidMoveToFront && !mTransientLaunch && !started.shouldBeVisible(true /* ignoringKeyguard */)) { Slog.i(TAG, "Abort " + transition + " of invisible launch " + started); transition.abort(); @@ -1784,7 +1749,7 @@ class ActivityStarter { currentTop, currentTop.mDisplayContent, false /* deferResume */); } - if (!avoidMoveToFront() && mDoResume + if (!mAvoidMoveToFront && mDoResume && !mService.getUserManagerInternal().isVisibleBackgroundFullUser(started.mUserId) && mRootWindowContainer.hasVisibleWindowAboveButDoesNotOwnNotificationShade( started.launchedFromUid)) { @@ -1934,19 +1899,17 @@ class ActivityStarter { } // When running transient transition, the transient launch target should keep on top. // So disallow the transient hide activity to move itself to front, e.g. trampoline. - if (!avoidMoveToFront() && (mService.mHomeProcess == null + if (!mAvoidMoveToFront && (mService.mHomeProcess == null || mService.mHomeProcess.mUid != realCallingUid) && (prevTopTask != null && prevTopTask.isActivityTypeHomeOrRecents()) && r.mTransitionController.isTransientHide(targetTask)) { - mCanMoveToFrontCode = MOVE_TO_FRONT_AVOID_LEGACY; + mAvoidMoveToFront = true; } // If the activity is started by sending a pending intent and only its creator has the // privilege to allow BAL (its sender does not), avoid move it to the front. Only do // this when it is not a new task and not already been marked as avoid move to front. - // Guarded by a flag: balDontBringExistingBackgroundTaskStackToFg - if (balDontBringExistingBackgroundTaskStackToFg() && !avoidMoveToFront() - && balVerdict.onlyCreatorAllows()) { - mCanMoveToFrontCode = MOVE_TO_FRONT_AVOID_PI_ONLY_CREATOR_ALLOWS; + if (!mAvoidMoveToFront && balVerdict.onlyCreatorAllows()) { + mAvoidMoveToFront = true; } mPriorAboveTask = TaskDisplayArea.getRootTaskAbove(targetTask.getRootTask()); } @@ -2003,32 +1966,28 @@ class ActivityStarter { // After activity is attached to task, but before actual start recordTransientLaunchIfNeeded(mLastStartActivityRecord); - if (mDoResume) { - if (!avoidMoveToFront()) { - mTargetRootTask.getRootTask().moveToFront("reuseOrNewTask", targetTask); - - final boolean launchBehindDream; - if (com.android.window.flags.Flags.removeActivityStarterDreamCallback()) { - final TaskDisplayArea tda = mTargetRootTask.getTaskDisplayArea(); - final Task top = (tda != null ? tda.getTopRootTask() : null); - launchBehindDream = (top != null && top != mTargetRootTask) - && top.getActivityType() == WindowConfiguration.ACTIVITY_TYPE_DREAM - && top.getTopNonFinishingActivity() != null; - } else { - launchBehindDream = !mTargetRootTask.isTopRootTaskInDisplayArea() - && mService.isDreaming() - && !dreamStopping; - } + if (!mAvoidMoveToFront && mDoResume) { + mTargetRootTask.getRootTask().moveToFront("reuseOrNewTask", targetTask); - if (launchBehindDream) { - // Launching underneath dream activity (fullscreen, always-on-top). Run the - // launch--behind transition so the Activity gets created and starts - // in visible state. - mLaunchTaskBehind = true; - r.mLaunchTaskBehind = true; - } + final boolean launchBehindDream; + if (com.android.window.flags.Flags.removeActivityStarterDreamCallback()) { + final TaskDisplayArea tda = mTargetRootTask.getTaskDisplayArea(); + final Task top = (tda != null ? tda.getTopRootTask() : null); + launchBehindDream = (top != null && top != mTargetRootTask) + && top.getActivityType() == WindowConfiguration.ACTIVITY_TYPE_DREAM + && top.getTopNonFinishingActivity() != null; } else { - logPIOnlyCreatorAllowsBAL(); + launchBehindDream = !mTargetRootTask.isTopRootTaskInDisplayArea() + && mService.isDreaming() + && !dreamStopping; + } + + if (launchBehindDream) { + // Launching underneath dream activity (fullscreen, always-on-top). Run the + // launch--behind transition so the Activity gets created and starts + // in visible state. + mLaunchTaskBehind = true; + r.mLaunchTaskBehind = true; } } @@ -2089,13 +2048,9 @@ class ActivityStarter { // root-task to the will not update the focused root-task. If starting the new // activity now allows the task root-task to be focusable, then ensure that we // now update the focused root-task accordingly. - if (mTargetRootTask.isTopActivityFocusable() + if (!mAvoidMoveToFront && mTargetRootTask.isTopActivityFocusable() && !mRootWindowContainer.isTopDisplayFocusedRootTask(mTargetRootTask)) { - if (!avoidMoveToFront()) { - mTargetRootTask.moveToFront("startActivityInner"); - } else { - logPIOnlyCreatorAllowsBAL(); - } + mTargetRootTask.moveToFront("startActivityInner"); } mRootWindowContainer.resumeFocusedTasksTopActivities( mTargetRootTask, mStartActivity, mOptions, mTransientLaunch); @@ -2123,26 +2078,6 @@ class ActivityStarter { return START_SUCCESS; } - // TODO (b/316135632) Post V release, remove this log method. - private void logPIOnlyCreatorAllowsBAL() { - if (!avoidMoveToFrontPIOnlyCreatorAllows()) return; - String realCallingPackage = - mService.mContext.getPackageManager().getNameForUid(mRealCallingUid); - if (realCallingPackage == null) { - realCallingPackage = "uid=" + mRealCallingUid; - } - Slog.wtf(TAG, "Without Android 15 BAL hardening this activity would be moved to the " - + "foreground. The activity is started by a PendingIntent. However, only the " - + "creator of the PendingIntent allows BAL while the sender does not allow BAL. " - + "realCallingPackage: " + realCallingPackage - + "; callingPackage: " + mRequest.callingPackage - + "; mTargetRootTask:" + mTargetRootTask - + "; mIntent: " + mIntent - + "; mTargetRootTask.getTopNonFinishingActivity: " - + mTargetRootTask.getTopNonFinishingActivity() - + "; mTargetRootTask.getRootActivity: " + mTargetRootTask.getRootActivity()); - } - private void recordTransientLaunchIfNeeded(ActivityRecord r) { if (r == null || !mTransientLaunch) return; final TransitionController controller = r.mTransitionController; @@ -2287,7 +2222,7 @@ class ActivityStarter { } if (!mSupervisor.getBackgroundActivityLaunchController().checkActivityAllowedToStart( - mSourceRecord, r, newTask, avoidMoveToFront(), targetTask, mLaunchFlags, mBalCode, + mSourceRecord, r, newTask, mAvoidMoveToFront, targetTask, mLaunchFlags, mBalCode, mCallingUid, mRealCallingUid, mPreferredTaskDisplayArea)) { return START_ABORTED; } @@ -2635,7 +2570,7 @@ class ActivityStarter { mIsTaskCleared = false; mMovedToFront = false; mNoAnimation = false; - mCanMoveToFrontCode = MOVE_TO_FRONT_ALLOWED; + mAvoidMoveToFront = false; mFrozeTaskList = false; mTransientLaunch = false; mPriorAboveTask = null; @@ -2747,12 +2682,12 @@ class ActivityStarter { // The caller specifies that we'd like to be avoided to be moved to the // front, so be it! mDoResume = false; - mCanMoveToFrontCode = MOVE_TO_FRONT_AVOID_LEGACY; + mAvoidMoveToFront = true; } } } else if (mOptions.getAvoidMoveToFront()) { mDoResume = false; - mCanMoveToFrontCode = MOVE_TO_FRONT_AVOID_LEGACY; + mAvoidMoveToFront = true; } mTransientLaunch = mOptions.getTransientLaunch(); final KeyguardController kc = mSupervisor.getKeyguardController(); @@ -2762,7 +2697,7 @@ class ActivityStarter { if (mTransientLaunch && mDisplayLockAndOccluded && mService.getTransitionController().isShellTransitionsEnabled()) { mDoResume = false; - mCanMoveToFrontCode = MOVE_TO_FRONT_AVOID_LEGACY; + mAvoidMoveToFront = true; } mTargetRootTask = Task.fromWindowContainerToken(mOptions.getLaunchRootTask()); @@ -2819,7 +2754,7 @@ class ActivityStarter { mNoAnimation = (mLaunchFlags & FLAG_ACTIVITY_NO_ANIMATION) != 0; if (mBalCode == BAL_BLOCK && !mService.isBackgroundActivityStartsEnabled()) { - mCanMoveToFrontCode = MOVE_TO_FRONT_AVOID_LEGACY; + mAvoidMoveToFront = true; mDoResume = false; } } @@ -3050,7 +2985,7 @@ class ActivityStarter { differentTopTask = true; } - if (differentTopTask && !avoidMoveToFront()) { + if (differentTopTask && !mAvoidMoveToFront) { mStartActivity.intent.addFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT); // We really do want to push this one into the user's face, right now. if (mLaunchTaskBehind && mSourceRecord != null) { @@ -3094,9 +3029,6 @@ class ActivityStarter { } mOptions = null; } - if (differentTopTask) { - logPIOnlyCreatorAllowsBAL(); - } // Update the target's launch cookie and pending remote animation to those specified in the // options if set. if (mStartActivity.mLaunchCookie != null) { @@ -3137,7 +3069,7 @@ class ActivityStarter { } private void setNewTask(Task taskToAffiliate) { - final boolean toTop = !mLaunchTaskBehind && !avoidMoveToFront(); + final boolean toTop = !mLaunchTaskBehind && !mAvoidMoveToFront; final Task task = mTargetRootTask.reuseOrCreateTask( mStartActivity.info, mIntent, mVoiceSession, mVoiceInteractor, toTop, mStartActivity, mSourceRecord, mOptions); diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index d4f9c0901162..cf111cdbcc6a 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -2154,6 +2154,16 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } } + /** + * @return ehether the application could be universal resizeable on a large screen, + * ignoring any overrides + */ + @Override + public boolean canBeUniversalResizeable(@NonNull ApplicationInfo appInfo) { + return ActivityRecord.canBeUniversalResizeable(appInfo, mWindowManager, + /* isLargeScreen */ true, /* forActivity */ false); + } + @Override public void removeAllVisibleRecentTasks() { mAmInternal.enforceCallingPermission(REMOVE_TASKS, "removeAllVisibleRecentTasks()"); diff --git a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java index 35fa39dab900..d994a1904a14 100644 --- a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java @@ -180,10 +180,7 @@ class AppCompatOrientationPolicy { return true; } - final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy - .getAppCompatCameraPolicy(mActivityRecord); - if (cameraPolicy != null - && cameraPolicy.isTreatmentEnabledForActivity(mActivityRecord)) { + if (AppCompatCameraPolicy.isTreatmentEnabledForActivity(mActivityRecord)) { Slog.w(TAG, "Ignoring orientation update to " + screenOrientationToString(requestedOrientation) + " due to camera compat treatment for " + mActivityRecord); diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index 119709e86551..6df01f4b328b 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -45,12 +45,11 @@ import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_FG_ONL import static com.android.server.wm.ActivityTaskSupervisor.getApplicationLabel; import static com.android.server.wm.PendingRemoteAnimationRegistry.TIMEOUT_MS; import static com.android.window.flags.Flags.balAdditionalStartModes; -import static com.android.window.flags.Flags.balDontBringExistingBackgroundTaskStackToFg; import static com.android.window.flags.Flags.balImprovedMetrics; import static com.android.window.flags.Flags.balRequireOptInByPendingIntentCreator; import static com.android.window.flags.Flags.balShowToastsBlocked; -import static com.android.window.flags.Flags.balStrictModeRo; import static com.android.window.flags.Flags.balStrictModeGracePeriod; +import static com.android.window.flags.Flags.balStrictModeRo; import static java.lang.annotation.RetentionPolicy.SOURCE; import static java.util.Objects.requireNonNull; @@ -620,8 +619,6 @@ public class BackgroundActivityStartController { // features sb.append("; balRequireOptInByPendingIntentCreator: ") .append(balRequireOptInByPendingIntentCreator()); - sb.append("; balDontBringExistingBackgroundTaskStackToFg: ") - .append(balDontBringExistingBackgroundTaskStackToFg()); sb.append("]"); return sb.toString(); } diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index e1a50a93edcc..5b1619995529 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -1910,6 +1910,11 @@ public class DisplayPolicy { if (statusBar != null) { statusBar.onDisplayRemoveSystemDecorations(displayId); } + final WallpaperManagerInternal wpMgr = + LocalServices.getService(WallpaperManagerInternal.class); + if (wpMgr != null) { + wpMgr.onDisplayRemoveSystemDecorations(displayId); + } }); } diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java index bcfaa3947e74..59ca79c7ffc3 100644 --- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java +++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java @@ -361,7 +361,7 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider { controlTarget = mDisplayContent.getImeHostOrFallback( ((InsetsControlTarget) imeInsetsTarget).getWindow()); - if (controlTarget != imeInsetsTarget) { + if (controlTarget != null && controlTarget != imeInsetsTarget) { ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_WM_SET_REMOTE_TARGET_IME_VISIBILITY); controlTarget.setImeInputTargetRequestedVisibility(imeVisible, statsToken); diff --git a/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java index efc68aac0323..00e1c01bbadb 100644 --- a/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java +++ b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java @@ -22,6 +22,7 @@ import static com.android.internal.protolog.WmProtoLogGroups.WM_ERROR; import android.media.projection.IMediaProjectionManager; import android.media.projection.IMediaProjectionWatcherCallback; +import android.media.projection.MediaProjectionEvent; import android.media.projection.MediaProjectionInfo; import android.os.Binder; import android.os.IBinder; @@ -84,6 +85,12 @@ public class ScreenRecordingCallbackController { public void onRecordingSessionSet(MediaProjectionInfo mediaProjectionInfo, ContentRecordingSession contentRecordingSession) { } + + @Override + public void onMediaProjectionEvent( + MediaProjectionEvent event, + MediaProjectionInfo mediaProjectionInfo, + ContentRecordingSession session) {} } ScreenRecordingCallbackController(WindowManagerService wms) { diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 37cc0d22c063..27683b2fcff2 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -1504,16 +1504,15 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } // Update the input-sink (touch-blocking) state now that the animation is finished. - SurfaceControl.Transaction inputSinkTransaction = null; + boolean scheduleAnimation = false; for (int i = 0; i < mParticipants.size(); ++i) { final ActivityRecord ar = mParticipants.valueAt(i).asActivityRecord(); if (ar == null || !ar.isVisible() || ar.getParent() == null) continue; - if (inputSinkTransaction == null) { - inputSinkTransaction = ar.mWmService.mTransactionFactory.get(); - } - ar.mActivityRecordInputSink.applyChangesToSurfaceIfChanged(inputSinkTransaction); + scheduleAnimation = true; + ar.mActivityRecordInputSink.applyChangesToSurfaceIfChanged(ar.getPendingTransaction()); } - if (inputSinkTransaction != null) inputSinkTransaction.apply(); + // To apply pending transactions. + if (scheduleAnimation) mController.mAtm.mWindowManager.scheduleAnimationLocked(); // Always schedule stop processing when transition finishes because activities don't // stop while they are in a transition thus their stop could still be pending. diff --git a/services/core/java/com/android/server/wm/WallpaperWindowToken.java b/services/core/java/com/android/server/wm/WallpaperWindowToken.java index 0ecd0251ca94..3b79c54f1c73 100644 --- a/services/core/java/com/android/server/wm/WallpaperWindowToken.java +++ b/services/core/java/com/android/server/wm/WallpaperWindowToken.java @@ -89,8 +89,9 @@ class WallpaperWindowToken extends WindowToken { // Similar to Task.prepareSurfaces, outside of transitions we need to apply visibility // changes directly. In transitions the transition player will take care of applying the // visibility change. - if (!mTransitionController.inTransition(this)) { - getSyncTransaction().setVisibility(mSurfaceControl, isVisible()); + if (!mTransitionController.isCollecting(this) + && !mTransitionController.isPlayingTarget(this)) { + getPendingTransaction().setVisibility(mSurfaceControl, isVisible()); } } } diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index e761e024b3ca..883d8f95b612 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -1680,26 +1680,27 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< * Sets the specified orientation of this container. It percolates this change upward along the * hierarchy to let each level of the hierarchy a chance to respond to it. * - * @param orientation the specified orientation. Needs to be one of {@link ScreenOrientation}. + * @param requestedOrientation the specified orientation. Needs to be one of + * {@link ScreenOrientation}. * @param requestingContainer the container which orientation request has changed. Mostly used * to ensure it gets correct configuration. * @return the resolved override orientation of this window container. */ @ScreenOrientation - int setOrientation(@ScreenOrientation int orientation, + int setOrientation(@ScreenOrientation int requestedOrientation, @Nullable WindowContainer requestingContainer) { - if (getOverrideOrientation() == orientation) { - return orientation; + if (getOverrideOrientation() == requestedOrientation) { + return requestedOrientation; } - setOverrideOrientation(orientation); + setOverrideOrientation(requestedOrientation); final WindowContainer parent = getParent(); if (parent == null) { - return orientation; + return requestedOrientation; } // The derived class can return a result that is different from the given orientation. - final int resolvedOrientation = getOverrideOrientation(); + final int actualOverrideOrientation = getOverrideOrientation(); if (getConfiguration().orientation != getRequestedConfigurationOrientation( - false /* forDisplay */, resolvedOrientation) + false /* forDisplay */, actualOverrideOrientation) // Update configuration directly only if the change won't be dispatched from // ancestor. This prevents from computing intermediate configuration when the // parent also needs to be updated from the ancestor. E.g. the app requests @@ -1707,12 +1708,12 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< // the task can be updated to portrait first so the configuration can be // computed in a consistent environment. && (inMultiWindowMode() - || !handlesOrientationChangeFromDescendant(orientation))) { + || !handlesOrientationChangeFromDescendant(requestedOrientation))) { // Resolve the requested orientation. onConfigurationChanged(parent.getConfiguration()); } onDescendantOrientationChanged(requestingContainer); - return resolvedOrientation; + return actualOverrideOrientation; } @ScreenOrientation diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 5de0e9b6ed93..3a1d652f82d4 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -309,6 +309,7 @@ import android.window.ActivityWindowInfo; import android.window.AddToSurfaceSyncGroupResult; import android.window.ClientWindowFrames; import android.window.ConfigurationChangeSetting; +import android.window.DesktopModeFlags; import android.window.IGlobalDragListener; import android.window.IScreenRecordingCallback; import android.window.ISurfaceSyncGroupCompletedListener; @@ -820,6 +821,8 @@ public class WindowManagerService extends IWindowManager.Stub DEVELOPMENT_WM_DISPLAY_SETTINGS_PATH); private final Uri mMaximumObscuringOpacityForTouchUri = Settings.Global.getUriFor( Settings.Global.MAXIMUM_OBSCURING_OPACITY_FOR_TOUCH); + private final Uri mDevelopmentOverrideDesktopExperienceUri = Settings.Global.getUriFor( + Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_EXPERIENCE_FEATURES); public SettingsObserver() { super(new Handler()); @@ -847,6 +850,8 @@ public class WindowManagerService extends IWindowManager.Stub UserHandle.USER_ALL); resolver.registerContentObserver(mMaximumObscuringOpacityForTouchUri, false, this, UserHandle.USER_ALL); + resolver.registerContentObserver(mDevelopmentOverrideDesktopExperienceUri, false, this, + UserHandle.USER_ALL); } @Override @@ -890,6 +895,11 @@ public class WindowManagerService extends IWindowManager.Stub return; } + if (mDevelopmentOverrideDesktopExperienceUri.equals(uri)) { + updateDevelopmentOverrideDesktopExperience(); + return; + } + @UpdateAnimationScaleMode final int mode; if (mWindowAnimationScaleUri.equals(uri)) { @@ -956,6 +966,16 @@ public class WindowManagerService extends IWindowManager.Stub mAtmService.mForceResizableActivities = forceResizable; } + void updateDevelopmentOverrideDesktopExperience() { + ContentResolver resolver = mContext.getContentResolver(); + final int overrideDesktopMode = Settings.Global.getInt(resolver, + Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_EXPERIENCE_FEATURES, + DesktopModeFlags.ToggleOverride.OVERRIDE_UNSET.getSetting()); + + SystemProperties.set(DesktopModeFlags.SYSTEM_PROPERTY_NAME, + Integer.toString(overrideDesktopMode)); + } + void updateDevEnableNonResizableMultiWindow() { ContentResolver resolver = mContext.getContentResolver(); final boolean devEnableNonResizableMultiWindow = Settings.Global.getInt(resolver, diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index a1755e4d9d3b..060f2e803ec9 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -756,40 +756,6 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub t.getTaskFragmentOrganizer()); } } - // Queue-up bounds-change transactions for tasks which are now organized. Do - // this after hierarchy ops so we have the final organized state. - entries = t.getChanges().entrySet().iterator(); - while (entries.hasNext()) { - final Map.Entry<IBinder, WindowContainerTransaction.Change> entry = entries.next(); - final WindowContainer wc = WindowContainer.fromBinder(entry.getKey()); - if (wc == null || !wc.isAttached()) { - Slog.e(TAG, "Attempt to operate on detached container: " + wc); - continue; - } - final Task task = wc.asTask(); - final Rect surfaceBounds = entry.getValue().getBoundsChangeSurfaceBounds(); - if (task == null || !task.isAttached() || surfaceBounds == null) { - continue; - } - if (!task.isOrganized()) { - final Task parent = task.getParent() != null ? task.getParent().asTask() : null; - // Also allow direct children of created-by-organizer tasks to be - // controlled. In the future, these will become organized anyways. - if (parent == null || !parent.mCreatedByOrganizer) { - throw new IllegalArgumentException( - "Can't manipulate non-organized task surface " + task); - } - } - final SurfaceControl.Transaction sft = new SurfaceControl.Transaction(); - final SurfaceControl sc = task.getSurfaceControl(); - sft.setPosition(sc, surfaceBounds.left, surfaceBounds.top); - if (surfaceBounds.isEmpty()) { - sft.setWindowCrop(sc, null); - } else { - sft.setWindowCrop(sc, surfaceBounds.width(), surfaceBounds.height()); - } - task.setMainWindowSizeChangeTransaction(sft); - } if ((effects & TRANSACT_EFFECTS_LIFECYCLE) != 0) { mService.mTaskSupervisor.setDeferRootVisibilityUpdate(false /* deferUpdate */); mService.mTaskSupervisor.endDeferResume(); diff --git a/services/core/jni/com_android_server_utils_AnrTimer.cpp b/services/core/jni/com_android_server_utils_AnrTimer.cpp index 2add5b09f15b..24909ac6150b 100644 --- a/services/core/jni/com_android_server_utils_AnrTimer.cpp +++ b/services/core/jni/com_android_server_utils_AnrTimer.cpp @@ -101,6 +101,9 @@ nsecs_t now() { return systemTime(SYSTEM_TIME_MONOTONIC); } +// The current process. This is cached here on startup. +const pid_t sThisProcess = getpid(); + // Return true if the process exists and false if we cannot know. bool processExists(pid_t pid) { char path[PATH_MAX]; @@ -726,7 +729,7 @@ class AnrTimerService::Timer { uid(uid), timeout(timeout), extend(extend), - freeze(pid != 0 && freeze), + freeze(freeze), split(trace.earlyTimeout), action(trace.action), status(Running), @@ -1188,8 +1191,11 @@ const char* AnrTimerService::statusString(Status s) { } AnrTimerService::timer_id_t AnrTimerService::start(int pid, int uid, nsecs_t timeout) { + // Use the freezer only if the pid is not 0 (a nonsense value) and the pid is not self. + // Freezing the current process is a fatal error. + bool useFreezer = freeze_ && (pid != 0) && (pid != sThisProcess); AutoMutex _l(lock_); - Timer t(pid, uid, timeout, extend_, freeze_, tracer_.getConfig(pid)); + Timer t(pid, uid, timeout, extend_, useFreezer, tracer_.getConfig(pid)); insertLocked(t); t.start(); counters_.started++; diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 25e9f8a38f89..c974d9e1dc87 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -1799,7 +1799,7 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(LogcatManagerService.class); t.traceEnd(); - if (!isWatch && !isTv && !isAutomotive + if (!isWatch && !isTv && !isAutomotive && !isDesktop && android.security.Flags.aflApi()) { t.traceBegin("StartIntrusionDetectionService"); mSystemServiceManager.startService(IntrusionDetectionService.class); diff --git a/services/supervision/java/com/android/server/supervision/SupervisionService.java b/services/supervision/java/com/android/server/supervision/SupervisionService.java index 073ee31ddd60..195c65d6ec45 100644 --- a/services/supervision/java/com/android/server/supervision/SupervisionService.java +++ b/services/supervision/java/com/android/server/supervision/SupervisionService.java @@ -23,6 +23,7 @@ import static com.android.internal.util.Preconditions.checkCallAuthorization; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.app.admin.DevicePolicyManager; import android.app.admin.DevicePolicyManagerInternal; @@ -51,7 +52,6 @@ import com.android.internal.util.DumpUtils; import com.android.internal.util.IndentingPrintWriter; import com.android.server.LocalServices; import com.android.server.SystemService; -import com.android.server.SystemService.TargetUser; import com.android.server.pm.UserManagerInternal; import java.io.FileDescriptor; @@ -62,26 +62,28 @@ import java.util.List; public class SupervisionService extends ISupervisionManager.Stub { private static final String LOG_TAG = "SupervisionService"; - private final Context mContext; - // TODO(b/362756788): Does this need to be a LockGuard lock? private final Object mLockDoNoUseDirectly = new Object(); @GuardedBy("getLockObject()") private final SparseArray<SupervisionUserData> mUserData = new SparseArray<>(); - private final DevicePolicyManagerInternal mDpmInternal; - private final PackageManager mPackageManager; - private final UserManagerInternal mUserManagerInternal; + private final Context mContext; + private final Injector mInjector; + final SupervisionManagerInternal mInternal = new SupervisionManagerInternalImpl(); public SupervisionService(Context context) { mContext = context.createAttributionContext(LOG_TAG); - mDpmInternal = LocalServices.getService(DevicePolicyManagerInternal.class); - mPackageManager = context.getPackageManager(); - mUserManagerInternal = LocalServices.getService(UserManagerInternal.class); - mUserManagerInternal.addUserLifecycleListener(new UserLifecycleListener()); + mInjector = new Injector(context); + mInjector.getUserManagerInternal().addUserLifecycleListener(new UserLifecycleListener()); } + /** + * Returns whether supervision is enabled for the given user. + * + * <p>Supervision is automatically enabled when the supervision app becomes the profile owner or + * explicitly enabled via an internal call to {@link #setSupervisionEnabledForUser}. + */ @Override public boolean isSupervisionEnabledForUser(@UserIdInt int userId) { if (UserHandle.getUserId(Binder.getCallingUid()) != userId) { @@ -92,6 +94,20 @@ public class SupervisionService extends ISupervisionManager.Stub { } } + /** + * Returns the package name of the active supervision app or null if supervision is disabled. + */ + @Override + @Nullable + public String getActiveSupervisionAppPackage(@UserIdInt int userId) { + if (UserHandle.getUserId(Binder.getCallingUid()) != userId) { + enforcePermission(INTERACT_ACROSS_USERS); + } + synchronized (getLockObject()) { + return getUserDataLocked(userId).supervisionAppPackage; + } + } + @Override public void onShellCommand( @Nullable FileDescriptor in, @@ -114,7 +130,7 @@ public class SupervisionService extends ISupervisionManager.Stub { pw.println("SupervisionService state:"); pw.increaseIndent(); - List<UserInfo> users = mUserManagerInternal.getUsers(false); + List<UserInfo> users = mInjector.getUserManagerInternal().getUsers(false); synchronized (getLockObject()) { for (var user : users) { getUserDataLocked(user.id).dump(pw); @@ -140,35 +156,54 @@ public class SupervisionService extends ISupervisionManager.Stub { return data; } - void setSupervisionEnabledForUser(@UserIdInt int userId, boolean enabled) { + /** + * Sets supervision as enabled or disabled for the given user and, in case supervision is being + * enabled, the package of the active supervision app. + */ + private void setSupervisionEnabledForUser( + @UserIdInt int userId, boolean enabled, @Nullable String supervisionAppPackage) { synchronized (getLockObject()) { - getUserDataLocked(userId).supervisionEnabled = enabled; + SupervisionUserData data = getUserDataLocked(userId); + data.supervisionEnabled = enabled; + data.supervisionAppPackage = enabled ? supervisionAppPackage : null; } } - /** Ensures that supervision is enabled when supervision app is the profile owner. */ + /** Ensures that supervision is enabled when the supervision app is the profile owner. */ private void syncStateWithDevicePolicyManager(@UserIdInt int userId) { - if (isProfileOwner(userId)) { - setSupervisionEnabledForUser(userId, true); + final DevicePolicyManagerInternal dpmInternal = mInjector.getDpmInternal(); + final ComponentName po = + dpmInternal != null ? dpmInternal.getProfileOwnerAsUser(userId) : null; + + if (po != null && po.getPackageName().equals(getSystemSupervisionPackage())) { + setSupervisionEnabledForUser(userId, true, po.getPackageName()); + } else if (po != null && po.equals(getSupervisionProfileOwnerComponent())) { + // TODO(b/392071637): Consider not enabling supervision in case profile owner is given + // to the legacy supervision profile owner component. + setSupervisionEnabledForUser(userId, true, po.getPackageName()); } else { // TODO(b/381428475): Avoid disabling supervision when the app is not the profile owner. // This might only be possible after introducing specific and public APIs to enable - // supervision. - setSupervisionEnabledForUser(userId, false); + // and disable supervision. + setSupervisionEnabledForUser(userId, false, /* supervisionAppPackage= */ null); } } - /** Returns whether the supervision app has profile owner status. */ - private boolean isProfileOwner(@UserIdInt int userId) { - ComponentName profileOwner = - mDpmInternal != null ? mDpmInternal.getProfileOwnerAsUser(userId) : null; - return profileOwner != null && isSupervisionAppPackage(profileOwner.getPackageName()); + /** + * Returns the {@link ComponentName} of the supervision profile owner component. + * + * <p>This component is used to give GMS Kids Module permission to supervise the device and may + * still be active during the transition to the {@code SYSTEM_SUPERVISION} role. + */ + private ComponentName getSupervisionProfileOwnerComponent() { + return ComponentName.unflattenFromString( + mContext.getResources() + .getString(R.string.config_defaultSupervisionProfileOwnerComponent)); } - /** Returns whether the given package name belongs to the supervision role holder. */ - private boolean isSupervisionAppPackage(String packageName) { - return packageName.equals( - mContext.getResources().getString(R.string.config_systemSupervision)); + /** Returns the package assigned to the {@code SYSTEM_SUPERVISION} role. */ + private String getSystemSupervisionPackage() { + return mContext.getResources().getString(R.string.config_systemSupervision); } /** Enforces that the caller has the given permission. */ @@ -177,6 +212,41 @@ public class SupervisionService extends ISupervisionManager.Stub { mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED); } + /** Provides local services in a lazy manner. */ + static class Injector { + private final Context mContext; + private DevicePolicyManagerInternal mDpmInternal; + private PackageManager mPackageManager; + private UserManagerInternal mUserManagerInternal; + + Injector(Context context) { + mContext = context; + } + + @Nullable + DevicePolicyManagerInternal getDpmInternal() { + if (mDpmInternal == null) { + mDpmInternal = LocalServices.getService(DevicePolicyManagerInternal.class); + } + return mDpmInternal; + } + + PackageManager getPackageManager() { + if (mPackageManager == null) { + mPackageManager = mContext.getPackageManager(); + } + return mPackageManager; + } + + UserManagerInternal getUserManagerInternal() { + if (mUserManagerInternal == null) { + mUserManagerInternal = LocalServices.getService(UserManagerInternal.class); + } + return mUserManagerInternal; + } + } + + /** Publishes local and binder services and allows the service to act during initialization. */ public static class Lifecycle extends SystemService { private final SupervisionService mSupervisionService; @@ -201,6 +271,7 @@ public class SupervisionService extends ISupervisionManager.Stub { } @VisibleForTesting + @SuppressLint("MissingPermission") // not needed for a service void registerProfileOwnerListener() { IntentFilter poIntentFilter = new IntentFilter(); poIntentFilter.addAction(DevicePolicyManager.ACTION_PROFILE_OWNER_CHANGED); @@ -209,7 +280,7 @@ public class SupervisionService extends ISupervisionManager.Stub { .registerReceiverForAllUsers( new ProfileOwnerBroadcastReceiver(), poIntentFilter, - /* brodcastPermission= */ null, + /* broadcastPermission= */ null, /* scheduler= */ null); } @@ -228,19 +299,22 @@ public class SupervisionService extends ISupervisionManager.Stub { } } - final SupervisionManagerInternal mInternal = new SupervisionManagerInternalImpl(); - + /** Implementation of the local service, API used by other services. */ private final class SupervisionManagerInternalImpl extends SupervisionManagerInternal { @Override public boolean isActiveSupervisionApp(int uid) { - String[] packages = mPackageManager.getPackagesForUid(uid); - if (packages == null) { + int userId = UserHandle.getUserId(uid); + String supervisionAppPackage = getActiveSupervisionAppPackage(userId); + if (supervisionAppPackage == null) { return false; } - for (var packageName : packages) { - if (SupervisionService.this.isSupervisionAppPackage(packageName)) { - int userId = UserHandle.getUserId(uid); - return SupervisionService.this.isSupervisionEnabledForUser(userId); + + String[] packages = mInjector.getPackageManager().getPackagesForUid(uid); + if (packages != null) { + for (var packageName : packages) { + if (supervisionAppPackage.equals(packageName)) { + return true; + } } } return false; @@ -253,7 +327,8 @@ public class SupervisionService extends ISupervisionManager.Stub { @Override public void setSupervisionEnabledForUser(@UserIdInt int userId, boolean enabled) { - SupervisionService.this.setSupervisionEnabledForUser(userId, enabled); + SupervisionService.this.setSupervisionEnabledForUser( + userId, enabled, getSystemSupervisionPackage()); } @Override @@ -274,6 +349,7 @@ public class SupervisionService extends ISupervisionManager.Stub { } } + /** Deletes user data when the user gets removed. */ private final class UserLifecycleListener implements UserManagerInternal.UserLifecycleListener { @Override public void onUserRemoved(UserInfo user) { diff --git a/services/supervision/java/com/android/server/supervision/SupervisionServiceShellCommand.java b/services/supervision/java/com/android/server/supervision/SupervisionServiceShellCommand.java index 2adaae3943f1..976642bd563d 100644 --- a/services/supervision/java/com/android/server/supervision/SupervisionServiceShellCommand.java +++ b/services/supervision/java/com/android/server/supervision/SupervisionServiceShellCommand.java @@ -32,16 +32,18 @@ public class SupervisionServiceShellCommand extends ShellCommand { return handleDefaultCommands(null); } switch (cmd) { - case "enable": return setEnabled(true); - case "disable": return setEnabled(false); - default: return handleDefaultCommands(cmd); + case "enable": + return setEnabled(true); + case "disable": + return setEnabled(false); + default: + return handleDefaultCommands(cmd); } } private int setEnabled(boolean enabled) { - final var pw = getOutPrintWriter(); final var userId = UserHandle.parseUserArg(getNextArgRequired()); - mService.setSupervisionEnabledForUser(userId, enabled); + mService.mInternal.setSupervisionEnabledForUser(userId, enabled); return 0; } diff --git a/services/supervision/java/com/android/server/supervision/SupervisionUserData.java b/services/supervision/java/com/android/server/supervision/SupervisionUserData.java index 1dd48f581bf4..06acb91509a1 100644 --- a/services/supervision/java/com/android/server/supervision/SupervisionUserData.java +++ b/services/supervision/java/com/android/server/supervision/SupervisionUserData.java @@ -26,6 +26,7 @@ import android.util.IndentingPrintWriter; public class SupervisionUserData { public final @UserIdInt int userId; public boolean supervisionEnabled; + @Nullable public String supervisionAppPackage; public boolean supervisionLockScreenEnabled; @Nullable public PersistableBundle supervisionLockScreenOptions; @@ -38,6 +39,7 @@ public class SupervisionUserData { pw.println("User " + userId + ":"); pw.increaseIndent(); pw.println("supervisionEnabled: " + supervisionEnabled); + pw.println("supervisionAppPackage: " + supervisionAppPackage); pw.println("supervisionLockScreenEnabled: " + supervisionLockScreenEnabled); pw.println("supervisionLockScreenOptions: " + supervisionLockScreenOptions); pw.decreaseIndent(); diff --git a/services/tests/displayservicetests/src/com/android/server/display/plugin/PluginManagerTest.kt b/services/tests/displayservicetests/src/com/android/server/display/plugin/PluginManagerTest.kt index 01061f16c279..d9224eaf66ca 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/plugin/PluginManagerTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/plugin/PluginManagerTest.kt @@ -29,6 +29,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever private val TEST_PLUGIN_TYPE = PluginType(Int::class.java, "test_type") +private val DISPLAY_ID = "display_id" @SmallTest class PluginManagerTest { @@ -62,18 +63,18 @@ class PluginManagerTest { fun testSubscribe() { val pluginManager = createPluginManager() - pluginManager.subscribe(TEST_PLUGIN_TYPE, mockListener) + pluginManager.subscribe(TEST_PLUGIN_TYPE, DISPLAY_ID, mockListener) - verify(testInjector.mockStorage).addListener(TEST_PLUGIN_TYPE, mockListener) + verify(testInjector.mockStorage).addListener(TEST_PLUGIN_TYPE, DISPLAY_ID, mockListener) } @Test fun testUnsubscribe() { val pluginManager = createPluginManager() - pluginManager.unsubscribe(TEST_PLUGIN_TYPE, mockListener) + pluginManager.unsubscribe(TEST_PLUGIN_TYPE, DISPLAY_ID, mockListener) - verify(testInjector.mockStorage).removeListener(TEST_PLUGIN_TYPE, mockListener) + verify(testInjector.mockStorage).removeListener(TEST_PLUGIN_TYPE, DISPLAY_ID, mockListener) } private fun createPluginManager(enabled: Boolean = true): PluginManager { diff --git a/services/tests/displayservicetests/src/com/android/server/display/plugin/PluginStorageTest.kt b/services/tests/displayservicetests/src/com/android/server/display/plugin/PluginStorageTest.kt index 218e34134e93..8eb3e9fbf9b0 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/plugin/PluginStorageTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/plugin/PluginStorageTest.kt @@ -23,6 +23,8 @@ import org.junit.Test private val TEST_PLUGIN_TYPE1 = PluginType(String::class.java, "test_type1") private val TEST_PLUGIN_TYPE2 = PluginType(String::class.java, "test_type2") +private val DISPLAY_ID_1 = "display_1" +private val DISPLAY_ID_2 = "display_2" @SmallTest class PluginStorageTest { @@ -33,9 +35,9 @@ class PluginStorageTest { fun testUpdateValue() { val type1Value = "value1" val testChangeListener = TestPluginChangeListener<String>() - storage.addListener(TEST_PLUGIN_TYPE1, testChangeListener) + storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, testChangeListener) - storage.updateValue(TEST_PLUGIN_TYPE1, type1Value) + storage.updateValue(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, type1Value) assertThat(testChangeListener.receivedValue).isEqualTo(type1Value) } @@ -44,9 +46,9 @@ class PluginStorageTest { fun testAddListener() { val type1Value = "value1" val testChangeListener = TestPluginChangeListener<String>() - storage.updateValue(TEST_PLUGIN_TYPE1, type1Value) + storage.updateValue(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, type1Value) - storage.addListener(TEST_PLUGIN_TYPE1, testChangeListener) + storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, testChangeListener) assertThat(testChangeListener.receivedValue).isEqualTo(type1Value) } @@ -55,10 +57,10 @@ class PluginStorageTest { fun testRemoveListener() { val type1Value = "value1" val testChangeListener = TestPluginChangeListener<String>() - storage.addListener(TEST_PLUGIN_TYPE1, testChangeListener) - storage.removeListener(TEST_PLUGIN_TYPE1, testChangeListener) + storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, testChangeListener) + storage.removeListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, testChangeListener) - storage.updateValue(TEST_PLUGIN_TYPE1, type1Value) + storage.updateValue(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, type1Value) assertThat(testChangeListener.receivedValue).isNull() } @@ -68,10 +70,10 @@ class PluginStorageTest { val type1Value = "value1" val type2Value = "value2" val testChangeListener = TestPluginChangeListener<String>() - storage.updateValue(TEST_PLUGIN_TYPE1, type1Value) - storage.updateValue(TEST_PLUGIN_TYPE2, type2Value) + storage.updateValue(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, type1Value) + storage.updateValue(TEST_PLUGIN_TYPE2, DISPLAY_ID_1, type2Value) - storage.addListener(TEST_PLUGIN_TYPE1, testChangeListener) + storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, testChangeListener) assertThat(testChangeListener.receivedValue).isEqualTo(type1Value) } @@ -81,15 +83,62 @@ class PluginStorageTest { val type1Value = "value1" val testChangeListener1 = TestPluginChangeListener<String>() val testChangeListener2 = TestPluginChangeListener<String>() - storage.addListener(TEST_PLUGIN_TYPE1, testChangeListener1) - storage.addListener(TEST_PLUGIN_TYPE2, testChangeListener2) + storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, testChangeListener1) + storage.addListener(TEST_PLUGIN_TYPE2, DISPLAY_ID_1, testChangeListener2) - storage.updateValue(TEST_PLUGIN_TYPE1, type1Value) + storage.updateValue(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, type1Value) assertThat(testChangeListener1.receivedValue).isEqualTo(type1Value) assertThat(testChangeListener2.receivedValue).isNull() } + @Test + fun testUpdateGlobal_noDisplaySpecificValue() { + val type1Value = "value1" + val testChangeListener1 = TestPluginChangeListener<String>() + val testChangeListener2 = TestPluginChangeListener<String>() + storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, testChangeListener1) + storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_2, testChangeListener2) + + storage.updateGlobalValue(TEST_PLUGIN_TYPE1, type1Value) + + assertThat(testChangeListener1.receivedValue).isEqualTo(type1Value) + assertThat(testChangeListener2.receivedValue).isEqualTo(type1Value) + } + + @Test + fun testUpdateGlobal_withDisplaySpecificValue() { + val type1Value = "value1" + val type1GlobalValue = "value1Global" + val testChangeListener1 = TestPluginChangeListener<String>() + val testChangeListener2 = TestPluginChangeListener<String>() + storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, testChangeListener1) + storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_2, testChangeListener2) + + storage.updateValue(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, type1Value) + storage.updateGlobalValue(TEST_PLUGIN_TYPE1, type1GlobalValue) + + assertThat(testChangeListener1.receivedValue).isEqualTo(type1Value) + assertThat(testChangeListener2.receivedValue).isEqualTo(type1GlobalValue) + } + + @Test + fun testUpdateGlobal_withDisplaySpecificValueRemoved() { + val type1Value = "value1" + val type1GlobalValue = "value1Global" + val testChangeListener1 = TestPluginChangeListener<String>() + val testChangeListener2 = TestPluginChangeListener<String>() + storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, testChangeListener1) + storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_2, testChangeListener2) + + storage.updateValue(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, type1Value) + storage.updateGlobalValue(TEST_PLUGIN_TYPE1, type1GlobalValue) + storage.updateValue(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, null) + + assertThat(testChangeListener1.receivedValue).isEqualTo(type1GlobalValue) + assertThat(testChangeListener2.receivedValue).isEqualTo(type1GlobalValue) + } + private class TestPluginChangeListener<T> : PluginChangeListener<T> { var receivedValue: T? = null diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java index 8dc8c14f8948..cb52f1849b5b 100644 --- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java @@ -151,6 +151,7 @@ import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; import android.platform.test.flag.util.FlagSetException; @@ -192,7 +193,9 @@ import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; +import org.mockito.InOrder; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.quality.Strictness; import org.mockito.stubbing.Answer; @@ -435,6 +438,7 @@ public final class AlarmManagerServiceTest { private void disableFlagsNotSetByAnnotation() { try { mSetFlagsRule.disableFlags(Flags.FLAG_START_USER_BEFORE_SCHEDULED_ALARMS); + mSetFlagsRule.disableFlags(Flags.FLAG_ACQUIRE_WAKELOCK_BEFORE_SEND); } catch (FlagSetException fse) { // Expected if the test about to be run requires this enabled. } @@ -948,6 +952,28 @@ public final class AlarmManagerServiceTest { } @Test + @EnableFlags(Flags.FLAG_ACQUIRE_WAKELOCK_BEFORE_SEND) + public void testWakelockOrdering() throws Exception { + final long triggerTime = mNowElapsedTest + 5000; + final PendingIntent alarmPi = getNewMockPendingIntent(); + setTestAlarm(ELAPSED_REALTIME_WAKEUP, triggerTime, alarmPi); + + mNowElapsedTest = mTestTimer.getElapsed(); + mTestTimer.expire(); + + final InOrder inOrder = Mockito.inOrder(alarmPi, mWakeLock); + inOrder.verify(mWakeLock).acquire(); + + final ArgumentCaptor<PendingIntent.OnFinished> onFinishedCaptor = + ArgumentCaptor.forClass(PendingIntent.OnFinished.class); + inOrder.verify(alarmPi).send(eq(mMockContext), eq(0), any(Intent.class), + onFinishedCaptor.capture(), any(Handler.class), isNull(), any()); + onFinishedCaptor.getValue().onSendFinished(alarmPi, null, 0, null, null); + + inOrder.verify(mWakeLock).release(); + } + + @Test public void testMinFuturityCoreUid() { setDeviceConfigLong(KEY_MIN_FUTURITY, 10L); assertEquals(10, mService.mConstants.MIN_FUTURITY); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java index 27eada013642..d6349fc0651f 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java @@ -18,6 +18,7 @@ package com.android.server.am; import static android.os.PowerWhitelistManager.REASON_NOTIFICATION_SERVICE; import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED; +import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED; import static android.os.Process.INVALID_UID; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; @@ -31,9 +32,10 @@ import static com.android.server.am.PendingIntentRecord.CANCEL_REASON_SUPERSEDED import static com.android.server.am.PendingIntentRecord.CANCEL_REASON_USER_STOPPED; import static com.android.server.am.PendingIntentRecord.FLAG_ACTIVITY_SENDER; import static com.android.server.am.PendingIntentRecord.cancelReasonToString; +import static com.android.window.flags.Flags.balClearAllowlistDuration; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertNotNull; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; @@ -207,10 +209,17 @@ public class PendingIntentControllerTest { PendingIntentRecord.TempAllowListDuration allowlistDurationLocked = pir.getAllowlistDurationLocked(token); assertEquals(1000, allowlistDurationLocked.duration); + assertEquals(TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED, + allowlistDurationLocked.type); pir.clearAllowBgActivityStarts(token); PendingIntentRecord.TempAllowListDuration allowlistDurationLockedAfterClear = pir.getAllowlistDurationLocked(token); - assertNull(allowlistDurationLockedAfterClear); + assertNotNull(allowlistDurationLockedAfterClear); + assertEquals(1000, allowlistDurationLockedAfterClear.duration); + assertEquals(balClearAllowlistDuration() + ? TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED + : TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED, + allowlistDurationLocked.type); } private void assertCancelReason(int expectedReason, int actualReason) { diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java index 1cf655675a30..b92afc5c0ca7 100644 --- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java @@ -144,6 +144,8 @@ public class WallpaperManagerServiceTests { private static ComponentName sImageWallpaperComponentName; private static ComponentName sDefaultWallpaperComponent; + private static ComponentName sFallbackWallpaperComponentName; + private IPackageManager mIpm = AppGlobals.getPackageManager(); @Mock @@ -195,6 +197,8 @@ public class WallpaperManagerServiceTests { sContext.getResources().getString(R.string.image_wallpaper_component)); // Mock default wallpaper as image wallpaper if there is no pre-defined default wallpaper. sDefaultWallpaperComponent = WallpaperManager.getCmfDefaultWallpaperComponent(sContext); + sFallbackWallpaperComponentName = ComponentName.unflattenFromString( + sContext.getResources().getString(R.string.fallback_wallpaper_component)); if (sDefaultWallpaperComponent == null) { sDefaultWallpaperComponent = sImageWallpaperComponentName; @@ -205,6 +209,9 @@ public class WallpaperManagerServiceTests { } sContext.addMockService(sImageWallpaperComponentName, sWallpaperService); + if (sFallbackWallpaperComponentName != null) { + sContext.addMockService(sFallbackWallpaperComponentName, sWallpaperService); + } } @AfterClass @@ -216,6 +223,7 @@ public class WallpaperManagerServiceTests { LocalServices.removeServiceForTest(WindowManagerInternal.class); sImageWallpaperComponentName = null; sDefaultWallpaperComponent = null; + sFallbackWallpaperComponentName = null; reset(sContext); } @@ -306,6 +314,7 @@ public class WallpaperManagerServiceTests { * Tests that internal basic data should be correct after boot up. */ @Test + @DisableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER) public void testDataCorrectAfterBoot() { mService.switchUser(USER_SYSTEM, null); @@ -719,7 +728,7 @@ public class WallpaperManagerServiceTests { final int testDisplayId = 2; setUpDisplays(List.of(DEFAULT_DISPLAY, testDisplayId)); - // WHEN displayId, 2, is ready. + // WHEN display ID, 2, is ready. WallpaperManagerInternal wallpaperManagerInternal = LocalServices.getService( WallpaperManagerInternal.class); wallpaperManagerInternal.onDisplayReady(testDisplayId); @@ -759,7 +768,7 @@ public class WallpaperManagerServiceTests { final int testDisplayId = 2; setUpDisplays(List.of(DEFAULT_DISPLAY, testDisplayId)); - // WHEN displayId, 2, is ready. + // WHEN display ID, 2, is ready. WallpaperManagerInternal wallpaperManagerInternal = LocalServices.getService( WallpaperManagerInternal.class); wallpaperManagerInternal.onDisplayReady(testDisplayId); @@ -791,6 +800,40 @@ public class WallpaperManagerServiceTests { /* info= */ any(), /* description= */ any()); } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER) + public void displayAdded_wallpaperIncompatibleForDisplay_shouldAttachFallbackWallpaperService() + throws Exception { + final int testUserId = USER_SYSTEM; + mService.switchUser(testUserId, null); + IWallpaperService mockIWallpaperService = mock(IWallpaperService.class); + mService.mFallbackWallpaper.connection.mService = mockIWallpaperService; + // GIVEN there are two displays: DEFAULT_DISPLAY, 2 + final int testDisplayId = 2; + setUpDisplays(List.of(DEFAULT_DISPLAY, testDisplayId)); + // GIVEN the wallpaper isn't compatible with display ID, 2 + mService.removeWallpaperCompatibleDisplayForTest(testDisplayId); + + // WHEN display ID, 2, is ready. + WallpaperManagerInternal wallpaperManagerInternal = LocalServices.getService( + WallpaperManagerInternal.class); + wallpaperManagerInternal.onDisplayReady(testDisplayId); + + // Then there is a connection established for the fallback wallpaper for display ID, 2. + verify(mockIWallpaperService).attach( + /* connection= */ eq(mService.mFallbackWallpaper.connection), + /* windowToken= */ any(), + /* windowType= */ anyInt(), + /* isPreview= */ anyBoolean(), + /* reqWidth= */ anyInt(), + /* reqHeight= */ anyInt(), + /* padding= */ any(), + /* displayId= */ eq(testDisplayId), + /* which= */ eq(FLAG_SYSTEM | FLAG_LOCK), + /* info= */ any(), + /* description= */ any()); + } // Verify a secondary display added end // Verify a secondary display removed started @@ -810,7 +853,7 @@ public class WallpaperManagerServiceTests { // GIVEN there are two displays: DEFAULT_DISPLAY, 2 final int testDisplayId = 2; setUpDisplays(List.of(DEFAULT_DISPLAY, testDisplayId)); - // GIVEN wallpaper connections have been established for displayID, 2. + // GIVEN wallpaper connections have been established for display ID, 2. WallpaperManagerInternal wallpaperManagerInternal = LocalServices.getService( WallpaperManagerInternal.class); wallpaperManagerInternal.onDisplayReady(testDisplayId); @@ -818,11 +861,11 @@ public class WallpaperManagerServiceTests { WallpaperManagerService.DisplayConnector displayConnector = wallpaper.connection.getDisplayConnectorOrCreate(testDisplayId); - // WHEN displayId, 2, is removed. + // WHEN display ID, 2, is removed. DisplayListener displayListener = displayListenerCaptor.getValue(); displayListener.onDisplayRemoved(testDisplayId); - // Then the wallpaper connection for displayId, 2, is detached. + // Then the wallpaper connection for display ID, 2, is detached. verify(mockIWallpaperService).detach(eq(displayConnector.mToken)); } @@ -848,26 +891,142 @@ public class WallpaperManagerServiceTests { // GIVEN there are two displays: DEFAULT_DISPLAY, 2 final int testDisplayId = 2; setUpDisplays(List.of(DEFAULT_DISPLAY, testDisplayId)); - // GIVEN wallpaper connections have been established for displayID, 2. + // GIVEN wallpaper connections have been established for display ID, 2. WallpaperManagerInternal wallpaperManagerInternal = LocalServices.getService( WallpaperManagerInternal.class); wallpaperManagerInternal.onDisplayReady(testDisplayId); - // Save displayConnectors for displayId 2 before display removal. + // Save displayConnectors for display ID, 2, before display removal. WallpaperManagerService.DisplayConnector systemWallpaperDisplayConnector = systemWallpaper.connection.getDisplayConnectorOrCreate(testDisplayId); WallpaperManagerService.DisplayConnector lockWallpaperDisplayConnector = lockWallpaper.connection.getDisplayConnectorOrCreate(testDisplayId); + // WHEN display ID, 2, is removed. + DisplayListener displayListener = displayListenerCaptor.getValue(); + displayListener.onDisplayRemoved(testDisplayId); + + // Then the system wallpaper connection for display ID, 2, is detached. + verify(mockIWallpaperService).detach(eq(systemWallpaperDisplayConnector.mToken)); + // Then the lock wallpaper connection for display ID, 2, is detached. + verify(mockIWallpaperService).detach(eq(lockWallpaperDisplayConnector.mToken)); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER) + public void displayRemoved_fallbackWallpaper_shouldDetachFallbackWallpaperService() + throws Exception { + ArgumentCaptor<DisplayListener> displayListenerCaptor = ArgumentCaptor.forClass( + DisplayListener.class); + verify(mDisplayManager).registerDisplayListener(displayListenerCaptor.capture(), eq(null)); + final int testUserId = USER_SYSTEM; + mService.switchUser(testUserId, null); + IWallpaperService mockIWallpaperService = mock(IWallpaperService.class); + mService.mFallbackWallpaper.connection.mService = mockIWallpaperService; + // GIVEN there are two displays: DEFAULT_DISPLAY, 2 + final int testDisplayId = 2; + setUpDisplays(List.of(DEFAULT_DISPLAY, testDisplayId)); + // GIVEN display ID, 2, is incompatible with the wallpaper. + mService.removeWallpaperCompatibleDisplayForTest(testDisplayId); + // GIVEN wallpaper connections have been established for display ID, 2. + WallpaperManagerInternal wallpaperManagerInternal = LocalServices.getService( + WallpaperManagerInternal.class); + wallpaperManagerInternal.onDisplayReady(testDisplayId); + // Save fallback wallpaper displayConnector for display ID, 2, before display removal. + WallpaperManagerService.DisplayConnector fallbackWallpaperConnector = + mService.mFallbackWallpaper.connection.getDisplayConnectorOrCreate(testDisplayId); + // WHEN displayId, 2, is removed. DisplayListener displayListener = displayListenerCaptor.getValue(); displayListener.onDisplayRemoved(testDisplayId); + // Then the fallback wallpaper connection for display ID, 2, is detached. + verify(mockIWallpaperService).detach(eq(fallbackWallpaperConnector.mToken)); + } + // Verify a secondary display removed ended + + // Test fallback wallpaper after enabling connected display supports. + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER) + public void testFallbackWallpaperForConnectedDisplays() { + final WallpaperData fallbackData = mService.mFallbackWallpaper; + + assertWithMessage( + "The connected wallpaper component should be fallback wallpaper component from " + + "config file.") + .that(fallbackData.connection.mWallpaper.getComponent()) + .isEqualTo(sFallbackWallpaperComponentName); + assertWithMessage("Fallback wallpaper should support both lock & system.") + .that(fallbackData.mWhich) + .isEqualTo(FLAG_LOCK | FLAG_SYSTEM); + } + + // Verify a secondary display removes system decorations started + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER) + public void displayRemoveSystemDecorations_sameSystemAndLockWallpaper_shouldDetachWallpaperServiceOnce() + throws Exception { + // GIVEN the same wallpaper used for the lock and system. + final int testUserId = USER_SYSTEM; + mService.switchUser(testUserId, null); + final WallpaperData wallpaper = mService.getCurrentWallpaperData(FLAG_SYSTEM, testUserId); + IWallpaperService mockIWallpaperService = mock(IWallpaperService.class); + wallpaper.connection.mService = mockIWallpaperService; + // GIVEN there are two displays: DEFAULT_DISPLAY, 2 + final int testDisplayId = 2; + setUpDisplays(List.of(DEFAULT_DISPLAY, testDisplayId)); + // GIVEN wallpaper connections have been established for displayID, 2. + WallpaperManagerInternal wallpaperManagerInternal = LocalServices.getService( + WallpaperManagerInternal.class); + wallpaperManagerInternal.onDisplayReady(testDisplayId); + // Save displayConnector for displayId 2 before display removal. + WallpaperManagerService.DisplayConnector displayConnector = + wallpaper.connection.getDisplayConnectorOrCreate(testDisplayId); + + // WHEN displayId, 2, is removed. + wallpaperManagerInternal.onDisplayRemoveSystemDecorations(testDisplayId); + + // Then the wallpaper connection for displayId, 2, is detached. + verify(mockIWallpaperService).detach(eq(displayConnector.mToken)); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER) + public void displayRemoveSystemDecorations_differentSystemAndLockWallpapers_shouldDetachWallpaperServiceTwice() + throws Exception { + // GIVEN different wallpapers used for the lock and system. + final int testUserId = USER_SYSTEM; + mService.switchUser(testUserId, null); + mService.setWallpaperComponent(sImageWallpaperComponentName, sContext.getOpPackageName(), + FLAG_LOCK, testUserId); + final WallpaperData systemWallpaper = mService.getCurrentWallpaperData(FLAG_SYSTEM, + testUserId); + final WallpaperData lockWallpaper = mService.getCurrentWallpaperData(FLAG_LOCK, + testUserId); + IWallpaperService mockIWallpaperService = mock(IWallpaperService.class); + systemWallpaper.connection.mService = mockIWallpaperService; + lockWallpaper.connection.mService = mockIWallpaperService; + // GIVEN there are two displays: DEFAULT_DISPLAY, 2 + final int testDisplayId = 2; + setUpDisplays(List.of(DEFAULT_DISPLAY, testDisplayId)); + // GIVEN wallpaper connections have been established for displayID, 2. + WallpaperManagerInternal wallpaperManagerInternal = LocalServices.getService( + WallpaperManagerInternal.class); + wallpaperManagerInternal.onDisplayReady(testDisplayId); + // Save displayConnectors for displayId 2 before display removal. + WallpaperManagerService.DisplayConnector systemWallpaperDisplayConnector = + systemWallpaper.connection.getDisplayConnectorOrCreate(testDisplayId); + WallpaperManagerService.DisplayConnector lockWallpaperDisplayConnector = + lockWallpaper.connection.getDisplayConnectorOrCreate(testDisplayId); + + // WHEN displayId, 2, is removed. + wallpaperManagerInternal.onDisplayRemoveSystemDecorations(testDisplayId); + // Then the system wallpaper connection for displayId, 2, is detached. verify(mockIWallpaperService).detach(eq(systemWallpaperDisplayConnector.mToken)); // Then the lock wallpaper connection for displayId, 2, is detached. verify(mockIWallpaperService).detach(eq(lockWallpaperDisplayConnector.mToken)); } - // Verify a secondary display removed ended + // Verify a secondary display removes system decorations ended // Verify that after continue switch user from userId 0 to lastUserId, the wallpaper data for // non-current user must not bind to wallpaper service. @@ -893,11 +1052,11 @@ public class WallpaperManagerServiceTests { final WallpaperData lastLockData = mService.mLastLockWallpaper; assertWithMessage("Last wallpaper must not be null").that(lastLockData).isNotNull(); assertWithMessage("Last wallpaper component must be equals.") - .that(expectedComponent) - .isEqualTo(lastLockData.getComponent()); + .that(lastLockData.getComponent()) + .isEqualTo(expectedComponent); assertWithMessage("The user id in last wallpaper should be the last switched user") - .that(lastUserId) - .isEqualTo(lastLockData.userId); + .that(lastLockData.userId) + .isEqualTo(lastUserId); assertWithMessage("Must exist user data connection on last wallpaper data") .that(lastLockData.connection) .isNotNull(); @@ -946,6 +1105,7 @@ public class WallpaperManagerServiceTests { doReturn(mockDisplay).when(mDisplayManager).getDisplay(eq(displayId)); doReturn(displayId).when(mockDisplay).getDisplayId(); doReturn(true).when(mockDisplay).hasAccess(anyInt()); + mService.addWallpaperCompatibleDisplayForTest(displayId); } doReturn(mockDisplays).when(mDisplayManager).getDisplays(); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java index d427c9d9ee37..e94ef5bb4871 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java @@ -39,6 +39,8 @@ import android.os.Handler; import android.os.Parcel; import android.os.Process; import android.os.UidBatteryConsumer; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.platform.test.ravenwood.RavenwoodRule; import android.util.SparseLongArray; @@ -49,6 +51,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.internal.os.BatteryStatsHistoryIterator; import com.android.internal.os.MonotonicClock; import com.android.internal.os.PowerProfile; +import com.android.server.power.optimization.Flags; import com.android.server.power.stats.processor.MultiStatePowerAttributor; import org.junit.Before; @@ -59,6 +62,7 @@ import org.junit.runner.RunWith; import java.io.File; import java.io.IOException; import java.util.List; +import java.util.concurrent.TimeUnit; @SmallTest @RunWith(AndroidJUnit4.class) @@ -68,11 +72,14 @@ public class BatteryUsageStatsProviderTest { .setProvideMainThread(true) .build(); + @Rule(order = 1) + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42; private static final long MINUTE_IN_MS = 60 * 1000; private static final double PRECISION = 0.00001; - @Rule(order = 1) + @Rule(order = 2) public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule(12345) .createTempDirectory() @@ -868,4 +875,62 @@ public class BatteryUsageStatsProviderTest { stats.close(); } + + @Test + @EnableFlags(Flags.FLAG_EXTENDED_BATTERY_HISTORY_CONTINUOUS_COLLECTION_ENABLED) + public void testIncludeSubsetOfHistory() throws IOException { + MockBatteryStatsImpl batteryStats = mStatsRule.getBatteryStats(); + batteryStats.getHistory().setMaxHistoryBufferSize(100); + synchronized (batteryStats) { + batteryStats.setRecordAllHistoryLocked(true); + } + batteryStats.forceRecordAllHistory(); + batteryStats.setNoAutoReset(true); + + long lastIncludedEventTimestamp = 0; + String tag = "work work work work work work work work work work work work work work work"; + for (int i = 1; i < 50; i++) { + mStatsRule.advanceTime(TimeUnit.MINUTES.toMillis(9)); + synchronized (batteryStats) { + batteryStats.noteJobStartLocked(tag, 42); + } + mStatsRule.advanceTime(TimeUnit.MINUTES.toMillis(1)); + synchronized (batteryStats) { + batteryStats.noteJobFinishLocked(tag, 42, 0); + } + lastIncludedEventTimestamp = mMonotonicClock.monotonicTime(); + } + + BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext, + mock(PowerAttributor.class), mStatsRule.getPowerProfile(), + mStatsRule.getCpuScalingPolicies(), mock(PowerStatsStore.class), 0, mMockClock, + mMonotonicClock); + + BatteryUsageStatsQuery query = new BatteryUsageStatsQuery.Builder() + .includeBatteryHistory() + .setPreferredHistoryDurationMs(TimeUnit.MINUTES.toMillis(20)) + .build(); + final BatteryUsageStats stats = provider.getBatteryUsageStats(batteryStats, query); + Parcel parcel = Parcel.obtain(); + stats.writeToParcel(parcel, 0); + stats.close(); + + parcel.setDataPosition(0); + BatteryUsageStats actual = BatteryUsageStats.CREATOR.createFromParcel(parcel); + + long firstIncludedEventTimestamp = 0; + try (BatteryStatsHistoryIterator it = actual.iterateBatteryStatsHistory()) { + BatteryStats.HistoryItem item; + while ((item = it.next()) != null) { + if (item.eventCode == BatteryStats.HistoryItem.EVENT_JOB_START) { + firstIncludedEventTimestamp = item.time; + break; + } + } + } + actual.close(); + + assertThat(firstIncludedEventTimestamp) + .isAtLeast(lastIncludedEventTimestamp - TimeUnit.MINUTES.toMillis(30)); + } } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterTest.java index ecc48bfc40e4..f55ca0c0b12b 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterTest.java @@ -57,6 +57,7 @@ import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import com.android.server.LocalServices; +import com.android.server.accessibility.autoclick.AutoclickController; import com.android.server.accessibility.gestures.TouchExplorer; import com.android.server.accessibility.magnification.FullScreenMagnificationGestureHandler; import com.android.server.accessibility.magnification.MagnificationGestureHandler; diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java index 5602fb76e6f5..28e5be505556 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java @@ -68,7 +68,6 @@ import android.annotation.UserIdInt; import android.app.PendingIntent; import android.app.RemoteAction; import android.app.admin.DevicePolicyManager; -import android.app.ecm.EnhancedConfirmationManager; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -2120,23 +2119,6 @@ public class AccessibilityManagerServiceTest { } @Test - @EnableFlags({android.permission.flags.Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED, - android.security.Flags.FLAG_EXTEND_ECM_TO_ALL_SETTINGS}) - public void isAccessibilityTargetAllowed_nonSystemUserId_useEcmWithNonSystemUserId() { - String fakePackageName = "FAKE_PACKAGE_NAME"; - int uid = 0; // uid is not used in the actual implementation when flags are on - int userId = mTestableContext.getUserId() + 1234; - when(mDevicePolicyManager.getPermittedAccessibilityServices(userId)).thenReturn( - List.of(fakePackageName)); - Context mockUserContext = mock(Context.class); - mTestableContext.addMockUserContext(userId, mockUserContext); - - mA11yms.isAccessibilityTargetAllowed(fakePackageName, uid, userId); - - verify(mockUserContext).getSystemService(EnhancedConfirmationManager.class); - } - - @Test @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES) public void handleKeyGestureEvent_toggleMagnifier() { mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AutoclickControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java index acce813ff659..f02bdae1d9e6 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AutoclickControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.server.accessibility; +package com.android.server.accessibility.autoclick; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; @@ -39,6 +39,8 @@ import android.view.MotionEvent; import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; +import com.android.server.accessibility.AccessibilityTraceManager; + import org.junit.After; import org.junit.Before; import org.junit.Rule; diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java index 4ef602f1a64c..3511ae12497a 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java @@ -58,9 +58,9 @@ import android.graphics.Rect; import android.graphics.Region; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManagerInternal; -import android.os.Looper; import android.os.RemoteException; import android.os.UserHandle; +import android.os.test.TestLooper; import android.provider.Settings; import android.test.mock.MockContentResolver; import android.testing.DexmakerShareClassLoaderRule; @@ -173,6 +173,8 @@ public class MagnificationControllerTest { @Mock private Scroller mMockScroller; + private TestLooper mTestLooper; + // To mock package-private class @Rule public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule = @@ -199,14 +201,16 @@ public class MagnificationControllerTest { mMockResolver = new MockContentResolver(); mMockResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); - Looper looper = InstrumentationRegistry.getContext().getMainLooper(); - // Pretending ID of the Thread associated with looper as main thread ID in controller - when(mContext.getMainLooper()).thenReturn(looper); + mTestLooper = new TestLooper(); + when(mContext.getMainLooper()).thenReturn( + InstrumentationRegistry.getContext().getMainLooper()); when(mContext.getContentResolver()).thenReturn(mMockResolver); when(mContext.getPackageManager()).thenReturn(mPackageManager); Settings.Secure.putFloatForUser(mMockResolver, Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, DEFAULT_SCALE, CURRENT_USER_ID); + Settings.Secure.putFloatForUser(mMockResolver, Settings.Secure.KEY_REPEAT_ENABLED, 1, + CURRENT_USER_ID); mScaleProvider = spy(new MagnificationScaleProvider(mContext)); when(mControllerCtx.getContext()).thenReturn(mContext); @@ -251,7 +255,7 @@ public class MagnificationControllerTest { mMagnificationController = spy(new MagnificationController(mService, globalLock, mContext, mScreenMagnificationController, mMagnificationConnectionManager, mScaleProvider, - ConcurrentUtils.DIRECT_EXECUTOR)); + ConcurrentUtils.DIRECT_EXECUTOR, mTestLooper.getLooper())); mMagnificationController.setMagnificationCapabilities( Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ALL); @@ -261,6 +265,7 @@ public class MagnificationControllerTest { @After public void tearDown() { + mTestLooper.dispatchAll(); FakeSettingsProvider.clearSettingsProvider(); } @@ -880,6 +885,69 @@ public class MagnificationControllerTest { } @Test + public void magnificationCallbacks_panMagnificationContinuous() throws RemoteException { + setMagnificationEnabled(MODE_FULLSCREEN); + mMagnificationController.onPerformScaleAction(TEST_DISPLAY, 8.0f, false); + reset(mScreenMagnificationController); + + DisplayMetrics metrics = new DisplayMetrics(); + mDisplay.getMetrics(metrics); + float expectedStep = 27 * metrics.density; + + float currentCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY); + float currentCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY); + + // Start moving right using keyboard callbacks. + mMagnificationController.onPanMagnificationStart(TEST_DISPLAY, + MagnificationController.PAN_DIRECTION_RIGHT); + + float newCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY); + float newCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY); + expect.that(currentCenterX).isLessThan(newCenterX); + expect.that(newCenterX - currentCenterX).isWithin(0.01f).of(expectedStep); + expect.that(currentCenterY).isEqualTo(newCenterY); + + currentCenterX = newCenterX; + currentCenterY = newCenterY; + + // Wait for the initial delay to occur. + advanceTime(MagnificationController.INITIAL_KEYBOARD_REPEAT_INTERVAL_MS + 1); + + // It should have moved again after the handler was triggered. + newCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY); + newCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY); + expect.that(currentCenterX).isLessThan(newCenterX); + expect.that(newCenterX - currentCenterX).isWithin(0.01f).of(expectedStep); + expect.that(currentCenterY).isEqualTo(newCenterY); + currentCenterX = newCenterX; + currentCenterY = newCenterY; + + // Wait for repeat delay to occur. + advanceTime(MagnificationController.KEYBOARD_REPEAT_INTERVAL_MS + 1); + + // It should have moved a third time. + newCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY); + newCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY); + expect.that(currentCenterX).isLessThan(newCenterX); + expect.that(newCenterX - currentCenterX).isWithin(0.01f).of(expectedStep); + expect.that(currentCenterY).isEqualTo(newCenterY); + currentCenterX = newCenterX; + currentCenterY = newCenterY; + + // Stop magnification pan. + mMagnificationController.onPanMagnificationStop(TEST_DISPLAY, + MagnificationController.PAN_DIRECTION_RIGHT); + + // It should not move again, even after the appropriate delay. + advanceTime(MagnificationController.KEYBOARD_REPEAT_INTERVAL_MS + 1); + + newCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY); + newCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY); + expect.that(newCenterX).isEqualTo(currentCenterX); + expect.that(newCenterY).isEqualTo(currentCenterY); + } + + @Test public void enableWindowMode_notifyMagnificationChanged() throws RemoteException { setMagnificationEnabled(MODE_WINDOW); @@ -1196,7 +1264,8 @@ public class MagnificationControllerTest { assertEquals(ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN, lastActivatedMode); } - @Test public void activateFullScreenMagnification_triggerCallback() throws RemoteException { + @Test + public void activateFullScreenMagnification_triggerCallback() throws RemoteException { setMagnificationEnabled(MODE_FULLSCREEN); verify(mMagnificationController).onFullScreenMagnificationActivationState( eq(TEST_DISPLAY), eq(true)); @@ -1573,8 +1642,8 @@ public class MagnificationControllerTest { float currentCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY); float currentCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY); - // Move right. - mMagnificationController.panMagnificationByStep(TEST_DISPLAY, + // Move right using keyboard callbacks. + mMagnificationController.onPanMagnificationStart(TEST_DISPLAY, MagnificationController.PAN_DIRECTION_RIGHT); float newCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY); float newCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY); @@ -1582,11 +1651,13 @@ public class MagnificationControllerTest { expect.that(newCenterX - currentCenterX).isWithin(0.01f).of(expectedStep); expect.that(currentCenterY).isEqualTo(newCenterY); + mMagnificationController.onPanMagnificationStop(TEST_DISPLAY, + MagnificationController.PAN_DIRECTION_RIGHT); currentCenterX = newCenterX; currentCenterY = newCenterY; // Move left. - mMagnificationController.panMagnificationByStep(TEST_DISPLAY, + mMagnificationController.onPanMagnificationStart(TEST_DISPLAY, MagnificationController.PAN_DIRECTION_LEFT); newCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY); newCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY); @@ -1594,11 +1665,13 @@ public class MagnificationControllerTest { expect.that(currentCenterX - newCenterX).isWithin(0.01f).of(expectedStep); expect.that(currentCenterY).isEqualTo(newCenterY); + mMagnificationController.onPanMagnificationStop(TEST_DISPLAY, + MagnificationController.PAN_DIRECTION_LEFT); currentCenterX = newCenterX; currentCenterY = newCenterY; // Move down. - mMagnificationController.panMagnificationByStep(TEST_DISPLAY, + mMagnificationController.onPanMagnificationStart(TEST_DISPLAY, MagnificationController.PAN_DIRECTION_DOWN); newCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY); newCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY); @@ -1606,17 +1679,22 @@ public class MagnificationControllerTest { expect.that(currentCenterY).isLessThan(newCenterY); expect.that(newCenterY - currentCenterY).isWithin(0.1f).of(expectedStep); + mMagnificationController.onPanMagnificationStop(TEST_DISPLAY, + MagnificationController.PAN_DIRECTION_DOWN); currentCenterX = newCenterX; currentCenterY = newCenterY; // Move up. - mMagnificationController.panMagnificationByStep(TEST_DISPLAY, + mMagnificationController.onPanMagnificationStart(TEST_DISPLAY, MagnificationController.PAN_DIRECTION_UP); newCenterX = mScreenMagnificationController.getCenterX(TEST_DISPLAY); newCenterY = mScreenMagnificationController.getCenterY(TEST_DISPLAY); expect.that(currentCenterX).isEqualTo(newCenterX); expect.that(currentCenterY).isGreaterThan(newCenterY); expect.that(currentCenterY - newCenterY).isWithin(0.01f).of(expectedStep); + + mMagnificationController.onPanMagnificationStop(TEST_DISPLAY, + MagnificationController.PAN_DIRECTION_UP); } private void testWindowMagnificationPanWithStepSize(float expectedStepDip) @@ -1626,28 +1704,41 @@ public class MagnificationControllerTest { final float expectedStep = expectedStepDip * metrics.density; // Move right. - mMagnificationController.panMagnificationByStep(TEST_DISPLAY, + mMagnificationController.onPanMagnificationStart(TEST_DISPLAY, MagnificationController.PAN_DIRECTION_RIGHT); verify(mMockConnection.getConnection()).moveWindowMagnifier(eq(TEST_DISPLAY), floatThat(step -> Math.abs(step - expectedStep) < 0.0001), eq(0.0f)); + mMagnificationController.onPanMagnificationStop(TEST_DISPLAY, + MagnificationController.PAN_DIRECTION_RIGHT); // Move left. - mMagnificationController.panMagnificationByStep(TEST_DISPLAY, + mMagnificationController.onPanMagnificationStart(TEST_DISPLAY, MagnificationController.PAN_DIRECTION_LEFT); verify(mMockConnection.getConnection()).moveWindowMagnifier(eq(TEST_DISPLAY), floatThat(step -> Math.abs(expectedStep - step) < 0.0001), eq(0.0f)); + mMagnificationController.onPanMagnificationStop(TEST_DISPLAY, + MagnificationController.PAN_DIRECTION_LEFT); // Move down. - mMagnificationController.panMagnificationByStep(TEST_DISPLAY, + mMagnificationController.onPanMagnificationStart(TEST_DISPLAY, MagnificationController.PAN_DIRECTION_DOWN); verify(mMockConnection.getConnection()).moveWindowMagnifier(eq(TEST_DISPLAY), eq(0.0f), floatThat(step -> Math.abs(expectedStep - step) < 0.0001)); + mMagnificationController.onPanMagnificationStop(TEST_DISPLAY, + MagnificationController.PAN_DIRECTION_DOWN); // Move up. - mMagnificationController.panMagnificationByStep(TEST_DISPLAY, + mMagnificationController.onPanMagnificationStart(TEST_DISPLAY, MagnificationController.PAN_DIRECTION_UP); verify(mMockConnection.getConnection()).moveWindowMagnifier(eq(TEST_DISPLAY), eq(0.0f), floatThat(step -> Math.abs(expectedStep - step) < 0.0001)); + mMagnificationController.onPanMagnificationStop(TEST_DISPLAY, + MagnificationController.PAN_DIRECTION_UP); + } + + private void advanceTime(long timeMs) { + mTestLooper.moveTimeForward(timeMs); + mTestLooper.dispatchAll(); } private static class WindowMagnificationMgrCallbackDelegate implements diff --git a/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java index ef9580c54de6..cbe2bfb26cd6 100644 --- a/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java @@ -28,8 +28,10 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.Manifest; import android.app.AppOpsManager; import android.content.Context; +import android.content.pm.PackageManager; import android.content.res.Resources; import android.media.AudioDeviceAttributes; import android.media.AudioDeviceInfo; @@ -38,6 +40,7 @@ import android.media.AudioManager; import android.media.AudioSystem; import android.media.IAudioDeviceVolumeDispatcher; import android.media.VolumeInfo; +import android.os.IpcDataCache; import android.os.PermissionEnforcer; import android.os.RemoteException; import android.os.test.TestLooper; @@ -83,6 +86,7 @@ public class AbsoluteVolumeBehaviorTest { @Before public void setUp() throws Exception { + IpcDataCache.disableForTestMode(); mTestLooper = new TestLooper(); mContext = spy(ApplicationProvider.getApplicationContext()); @@ -93,6 +97,10 @@ public class AbsoluteVolumeBehaviorTest { when(mResources.getBoolean(com.android.internal.R.bool.config_useFixedVolume)) .thenReturn(false); + when(mContext.checkCallingOrSelfPermission( + Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)) + .thenReturn(PackageManager.PERMISSION_GRANTED); + mSpyAudioSystem = spy(new NoOpAudioSystemAdapter()); mSystemServer = new NoOpSystemServerAdapter(); mSettingsAdapter = new NoOpSettingsAdapter(); diff --git a/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java index 746645a8c585..541dbba67957 100644 --- a/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java @@ -28,6 +28,7 @@ import android.media.AudioDeviceAttributes; import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.media.IDeviceVolumeBehaviorDispatcher; +import android.os.IpcDataCache; import android.os.PermissionEnforcer; import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; @@ -69,6 +70,7 @@ public class DeviceVolumeBehaviorTest { @Before public void setUp() throws Exception { + IpcDataCache.disableForTestMode(); mTestLooper = new TestLooper(); mContext = InstrumentationRegistry.getTargetContext(); mAudioSystem = new NoOpAudioSystemAdapter(); diff --git a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java index 6b41c434b80f..0bbae247d8bb 100644 --- a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java @@ -80,6 +80,7 @@ import android.media.AudioSystem; import android.media.IDeviceVolumeBehaviorDispatcher; import android.media.VolumeInfo; import android.media.audiopolicy.AudioVolumeGroup; +import android.os.IpcDataCache; import android.os.Looper; import android.os.PermissionEnforcer; import android.os.test.TestLooper; @@ -210,6 +211,8 @@ public class VolumeHelperTest { @Before public void setUp() throws Exception { + IpcDataCache.disableForTestMode(); + mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); mTestLooper = new TestLooper(); diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java index 3ced56a04138..a58a9cd2a28f 100644 --- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java @@ -34,7 +34,6 @@ import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -53,15 +52,11 @@ import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import static org.testng.Assert.assertThrows; -import android.Manifest; import android.annotation.SuppressLint; import android.app.ActivityManagerInternal; import android.app.ActivityOptions.LaunchCookie; import android.app.AppOpsManager; -import android.app.Instrumentation; import android.app.KeyguardManager; -import android.app.role.RoleManager; -import android.companion.AssociationRequest; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; @@ -76,11 +71,9 @@ import android.media.projection.StopReason; import android.os.Binder; import android.os.IBinder; import android.os.Looper; -import android.os.Process; import android.os.RemoteException; import android.os.UserHandle; import android.os.test.TestLooper; -import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; @@ -99,7 +92,6 @@ import com.android.server.testutils.OffsettableClock; import com.android.server.wm.WindowManagerInternal; import org.junit.After; -import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -110,7 +102,6 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -292,8 +283,6 @@ public class MediaProjectionManagerServiceTest { assertThat(stoppedCallback2).isFalse(); } - @EnableFlags(android.companion.virtualdevice.flags - .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS) @Test public void testCreateProjection_keyguardLocked() throws Exception { MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions(); @@ -308,8 +297,6 @@ public class MediaProjectionManagerServiceTest { assertThat(mIMediaProjectionCallback.mLatch.await(5, TimeUnit.SECONDS)).isTrue(); } - @EnableFlags(android.companion.virtualdevice.flags - .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS) @Test public void testCreateProjection_keyguardLocked_packageAllowlisted() throws NameNotFoundException { @@ -325,8 +312,6 @@ public class MediaProjectionManagerServiceTest { assertThat(mService.getActiveProjectionInfo()).isNotNull(); } - @EnableFlags(android.companion.virtualdevice.flags - .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS) @Test public void testCreateProjection_keyguardLocked_AppOpMediaProjection() throws NameNotFoundException { @@ -347,50 +332,6 @@ public class MediaProjectionManagerServiceTest { assertThat(mService.getActiveProjectionInfo()).isNotNull(); } - @EnableFlags(android.companion.virtualdevice.flags - .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS) - @Test - public void testCreateProjection_keyguardLocked_RoleHeld() { - runWithRole( - AssociationRequest.DEVICE_PROFILE_APP_STREAMING, - () -> { - try { - mAppInfo.privateFlags |= PRIVATE_FLAG_PRIVILEGED; - doReturn(mAppInfo) - .when(mPackageManager) - .getApplicationInfoAsUser( - anyString(), - any(ApplicationInfoFlags.class), - any(UserHandle.class)); - MediaProjectionManagerService.MediaProjection projection = - mService.createProjectionInternal( - Process.myUid(), - mContext.getPackageName(), - TYPE_MIRRORING, - /* isPermanentGrant= */ false, - UserHandle.CURRENT, - DEFAULT_DISPLAY); - doReturn(true).when(mKeyguardManager).isKeyguardLocked(); - doReturn(PackageManager.PERMISSION_DENIED) - .when(mPackageManager) - .checkPermission(RECORD_SENSITIVE_CONTENT, projection.packageName); - - projection.start(mIMediaProjectionCallback); - projection.notifyVirtualDisplayCreated(10); - - // The projection was started because it was allowed to capture the - // keyguard. - assertWithMessage("Failed to run projection") - .that(mService.getActiveProjectionInfo()) - .isNotNull(); - } catch (NameNotFoundException e) { - throw new RuntimeException(e); - } - }); - } - - @EnableFlags(android.companion.virtualdevice.flags - .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS) @Test public void testCreateProjection_keyguardLocked_screenshareProtectionsDisabled() throws NameNotFoundException { @@ -416,8 +357,6 @@ public class MediaProjectionManagerServiceTest { } } - @EnableFlags(android.companion.virtualdevice.flags - .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS) @Test public void testCreateProjection_keyguardLocked_noDisplayCreated() throws NameNotFoundException { @@ -509,8 +448,6 @@ public class MediaProjectionManagerServiceTest { assertThat(secondProjection).isNotEqualTo(projection); } - @EnableFlags(android.companion.virtualdevice.flags - .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS) @Test public void testReuseProjection_keyguardNotLocked_startConsentDialog() throws NameNotFoundException { @@ -527,8 +464,6 @@ public class MediaProjectionManagerServiceTest { verify(mContext).startActivityAsUser(any(), any()); } - @EnableFlags(android.companion.virtualdevice.flags - .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS) @Test public void testReuseProjection_keyguardLocked_noConsentDialog() throws NameNotFoundException { MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions(); @@ -1302,48 +1237,6 @@ public class MediaProjectionManagerServiceTest { return mService.getProjectionInternal(UID, PACKAGE_NAME); } - /** - * Run the provided block giving the current context's package the provided role. - */ - @SuppressWarnings("SameParameterValue") - private void runWithRole(String role, Runnable block) { - Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); - String packageName = mContext.getPackageName(); - UserHandle user = instrumentation.getTargetContext().getUser(); - RoleManager roleManager = Objects.requireNonNull( - mContext.getSystemService(RoleManager.class)); - try { - CountDownLatch latch = new CountDownLatch(1); - instrumentation.getUiAutomation().adoptShellPermissionIdentity( - Manifest.permission.MANAGE_ROLE_HOLDERS, - Manifest.permission.BYPASS_ROLE_QUALIFICATION); - - roleManager.setBypassingRoleQualification(true); - roleManager.addRoleHolderAsUser(role, packageName, - /* flags= */ RoleManager.MANAGE_HOLDERS_FLAG_DONT_KILL_APP, user, - mContext.getMainExecutor(), success -> { - if (success) { - latch.countDown(); - } else { - Assert.fail("Couldn't set role for test (failure) " + role); - } - }); - assertWithMessage("Couldn't set role for test (timeout) : " + role) - .that(latch.await(1, TimeUnit.SECONDS)).isTrue(); - block.run(); - - } catch (InterruptedException e) { - throw new RuntimeException(e); - } finally { - roleManager.removeRoleHolderAsUser(role, packageName, - /* flags= */ RoleManager.MANAGE_HOLDERS_FLAG_DONT_KILL_APP, user, - mContext.getMainExecutor(), (aBool) -> {}); - roleManager.setBypassingRoleQualification(false); - instrumentation.getUiAutomation() - .dropShellPermissionIdentity(); - } - } - private static class FakeIMediaProjectionCallback extends IMediaProjectionCallback.Stub { CountDownLatch mLatch = new CountDownLatch(1); @Override diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionStopControllerTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionStopControllerTest.java index affcfc14034e..10ac0495d69a 100644 --- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionStopControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionStopControllerTest.java @@ -22,7 +22,6 @@ import static android.provider.Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_ import static android.view.Display.INVALID_DISPLAY; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -37,13 +36,10 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.Manifest; import android.annotation.SuppressLint; import android.app.ActivityManagerInternal; import android.app.AppOpsManager; -import android.app.Instrumentation; import android.app.KeyguardManager; -import android.app.role.RoleManager; import android.companion.AssociationRequest; import android.content.Context; import android.content.pm.ApplicationInfo; @@ -69,7 +65,6 @@ import com.android.server.SystemConfig; import com.android.server.wm.WindowManagerInternal; import org.junit.After; -import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -79,9 +74,7 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; -import java.util.Objects; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; +import java.util.List; import java.util.function.Consumer; /** @@ -123,6 +116,8 @@ public class MediaProjectionStopControllerTest { private KeyguardManager mKeyguardManager; @Mock private TelecomManager mTelecomManager; + @Mock + private MediaProjectionStopController.RoleHolderProvider mRoleManager; private AppOpsManager mAppOpsManager; @Mock @@ -145,7 +140,7 @@ public class MediaProjectionStopControllerTest { mContext.addMockSystemService(TelecomManager.class, mTelecomManager); mContext.setMockPackageManager(mPackageManager); - mStopController = new MediaProjectionStopController(mContext, mStopConsumer); + mStopController = new MediaProjectionStopController(mContext, mStopConsumer, mRoleManager); mService = new MediaProjectionManagerService(mContext, mMediaProjectionMetricsLoggerInjector); @@ -170,8 +165,6 @@ public class MediaProjectionStopControllerTest { } @Test - @EnableFlags( - android.companion.virtualdevice.flags.Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS) public void testMediaProjectionNotRestricted() throws Exception { when(mKeyguardManager.isKeyguardLocked()).thenReturn(false); @@ -180,8 +173,6 @@ public class MediaProjectionStopControllerTest { } @Test - @EnableFlags( - android.companion.virtualdevice.flags.Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS) public void testMediaProjectionRestricted() throws Exception { MediaProjectionManagerService.MediaProjection mediaProjection = createMediaProjection(); mediaProjection.notifyVirtualDisplayCreated(1); @@ -239,21 +230,13 @@ public class MediaProjectionStopControllerTest { @Test public void testExemptFromStoppingHasAppStreamingRole() throws Exception { - runWithRole( - AssociationRequest.DEVICE_PROFILE_APP_STREAMING, - () -> { - try { - MediaProjectionManagerService.MediaProjection mediaProjection = - createMediaProjection(); - doReturn(PackageManager.PERMISSION_DENIED).when( - mPackageManager).checkPermission( - RECORD_SENSITIVE_CONTENT, mediaProjection.packageName); - assertThat(mStopController.isExemptFromStopping(mediaProjection, - MediaProjectionStopController.STOP_REASON_UNKNOWN)).isTrue(); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); + MediaProjectionManagerService.MediaProjection mediaProjection = createMediaProjection(); + doReturn(PackageManager.PERMISSION_DENIED).when(mPackageManager).checkPermission( + RECORD_SENSITIVE_CONTENT, mediaProjection.packageName); + doReturn(List.of(mediaProjection.packageName)).when(mRoleManager).getRoleHoldersAsUser( + eq(AssociationRequest.DEVICE_PROFILE_APP_STREAMING), any(UserHandle.class)); + assertThat(mStopController.isExemptFromStopping(mediaProjection, + MediaProjectionStopController.STOP_REASON_UNKNOWN)).isTrue(); } @Test @@ -300,8 +283,22 @@ public class MediaProjectionStopControllerTest { } @Test - @EnableFlags( - android.companion.virtualdevice.flags.Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS) + public void isStopReasonCallEnd_stopReasonCallEnd_returnsTrue() { + boolean result = + mStopController.isStopReasonCallEnd( + MediaProjectionStopController.STOP_REASON_CALL_END); + assertThat(result).isTrue(); + } + + @Test + public void isStopReasonCallEnd_stopReasonKeyguard_returnsFalse() { + boolean result = + mStopController.isStopReasonCallEnd( + MediaProjectionStopController.STOP_REASON_KEYGUARD); + assertThat(result).isFalse(); + } + + @Test public void testKeyguardLockedStateChanged_unlocked() { mStopController.onKeyguardLockedStateChanged(false); @@ -309,8 +306,6 @@ public class MediaProjectionStopControllerTest { } @Test - @EnableFlags( - android.companion.virtualdevice.flags.Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS) public void testKeyguardLockedStateChanged_locked() { mStopController.onKeyguardLockedStateChanged(true); @@ -422,47 +417,4 @@ public class MediaProjectionStopControllerTest { MediaProjectionManager.TYPE_SCREEN_CAPTURE, false, mContext.getUser(), INVALID_DISPLAY); } - - /** - * Run the provided block giving the current context's package the provided role. - */ - @SuppressWarnings("SameParameterValue") - private void runWithRole(String role, Runnable block) { - Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); - String packageName = mContext.getPackageName(); - UserHandle user = instrumentation.getTargetContext().getUser(); - RoleManager roleManager = Objects.requireNonNull( - mContext.getSystemService(RoleManager.class)); - try { - CountDownLatch latch = new CountDownLatch(1); - instrumentation.getUiAutomation().adoptShellPermissionIdentity( - Manifest.permission.MANAGE_ROLE_HOLDERS, - Manifest.permission.BYPASS_ROLE_QUALIFICATION); - - roleManager.setBypassingRoleQualification(true); - roleManager.addRoleHolderAsUser(role, packageName, - /* flags= */ RoleManager.MANAGE_HOLDERS_FLAG_DONT_KILL_APP, user, - mContext.getMainExecutor(), success -> { - if (success) { - latch.countDown(); - } else { - Assert.fail("Couldn't set role for test (failure) " + role); - } - }); - assertWithMessage("Couldn't set role for test (timeout) : " + role) - .that(latch.await(1, TimeUnit.SECONDS)).isTrue(); - block.run(); - - } catch (InterruptedException e) { - throw new RuntimeException(e); - } finally { - roleManager.removeRoleHolderAsUser(role, packageName, - /* flags= */ RoleManager.MANAGE_HOLDERS_FLAG_DONT_KILL_APP, user, - mContext.getMainExecutor(), (aBool) -> { - }); - roleManager.setBypassingRoleQualification(false); - instrumentation.getUiAutomation() - .dropShellPermissionIdentity(); - } - } } diff --git a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java index 2f3bca031f46..fbb673a194b4 100644 --- a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java @@ -1762,7 +1762,8 @@ public final class DataManagerTest { /* rankingAdjustment= */ 0, /* isBubble= */ false, /* proposedImportance= */ 0, - /* sensitiveContent= */ false + /* sensitiveContent= */ false, + /* summarization = */ null ); return true; }).when(mRankingMap).getRanking(eq(key), @@ -1806,7 +1807,8 @@ public final class DataManagerTest { /* rankingAdjustment= */ 0, /* isBubble= */ false, /* proposedImportance= */ 0, - /* sensitiveContent= */ false + /* sensitiveContent= */ false, + /* summarization = */ null ); return true; }).when(mRankingMap).getRanking(eq(CUSTOM_KEY), diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java index 5c2132f0e0e9..1c0ee09ccc6f 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java @@ -336,7 +336,7 @@ public class ShortcutManagerTest8 extends BaseShortcutManagerTest { checkRequestPinShortcut(makeResultIntent()); } - public void testRequestPinShortcut_explicitTargetActivity() { + public void disabled_testRequestPinShortcut_explicitTargetActivity() { setDefaultLauncher(USER_10, LAUNCHER_1); setDefaultLauncher(USER_11, LAUNCHER_2); @@ -475,7 +475,7 @@ public class ShortcutManagerTest8 extends BaseShortcutManagerTest { } - public void testRequestPinShortcut_dynamicExists() { + public void disabled_testRequestPinShortcut_dynamicExists() { setDefaultLauncher(USER_10, LAUNCHER_1); final Icon res32x32 = Icon.createWithResource(getTestContext(), R.drawable.black_32x32); @@ -590,7 +590,7 @@ public class ShortcutManagerTest8 extends BaseShortcutManagerTest { }); } - public void testRequestPinShortcut_dynamicExists_alreadyPinned() { + public void disabled_testRequestPinShortcut_dynamicExists_alreadyPinned() { setDefaultLauncher(USER_10, LAUNCHER_1); final Icon res32x32 = Icon.createWithResource(getTestContext(), R.drawable.black_32x32); @@ -848,7 +848,7 @@ public class ShortcutManagerTest8 extends BaseShortcutManagerTest { }); } - public void testRequestPinShortcut_dynamicExists_alreadyPinnedByAnother() { + public void disabled_testRequestPinShortcut_dynamicExists_alreadyPinnedByAnother() { // Initially all launchers have the shortcut permission, until we call setDefaultLauncher(). runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> { @@ -1041,7 +1041,7 @@ public class ShortcutManagerTest8 extends BaseShortcutManagerTest { /** * When trying to pin an existing shortcut, the new fields shouldn't override existing fields. */ - public void testRequestPinShortcut_dynamicExists_titleWontChange() { + public void disabled_testRequestPinShortcut_dynamicExists_titleWontChange() { setDefaultLauncher(USER_10, LAUNCHER_1); final Icon res32x32 = Icon.createWithResource(getTestContext(), R.drawable.black_32x32); @@ -1173,7 +1173,7 @@ public class ShortcutManagerTest8 extends BaseShortcutManagerTest { * The dynamic shortcut existed, but before accepting(), it's removed. Because the request * has a partial shortcut, accept() should fail. */ - public void testRequestPinShortcut_dynamicExists_thenRemoved_error() { + public void disabled_testRequestPinShortcut_dynamicExists_thenRemoved_error() { setDefaultLauncher(USER_10, LAUNCHER_1); runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> { @@ -1231,7 +1231,7 @@ public class ShortcutManagerTest8 extends BaseShortcutManagerTest { * The dynamic shortcut existed, but before accepting(), it's removed. Because the request * has all the mandatory fields, we can go ahead and still publish it. */ - public void testRequestPinShortcut_dynamicExists_thenRemoved_okay() { + public void disabled_testRequestPinShortcut_dynamicExists_thenRemoved_okay() { setDefaultLauncher(USER_10, LAUNCHER_1); runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> { @@ -1404,7 +1404,7 @@ public class ShortcutManagerTest8 extends BaseShortcutManagerTest { * The dynamic shortcut existed, but before accepting(), it's removed. Because the request * has a partial shortcut, accept() should fail. */ - public void testRequestPinShortcut_dynamicExists_thenDisabled_error() { + public void disabled_testRequestPinShortcut_dynamicExists_thenDisabled_error() { setDefaultLauncher(USER_10, LAUNCHER_1); runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> { diff --git a/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt index 5862ac65eba9..af50effb7c8e 100644 --- a/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt +++ b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt @@ -92,6 +92,21 @@ class SupervisionServiceTest { simulateUserStarting(USER_ID) assertThat(service.isSupervisionEnabledForUser(USER_ID)).isTrue() + assertThat(service.getActiveSupervisionAppPackage(USER_ID)) + .isEqualTo(systemSupervisionPackage) + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SYNC_WITH_DPM) + fun onUserStarting_legacyProfileOwnerComponent_enablesSupervision() { + whenever(mockDpmInternal.getProfileOwnerAsUser(USER_ID)) + .thenReturn(supervisionProfileOwnerComponent) + + simulateUserStarting(USER_ID) + + assertThat(service.isSupervisionEnabledForUser(USER_ID)).isTrue() + assertThat(service.getActiveSupervisionAppPackage(USER_ID)) + .isEqualTo(supervisionProfileOwnerComponent.packageName) } @Test @@ -103,6 +118,7 @@ class SupervisionServiceTest { simulateUserStarting(USER_ID, preCreated = true) assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse() + assertThat(service.getActiveSupervisionAppPackage(USER_ID)).isNull() } @Test @@ -114,6 +130,7 @@ class SupervisionServiceTest { simulateUserStarting(USER_ID) assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse() + assertThat(service.getActiveSupervisionAppPackage(USER_ID)).isNull() } @Test @@ -125,6 +142,21 @@ class SupervisionServiceTest { broadcastProfileOwnerChanged(USER_ID) assertThat(service.isSupervisionEnabledForUser(USER_ID)).isTrue() + assertThat(service.getActiveSupervisionAppPackage(USER_ID)) + .isEqualTo(systemSupervisionPackage) + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SYNC_WITH_DPM) + fun profileOwnerChanged_legacyProfileOwnerComponent_enablesSupervision() { + whenever(mockDpmInternal.getProfileOwnerAsUser(USER_ID)) + .thenReturn(supervisionProfileOwnerComponent) + + broadcastProfileOwnerChanged(USER_ID) + + assertThat(service.isSupervisionEnabledForUser(USER_ID)).isTrue() + assertThat(service.getActiveSupervisionAppPackage(USER_ID)) + .isEqualTo(supervisionProfileOwnerComponent.packageName) } @Test @@ -136,13 +168,14 @@ class SupervisionServiceTest { broadcastProfileOwnerChanged(USER_ID) assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse() + assertThat(service.getActiveSupervisionAppPackage(USER_ID)).isNull() } @Test fun isActiveSupervisionApp_supervisionUid_supervisionEnabled_returnsTrue() { whenever(mockPackageManager.getPackagesForUid(APP_UID)) .thenReturn(arrayOf(systemSupervisionPackage)) - service.setSupervisionEnabledForUser(USER_ID, true) + service.mInternal.setSupervisionEnabledForUser(USER_ID, true) assertThat(service.mInternal.isActiveSupervisionApp(APP_UID)).isTrue() } @@ -151,7 +184,7 @@ class SupervisionServiceTest { fun isActiveSupervisionApp_supervisionUid_supervisionNotEnabled_returnsFalse() { whenever(mockPackageManager.getPackagesForUid(APP_UID)) .thenReturn(arrayOf(systemSupervisionPackage)) - service.setSupervisionEnabledForUser(USER_ID, false) + service.mInternal.setSupervisionEnabledForUser(USER_ID, false) assertThat(service.mInternal.isActiveSupervisionApp(APP_UID)).isFalse() } @@ -167,15 +200,15 @@ class SupervisionServiceTest { fun setSupervisionEnabledForUser() { assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse() - service.setSupervisionEnabledForUser(USER_ID, true) + service.mInternal.setSupervisionEnabledForUser(USER_ID, true) assertThat(service.isSupervisionEnabledForUser(USER_ID)).isTrue() - service.setSupervisionEnabledForUser(USER_ID, false) + service.mInternal.setSupervisionEnabledForUser(USER_ID, false) assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse() } @Test - fun supervisionEnabledForUser_internal() { + fun setSupervisionEnabledForUser_internal() { assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse() service.mInternal.setSupervisionEnabledForUser(USER_ID, true) @@ -205,6 +238,13 @@ class SupervisionServiceTest { private val systemSupervisionPackage: String get() = context.getResources().getString(R.string.config_systemSupervision) + private val supervisionProfileOwnerComponent: ComponentName + get() = + context + .getResources() + .getString(R.string.config_defaultSupervisionProfileOwnerComponent) + .let(ComponentName::unflattenFromString)!! + private fun simulateUserStarting(userId: Int, preCreated: Boolean = false) { val userInfo = UserInfo(userId, /* name= */ "tempUser", /* flags= */ 0) userInfo.preCreated = preCreated diff --git a/services/tests/uiservicestests/Android.bp b/services/tests/uiservicestests/Android.bp index a63a38da3740..0eb20eb22380 100644 --- a/services/tests/uiservicestests/Android.bp +++ b/services/tests/uiservicestests/Android.bp @@ -20,6 +20,7 @@ android_test { ], static_libs: [ + "compatibility-device-util-axt-minus-dexmaker", "frameworks-base-testutils", "services.accessibility", "services.core", diff --git a/services/tests/uiservicestests/AndroidManifest.xml b/services/tests/uiservicestests/AndroidManifest.xml index 4315254f68a9..69f17757b367 100644 --- a/services/tests/uiservicestests/AndroidManifest.xml +++ b/services/tests/uiservicestests/AndroidManifest.xml @@ -45,6 +45,8 @@ <provider android:name=".DummyProvider" android:authorities="com.android.services.uitests" /> + <activity android:name="android.app.ExampleActivity" /> + </application> <instrumentation diff --git a/services/tests/uiservicestests/src/android/app/ExampleActivity.java b/services/tests/uiservicestests/src/android/app/ExampleActivity.java new file mode 100644 index 000000000000..58395e4d75e1 --- /dev/null +++ b/services/tests/uiservicestests/src/android/app/ExampleActivity.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +public class ExampleActivity extends Activity { +} diff --git a/services/tests/uiservicestests/src/android/app/NotificationManagerZenTest.java b/services/tests/uiservicestests/src/android/app/NotificationManagerZenTest.java new file mode 100644 index 000000000000..779fa1aa2f72 --- /dev/null +++ b/services/tests/uiservicestests/src/android/app/NotificationManagerZenTest.java @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL; +import static android.app.NotificationSystemUtil.runAsSystemUi; +import static android.app.NotificationSystemUtil.toggleNotificationPolicyAccess; +import static android.service.notification.Condition.STATE_FALSE; +import static android.service.notification.Condition.STATE_TRUE; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.ComponentName; +import android.content.Context; +import android.net.Uri; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.service.notification.Condition; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Map; + +@RunWith(AndroidJUnit4.class) +public class NotificationManagerZenTest { + + private Context mContext; + private NotificationManager mNotificationManager; + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + @Before + public void setUp() throws Exception { + mContext = ApplicationProvider.getApplicationContext(); + mNotificationManager = mContext.getSystemService(NotificationManager.class); + + toggleNotificationPolicyAccess(mContext, mContext.getPackageName(), true); + runAsSystemUi(() -> mNotificationManager.setInterruptionFilter(INTERRUPTION_FILTER_ALL)); + removeAutomaticZenRules(); + } + + @After + public void tearDown() { + runAsSystemUi(() -> mNotificationManager.setInterruptionFilter(INTERRUPTION_FILTER_ALL)); + removeAutomaticZenRules(); + } + + private void removeAutomaticZenRules() { + // Delete AZRs created by this test (query "as app", then delete "as system" so they are + // not preserved to be restored later). + Map<String, AutomaticZenRule> rules = mNotificationManager.getAutomaticZenRules(); + runAsSystemUi(() -> { + for (String ruleId : rules.keySet()) { + mNotificationManager.removeAutomaticZenRule(ruleId); + } + }); + } + + @Test + @RequiresFlagsEnabled({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI}) + public void setAutomaticZenRuleState_manualActivation() { + AutomaticZenRule ruleToCreate = createZenRule("rule"); + String ruleId = mNotificationManager.addAutomaticZenRule(ruleToCreate); + Condition manualActivate = new Condition(ruleToCreate.getConditionId(), "manual-on", + STATE_TRUE, Condition.SOURCE_USER_ACTION); + Condition manualDeactivate = new Condition(ruleToCreate.getConditionId(), "manual-off", + STATE_FALSE, Condition.SOURCE_USER_ACTION); + Condition autoActivate = new Condition(ruleToCreate.getConditionId(), "auto-on", + STATE_TRUE); + Condition autoDeactivate = new Condition(ruleToCreate.getConditionId(), "auto-off", + STATE_FALSE); + + // User manually activates -> it's active. + runAsSystemUi( + () -> mNotificationManager.setAutomaticZenRuleState(ruleId, manualActivate)); + assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_TRUE); + + // User manually deactivates -> it's inactive. + runAsSystemUi( + () -> mNotificationManager.setAutomaticZenRuleState(ruleId, manualDeactivate)); + assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_FALSE); + + // And app can activate and deactivate. + mNotificationManager.setAutomaticZenRuleState(ruleId, autoActivate); + assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_TRUE); + mNotificationManager.setAutomaticZenRuleState(ruleId, autoDeactivate); + assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_FALSE); + } + + @Test + @RequiresFlagsEnabled({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI}) + public void setAutomaticZenRuleState_manualDeactivation() { + AutomaticZenRule ruleToCreate = createZenRule("rule"); + String ruleId = mNotificationManager.addAutomaticZenRule(ruleToCreate); + Condition manualActivate = new Condition(ruleToCreate.getConditionId(), "manual-on", + STATE_TRUE, Condition.SOURCE_USER_ACTION); + Condition manualDeactivate = new Condition(ruleToCreate.getConditionId(), "manual-off", + STATE_FALSE, Condition.SOURCE_USER_ACTION); + Condition autoActivate = new Condition(ruleToCreate.getConditionId(), "auto-on", + STATE_TRUE); + Condition autoDeactivate = new Condition(ruleToCreate.getConditionId(), "auto-off", + STATE_FALSE); + + // App activates rule. + mNotificationManager.setAutomaticZenRuleState(ruleId, autoActivate); + assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_TRUE); + + // User manually deactivates -> it's inactive. + runAsSystemUi( + () -> mNotificationManager.setAutomaticZenRuleState(ruleId, manualDeactivate)); + assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_FALSE); + + // User manually reactivates -> it's active. + runAsSystemUi( + () -> mNotificationManager.setAutomaticZenRuleState(ruleId, manualActivate)); + assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_TRUE); + + // That manual activation removed the override-deactivate, but didn't put an + // override-activate, so app can deactivate when its natural schedule ends. + mNotificationManager.setAutomaticZenRuleState(ruleId, autoDeactivate); + assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_FALSE); + } + + @Test + @RequiresFlagsEnabled({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI}) + public void setAutomaticZenRuleState_respectsManuallyActivated() { + AutomaticZenRule ruleToCreate = createZenRule("rule"); + String ruleId = mNotificationManager.addAutomaticZenRule(ruleToCreate); + Condition manualActivate = new Condition(ruleToCreate.getConditionId(), "manual-on", + STATE_TRUE, Condition.SOURCE_USER_ACTION); + Condition autoActivate = new Condition(ruleToCreate.getConditionId(), "auto-on", + STATE_TRUE); + Condition autoDeactivate = new Condition(ruleToCreate.getConditionId(), "auto-off", + STATE_FALSE); + + // App thinks rule should be inactive. + mNotificationManager.setAutomaticZenRuleState(ruleId, autoDeactivate); + assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_FALSE); + + // Manually activate -> it's active. + runAsSystemUi(() -> mNotificationManager.setAutomaticZenRuleState(ruleId, manualActivate)); + assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_TRUE); + + // App says it should be inactive, but it's ignored. + mNotificationManager.setAutomaticZenRuleState(ruleId, autoDeactivate); + assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_TRUE); + + // App says it should be active. No change now... + mNotificationManager.setAutomaticZenRuleState(ruleId, autoActivate); + assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_TRUE); + + // ... but when the app wants to deactivate next time, it works. + mNotificationManager.setAutomaticZenRuleState(ruleId, autoDeactivate); + assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_FALSE); + } + + @Test + @RequiresFlagsEnabled({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI}) + public void setAutomaticZenRuleState_respectsManuallyDeactivated() { + AutomaticZenRule ruleToCreate = createZenRule("rule"); + String ruleId = mNotificationManager.addAutomaticZenRule(ruleToCreate); + Condition manualDeactivate = new Condition(ruleToCreate.getConditionId(), "manual-off", + STATE_FALSE, Condition.SOURCE_USER_ACTION); + Condition autoActivate = new Condition(ruleToCreate.getConditionId(), "auto-on", + STATE_TRUE); + Condition autoDeactivate = new Condition(ruleToCreate.getConditionId(), "auto-off", + STATE_FALSE); + + // App activates rule. + mNotificationManager.setAutomaticZenRuleState(ruleId, autoActivate); + assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_TRUE); + + // User manually deactivates -> it's inactive. + runAsSystemUi( + () -> mNotificationManager.setAutomaticZenRuleState(ruleId, manualDeactivate)); + assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_FALSE); + + // App says it should be active, but it's ignored. + mNotificationManager.setAutomaticZenRuleState(ruleId, autoActivate); + assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_FALSE); + + // App says it should be inactive. No change now... + mNotificationManager.setAutomaticZenRuleState(ruleId, autoDeactivate); + assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_FALSE); + + // ... but when the app wants to activate next time, it works. + mNotificationManager.setAutomaticZenRuleState(ruleId, autoActivate); + assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_TRUE); + } + + @Test + @RequiresFlagsEnabled({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI}) + public void setAutomaticZenRuleState_manualActivationFromApp() { + AutomaticZenRule ruleToCreate = createZenRule("rule"); + String ruleId = mNotificationManager.addAutomaticZenRule(ruleToCreate); + Condition manualActivate = new Condition(ruleToCreate.getConditionId(), "manual-off", + STATE_TRUE, Condition.SOURCE_USER_ACTION); + Condition manualDeactivate = new Condition(ruleToCreate.getConditionId(), "manual-off", + STATE_FALSE, Condition.SOURCE_USER_ACTION); + Condition autoActivate = new Condition(ruleToCreate.getConditionId(), "auto-on", + STATE_TRUE); + Condition autoDeactivate = new Condition(ruleToCreate.getConditionId(), "auto-off", + STATE_FALSE); + + // App activates rule. + mNotificationManager.setAutomaticZenRuleState(ruleId, autoActivate); + assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_TRUE); + + // User manually deactivates from SysUI -> it's inactive. + runAsSystemUi( + () -> mNotificationManager.setAutomaticZenRuleState(ruleId, manualDeactivate)); + assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_FALSE); + + // User manually activates from App -> it's active. + mNotificationManager.setAutomaticZenRuleState(ruleId, manualActivate); + assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_TRUE); + + // And app can automatically deactivate it later. + mNotificationManager.setAutomaticZenRuleState(ruleId, autoDeactivate); + assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_FALSE); + } + + @Test + @RequiresFlagsEnabled({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI}) + public void setAutomaticZenRuleState_manualDeactivationFromApp() { + AutomaticZenRule ruleToCreate = createZenRule("rule"); + String ruleId = mNotificationManager.addAutomaticZenRule(ruleToCreate); + Condition manualActivate = new Condition(ruleToCreate.getConditionId(), "manual-off", + STATE_TRUE, Condition.SOURCE_USER_ACTION); + Condition manualDeactivate = new Condition(ruleToCreate.getConditionId(), "manual-off", + STATE_FALSE, Condition.SOURCE_USER_ACTION); + Condition autoActivate = new Condition(ruleToCreate.getConditionId(), "auto-on", + STATE_TRUE); + + // User manually activates from SysUI -> it's active. + runAsSystemUi( + () -> mNotificationManager.setAutomaticZenRuleState(ruleId, manualActivate)); + assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_TRUE); + + // User manually deactivates from App -> it's inactive. + mNotificationManager.setAutomaticZenRuleState(ruleId, manualDeactivate); + assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_FALSE); + + // And app can automatically activate it later. + mNotificationManager.setAutomaticZenRuleState(ruleId, autoActivate); + assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_TRUE); + } + + private AutomaticZenRule createZenRule(String name) { + return createZenRule(name, NotificationManager.INTERRUPTION_FILTER_PRIORITY); + } + + private AutomaticZenRule createZenRule(String name, int filter) { + return new AutomaticZenRule(name, null, + new ComponentName(mContext, ExampleActivity.class), + new Uri.Builder().scheme("scheme") + .appendPath("path") + .appendQueryParameter("fake_rule", "fake_value") + .build(), null, filter, true); + } +} diff --git a/services/tests/uiservicestests/src/android/app/NotificationSystemUtil.java b/services/tests/uiservicestests/src/android/app/NotificationSystemUtil.java new file mode 100644 index 000000000000..cf6e39b962ab --- /dev/null +++ b/services/tests/uiservicestests/src/android/app/NotificationSystemUtil.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import static org.junit.Assert.assertEquals; + +import android.Manifest; +import android.content.Context; +import android.os.ParcelFileDescriptor; + +import androidx.annotation.NonNull; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.compatibility.common.util.AmUtils; +import com.android.compatibility.common.util.FileUtils; +import com.android.compatibility.common.util.SystemUtil; +import com.android.compatibility.common.util.ThrowingRunnable; + +import java.io.FileInputStream; +import java.io.IOException; + +public class NotificationSystemUtil { + + /** + * Runs a {@link ThrowingRunnable} as the Shell, while adopting SystemUI's permission (as + * checked by {@code NotificationManagerService#isCallerSystemOrSystemUi}). + */ + protected static void runAsSystemUi(@NonNull ThrowingRunnable runnable) { + SystemUtil.runWithShellPermissionIdentity( + InstrumentationRegistry.getInstrumentation().getUiAutomation(), + runnable, Manifest.permission.STATUS_BAR_SERVICE); + } + + static void toggleNotificationPolicyAccess(Context context, String packageName, + boolean on) throws IOException { + + String command = " cmd notification " + (on ? "allow_dnd " : "disallow_dnd ") + packageName + + " " + context.getUserId(); + + runCommand(command, InstrumentationRegistry.getInstrumentation()); + AmUtils.waitForBroadcastBarrier(); + + NotificationManager nm = context.getSystemService(NotificationManager.class); + assertEquals("Notification Policy Access Grant is " + + nm.isNotificationPolicyAccessGranted() + " not " + on + " for " + + packageName, on, nm.isNotificationPolicyAccessGranted()); + } + + private static void runCommand(String command, Instrumentation instrumentation) + throws IOException { + UiAutomation uiAutomation = instrumentation.getUiAutomation(); + try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream( + uiAutomation.executeShellCommand(command))) { + FileUtils.readInputStreamFully(fis); + } + } +} diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java index 8fad01a7541d..2616ccbe474a 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java @@ -247,7 +247,8 @@ public class NotificationListenerServiceTest extends UiServiceTestCase { getRankingAdjustment(i), isBubble(i), getProposedImportance(i), - hasSensitiveContent(i) + hasSensitiveContent(i), + getSummarization(i) ); rankings[i] = ranking; } @@ -383,6 +384,13 @@ public class NotificationListenerServiceTest extends UiServiceTestCase { return index % 3 == 0; } + public static String getSummarization(int index) { + if ((android.app.Flags.nmSummarizationUi() || android.app.Flags.nmSummarization())) { + return "summary " + index; + } + return null; + } + private boolean isBubble(int index) { return index % 4 == 0; } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index edfdb4ffec3a..a8fcadd9dd74 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -51,6 +51,7 @@ import static android.app.NotificationChannel.RECS_ID; import static android.app.NotificationChannel.SOCIAL_MEDIA_ID; import static android.app.NotificationChannel.USER_LOCKED_ALLOW_BUBBLE; import static android.app.NotificationManager.ACTION_AUTOMATIC_ZEN_RULE_STATUS_CHANGED; +import static android.app.NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED; import static android.app.NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED; import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_ACTIVATED; import static android.app.NotificationManager.BUBBLE_PREFERENCE_ALL; @@ -123,7 +124,9 @@ import static android.service.notification.Flags.FLAG_REDACT_SENSITIVE_NOTIFICAT import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ONGOING; +import static android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_CALL_EFFECTS; import static android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_EFFECTS; +import static android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_NOTIFICATION_EFFECTS; import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL; import static android.service.notification.NotificationListenerService.REASON_CANCEL; import static android.service.notification.NotificationListenerService.REASON_LOCKDOWN; @@ -163,6 +166,7 @@ import static junit.framework.Assert.fail; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Matchers.anyBoolean; import static org.mockito.Matchers.anyLong; @@ -360,7 +364,6 @@ import org.junit.rules.TestRule; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatcher; -import org.mockito.ArgumentMatchers; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.Mockito; @@ -368,6 +371,9 @@ import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; @@ -383,9 +389,6 @@ import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.function.Consumer; -import platform.test.runner.parameterized.ParameterizedAndroidJunit4; -import platform.test.runner.parameterized.Parameters; - @SmallTest @RunWith(ParameterizedAndroidJunit4.class) @RunWithLooper @@ -600,7 +603,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Parameters(name = "{0}") public static List<FlagsParameterization> getParams() { - return FlagsParameterization.allCombinationsOf(); + return FlagsParameterization.allCombinationsOf( + FLAG_NOTIFICATION_CLASSIFICATION); } public NotificationManagerServiceTest(FlagsParameterization flags) { @@ -10889,6 +10893,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { Bundle signals = new Bundle(); signals.putInt(KEY_IMPORTANCE, IMPORTANCE_LOW); signals.putInt(KEY_USER_SENTIMENT, USER_SENTIMENT_NEGATIVE); + signals.putInt(KEY_TYPE, TYPE_PROMOTION); Adjustment adjustment = new Adjustment(r.getSbn().getPackageName(), r.getKey(), signals, "", r.getUser().getIdentifier()); @@ -11413,8 +11418,14 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { verify(mContext).sendBroadcastAsUser(eqIntent(expected), eq(UserHandle.of(mUserId))); } + private static Intent isIntentWithAction(String wantedAction) { + return argThat( + intent -> intent != null && wantedAction.equals(intent.getAction()) + ); + } + private static Intent eqIntent(Intent wanted) { - return ArgumentMatchers.argThat( + return argThat( new ArgumentMatcher<Intent>() { @Override public boolean matches(Intent argument) { @@ -17491,6 +17502,66 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @EnableFlags({Flags.FLAG_NM_BINDER_PERF_THROTTLE_EFFECTS_SUPPRESSOR_BROADCAST, + Flags.FLAG_NM_BINDER_PERF_REDUCE_ZEN_BROADCASTS}) + public void requestHintsFromListener_changingEffectsButNotSuppressor_noBroadcast() + throws Exception { + // Note that NM_BINDER_PERF_REDUCE_ZEN_BROADCASTS is not strictly necessary; however each + // path will do slightly different calls so we force one of them to simplify the test. + when(mUmInternal.getProfileIds(anyInt(), anyBoolean())).thenReturn(new int[]{mUserId}); + when(mListeners.checkServiceTokenLocked(any())).thenReturn(mListener); + INotificationListener token = mock(INotificationListener.class); + mService.isSystemUid = true; + + mBinderService.requestHintsFromListener(token, HINT_HOST_DISABLE_CALL_EFFECTS); + mTestableLooper.moveTimeForward(500); // more than ZEN_BROADCAST_DELAY + waitForIdle(); + + verify(mContext, times(1)).sendBroadcastMultiplePermissions( + isIntentWithAction(ACTION_EFFECTS_SUPPRESSOR_CHANGED), any(), any(), any()); + + // Same suppressor suppresses something else. + mBinderService.requestHintsFromListener(token, HINT_HOST_DISABLE_NOTIFICATION_EFFECTS); + mTestableLooper.moveTimeForward(500); // more than ZEN_BROADCAST_DELAY + waitForIdle(); + + // Still 1 total calls (the previous one). + verify(mContext, times(1)).sendBroadcastMultiplePermissions( + isIntentWithAction(ACTION_EFFECTS_SUPPRESSOR_CHANGED), any(), any(), any()); + } + + @Test + @EnableFlags({Flags.FLAG_NM_BINDER_PERF_THROTTLE_EFFECTS_SUPPRESSOR_BROADCAST, + Flags.FLAG_NM_BINDER_PERF_REDUCE_ZEN_BROADCASTS}) + public void requestHintsFromListener_changingSuppressor_throttlesBroadcast() throws Exception { + // Note that NM_BINDER_PERF_REDUCE_ZEN_BROADCASTS is not strictly necessary; however each + // path will do slightly different calls so we force one of them to simplify the test. + when(mUmInternal.getProfileIds(anyInt(), anyBoolean())).thenReturn(new int[]{mUserId}); + when(mListeners.checkServiceTokenLocked(any())).thenReturn(mListener); + INotificationListener token = mock(INotificationListener.class); + mService.isSystemUid = true; + + // Several updates in quick succession. + mBinderService.requestHintsFromListener(token, HINT_HOST_DISABLE_CALL_EFFECTS); + mBinderService.clearRequestedListenerHints(token); + mBinderService.requestHintsFromListener(token, HINT_HOST_DISABLE_NOTIFICATION_EFFECTS); + mBinderService.clearRequestedListenerHints(token); + mBinderService.requestHintsFromListener(token, HINT_HOST_DISABLE_CALL_EFFECTS); + mBinderService.clearRequestedListenerHints(token); + mBinderService.requestHintsFromListener(token, HINT_HOST_DISABLE_NOTIFICATION_EFFECTS); + + // No broadcasts yet! + verify(mContext, never()).sendBroadcastMultiplePermissions(any(), any(), any(), any()); + + mTestableLooper.moveTimeForward(500); // more than ZEN_BROADCAST_DELAY + waitForIdle(); + + // Only one broadcast after idle time. + verify(mContext, times(1)).sendBroadcastMultiplePermissions( + isIntentWithAction(ACTION_EFFECTS_SUPPRESSOR_CHANGED), any(), any(), any()); + } + + @Test @EnableFlags(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION) public void testApplyAdjustment_keyType_validType() throws Exception { final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordExtractorDataTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordExtractorDataTest.java index 9fe0e49c4ab8..09ebc23a1d7b 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordExtractorDataTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordExtractorDataTest.java @@ -29,12 +29,19 @@ import android.os.UserHandle; import android.service.notification.Adjustment; import android.service.notification.StatusBarNotification; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; + import com.android.server.UiServiceTestCase; +import org.junit.Rule; import org.junit.Test; public class NotificationRecordExtractorDataTest extends UiServiceTestCase { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Test public void testHasDiffs_noDiffs() { NotificationRecord r = generateRecord(); @@ -57,7 +64,8 @@ public class NotificationRecordExtractorDataTest extends UiServiceTestCase { r.getRankingScore(), r.isConversation(), r.getProposedImportance(), - r.hasSensitiveContent()); + r.hasSensitiveContent(), + r.getSummarization()); assertFalse(extractorData.hasDiffForRankingLocked(r, 1)); assertFalse(extractorData.hasDiffForLoggingLocked(r, 1)); @@ -85,7 +93,8 @@ public class NotificationRecordExtractorDataTest extends UiServiceTestCase { r.getRankingScore(), r.isConversation(), r.getProposedImportance(), - r.hasSensitiveContent()); + r.hasSensitiveContent(), + r.getSummarization()); Bundle signals = new Bundle(); signals.putInt(Adjustment.KEY_IMPORTANCE_PROPOSAL, IMPORTANCE_HIGH); @@ -119,7 +128,8 @@ public class NotificationRecordExtractorDataTest extends UiServiceTestCase { r.getRankingScore(), r.isConversation(), r.getProposedImportance(), - r.hasSensitiveContent()); + r.hasSensitiveContent(), + r.getSummarization()); Bundle signals = new Bundle(); signals.putString(Adjustment.KEY_GROUP_KEY, "ranker_group"); @@ -154,7 +164,8 @@ public class NotificationRecordExtractorDataTest extends UiServiceTestCase { r.getRankingScore(), r.isConversation(), r.getProposedImportance(), - r.hasSensitiveContent()); + r.hasSensitiveContent(), + r.getSummarization()); Bundle signals = new Bundle(); signals.putBoolean(Adjustment.KEY_SENSITIVE_CONTENT, true); @@ -166,6 +177,42 @@ public class NotificationRecordExtractorDataTest extends UiServiceTestCase { assertTrue(extractorData.hasDiffForLoggingLocked(r, 1)); } + @Test + @EnableFlags(android.app.Flags.FLAG_NM_SUMMARIZATION) + public void testHasDiffs_summarization() { + NotificationRecord r = generateRecord(); + + NotificationRecordExtractorData extractorData = new NotificationRecordExtractorData( + 1, + r.getPackageVisibilityOverride(), + r.canShowBadge(), + r.canBubble(), + r.getNotification().isBubbleNotification(), + r.getChannel(), + r.getGroupKey(), + r.getPeopleOverride(), + r.getSnoozeCriteria(), + r.getUserSentiment(), + r.getSuppressedVisualEffects(), + r.getSystemGeneratedSmartActions(), + r.getSmartReplies(), + r.getImportance(), + r.getRankingScore(), + r.isConversation(), + r.getProposedImportance(), + r.hasSensitiveContent(), + r.getSummarization()); + + Bundle signals = new Bundle(); + signals.putString(Adjustment.KEY_SUMMARIZATION, "SUMMARIZED!"); + Adjustment adjustment = new Adjustment("pkg", r.getKey(), signals, "", 0); + r.addAdjustment(adjustment); + r.applyAdjustments(); + + assertTrue(extractorData.hasDiffForRankingLocked(r, 1)); + assertTrue(extractorData.hasDiffForLoggingLocked(r, 1)); + } + private NotificationRecord generateRecord() { NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_LOW); final Notification.Builder builder = new Notification.Builder(getContext()) diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index 704b580a80b0..832ca51ae580 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -260,7 +260,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { return FlagsParameterization.allCombinationsOf( android.app.Flags.FLAG_API_RICH_ONGOING, FLAG_NOTIFICATION_CLASSIFICATION, FLAG_NOTIFICATION_CLASSIFICATION_UI, - FLAG_MODES_UI); + FLAG_MODES_UI, android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS); } public PreferencesHelperTest(FlagsParameterization flags) { @@ -3381,13 +3381,12 @@ public class PreferencesHelperTest extends UiServiceTestCase { // user 0 records remain for (int i = 0; i < user0Uids.length; i++) { assertEquals(1, - mHelper.getNotificationChannels(PKG_N_MR1, user0Uids[i], false, true) - .getList().size()); + mHelper.getRemovedPkgNotificationChannels(PKG_N_MR1, user0Uids[i]).size()); } // user 1 records are gone for (int i = 0; i < user1Uids.length; i++) { - assertEquals(0, mHelper.getNotificationChannels(PKG_N_MR1, user1Uids[i], false, true) - .getList().size()); + assertEquals(0, + mHelper.getRemovedPkgNotificationChannels(PKG_N_MR1, user1Uids[i]).size()); } } @@ -3402,8 +3401,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { assertTrue(mHelper.onPackagesChanged(true, USER_SYSTEM, new String[]{PKG_N_MR1}, new int[]{UID_N_MR1})); - assertEquals(0, mHelper.getNotificationChannels( - PKG_N_MR1, UID_N_MR1, true, true).getList().size()); + assertEquals(0, mHelper.getRemovedPkgNotificationChannels(PKG_N_MR1, UID_N_MR1).size()); // Not deleted mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false, @@ -3472,7 +3470,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { assertTrue(mHelper.canShowBadge(PKG_O, UID_O)); assertNull(mHelper.getNotificationDelegate(PKG_O, UID_O)); assertEquals(0, mHelper.getAppLockedFields(PKG_O, UID_O)); - assertEquals(0, mHelper.getNotificationChannels(PKG_O, UID_O, true, true).getList().size()); + assertEquals(0, mHelper.getRemovedPkgNotificationChannels(PKG_O, UID_O).size()); assertEquals(0, mHelper.getNotificationChannelGroups(PKG_O, UID_O).size()); NotificationChannel channel = getChannel(); @@ -6836,38 +6834,11 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) - public void testGetNotificationChannels_createIfNeeded() { - // Test setup hasn't created any channels or read package preferences yet. - // If we ask for notification channels _without_ creating, we should get no result. - ParceledListSlice<NotificationChannel> channels = mHelper.getNotificationChannels(PKG_N_MR1, - UID_N_MR1, false, false, /* createPrefsIfNeeded= */ false); - assertThat(channels.getList().size()).isEqualTo(0); - - // If we ask it to create package preferences, we expect the default channel to be created - // for N_MR1. - channels = mHelper.getNotificationChannels(PKG_N_MR1, UID_N_MR1, false, - false, /* createPrefsIfNeeded= */ true); - assertThat(channels.getList().size()).isEqualTo(1); - assertThat(channels.getList().getFirst().getId()).isEqualTo( - NotificationChannel.DEFAULT_CHANNEL_ID); - } - - @Test @DisableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) public void testGetNotificationChannels_neverCreatesWhenFlagOff() { - ParceledListSlice<NotificationChannel> channels; - try { - channels = mHelper.getNotificationChannels(PKG_N_MR1, UID_N_MR1, false, - false, /* createPrefsIfNeeded= */ true); - } catch (Exception e) { - // Slog.wtf kicks in, presumably - } finally { - channels = mHelper.getNotificationChannels(PKG_N_MR1, UID_N_MR1, false, - false, /* createPrefsIfNeeded= */ false); - assertThat(channels.getList().size()).isEqualTo(0); - } - + ParceledListSlice<NotificationChannel> channels = mHelper.getNotificationChannels(PKG_N_MR1, + UID_N_MR1, false, false); + assertThat(channels.getList().size()).isEqualTo(0); } // Test version of PreferencesHelper whose only functional difference is that it does not diff --git a/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java b/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java index 038e1357159b..036e03c60091 100644 --- a/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java @@ -23,6 +23,7 @@ import static com.android.server.policy.PhoneWindowManager.POWER_VOLUME_UP_BEHAV import static com.android.server.policy.PhoneWindowManager.POWER_VOLUME_UP_BEHAVIOR_MUTE; import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.Presubmit; import android.view.ViewConfiguration; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -38,6 +39,7 @@ import org.junit.runner.RunWith; * Build/Install/Run: * atest WmTests:CombinationKeyTests */ +@Presubmit @MediumTest @RunWith(AndroidJUnit4.class) @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER_MULTI_KEY_GESTURES) diff --git a/services/tests/wmtests/src/com/android/server/policy/DeferredKeyActionExecutorTests.java b/services/tests/wmtests/src/com/android/server/policy/DeferredKeyActionExecutorTests.java index ca3787ec4546..bccdd67d33ed 100644 --- a/services/tests/wmtests/src/com/android/server/policy/DeferredKeyActionExecutorTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/DeferredKeyActionExecutorTests.java @@ -19,6 +19,7 @@ package com.android.server.policy; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import android.platform.test.annotations.Presubmit; import android.view.KeyEvent; import org.junit.Before; @@ -29,6 +30,7 @@ import org.junit.Test; * * <p>Build/Install/Run: atest WmTests:DeferredKeyActionExecutorTests */ +@Presubmit public final class DeferredKeyActionExecutorTests { private DeferredKeyActionExecutor mKeyActionExecutor; diff --git a/services/tests/wmtests/src/com/android/server/policy/KeyCombinationManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyCombinationManagerTests.java index 8b5f68a1e974..a912c177c863 100644 --- a/services/tests/wmtests/src/com/android/server/policy/KeyCombinationManagerTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/KeyCombinationManagerTests.java @@ -29,6 +29,7 @@ import static org.testng.Assert.assertTrue; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; +import android.platform.test.annotations.Presubmit; import android.view.KeyEvent; import androidx.test.filters.SmallTest; @@ -45,7 +46,7 @@ import java.util.concurrent.TimeUnit; * Build/Install/Run: * atest KeyCombinationManagerTests */ - +@Presubmit @SmallTest public class KeyCombinationManagerTests { private KeyCombinationManager mKeyCombinationManager; diff --git a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java index 85ef466b2477..16c786b52655 100644 --- a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java @@ -544,17 +544,6 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase { } @Test - public void testKeyGestureSplitscreenFocus() { - Assert.assertTrue(sendKeyGestureEventComplete( - KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT)); - mPhoneWindowManager.assertSetSplitscreenFocus(true); - - Assert.assertTrue(sendKeyGestureEventComplete( - KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT)); - mPhoneWindowManager.assertSetSplitscreenFocus(false); - } - - @Test public void testKeyGestureShortcutHelper() { Assert.assertTrue(sendKeyGestureEventComplete( KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER)); diff --git a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java index 82a5add407f4..d961a6acc9c1 100644 --- a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java @@ -42,6 +42,7 @@ import android.os.Handler; import android.os.Looper; import android.os.UserHandle; import android.platform.test.annotations.EnableFlags; +import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; import android.view.KeyEvent; import android.view.KeyboardShortcutGroup; @@ -64,7 +65,7 @@ import java.util.Collections; * Build/Install/Run: * atest ModifierShortcutManagerTests */ - +@Presubmit @SmallTest @EnableFlags(com.android.hardware.input.Flags.FLAG_MODIFIER_SHORTCUT_MANAGER_REFACTOR) public class ModifierShortcutManagerTests { @@ -127,7 +128,7 @@ public class ModifierShortcutManagerTests { // Total valid shortcuts. KeyboardShortcutGroup group = mModifierShortcutManager.getApplicationLaunchKeyboardShortcuts(-1); - assertEquals(13, group.getItems().size()); + assertEquals(11, group.getItems().size()); // Total valid shift shortcuts. assertEquals(3, group.getItems().stream() diff --git a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java index af3dc1da4dcc..c73ce23fe6b5 100644 --- a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java @@ -48,6 +48,7 @@ import android.app.AppOpsManager; import android.content.Context; import android.hardware.input.InputManager; import android.os.PowerManager; +import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; import androidx.test.filters.SmallTest; @@ -72,6 +73,7 @@ import org.junit.Test; * Build/Install/Run: * atest WmTests:PhoneWindowManagerTests */ +@Presubmit @SmallTest public class PhoneWindowManagerTests { diff --git a/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java b/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java index 33ccec3f785a..53e82bad818d 100644 --- a/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java @@ -28,6 +28,7 @@ import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_POWER_GO_ import static org.junit.Assert.assertEquals; import android.platform.test.annotations.EnableFlags; +import android.platform.test.annotations.Presubmit; import android.provider.Settings; import android.view.Display; @@ -40,6 +41,7 @@ import org.junit.Test; * Build/Install/Run: * atest WmTests:PowerKeyGestureTests */ +@Presubmit public class PowerKeyGestureTests extends ShortcutKeyTestBase { @Before public void setUp() { diff --git a/services/tests/wmtests/src/com/android/server/policy/SingleKeyGestureTests.java b/services/tests/wmtests/src/com/android/server/policy/SingleKeyGestureTests.java index ff8b6d3c1962..6c6594220bad 100644 --- a/services/tests/wmtests/src/com/android/server/policy/SingleKeyGestureTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/SingleKeyGestureTests.java @@ -23,6 +23,8 @@ import static android.view.KeyEvent.KEYCODE_POWER; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; +import static com.android.hardware.input.Flags.FLAG_ABORT_SLOW_MULTI_PRESS; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -35,9 +37,14 @@ import android.os.HandlerThread; import android.os.Looper; import android.os.Process; import android.os.SystemClock; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; import android.view.KeyEvent; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import java.util.concurrent.BlockingQueue; @@ -51,7 +58,10 @@ import java.util.concurrent.TimeUnit; * Build/Install/Run: * atest WmTests:SingleKeyGestureTests */ +@Presubmit public class SingleKeyGestureTests { + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private SingleKeyGestureDetector mDetector; private int mMaxMultiPressCount = 3; @@ -260,6 +270,44 @@ public class SingleKeyGestureTests { } @Test + @EnableFlags(FLAG_ABORT_SLOW_MULTI_PRESS) + public void testMultipress_noLongPressBehavior_longPressCancelsMultiPress() + throws InterruptedException { + mLongPressOnPowerBehavior = false; + + pressKey(KEYCODE_POWER, 0 /* pressTime */); + pressKey(KEYCODE_POWER, mLongPressTime /* pressTime */); + + assertFalse(mMultiPressed.await(mWaitTimeout, TimeUnit.MILLISECONDS)); + } + + @Test + @EnableFlags(FLAG_ABORT_SLOW_MULTI_PRESS) + public void testMultipress_noVeryLongPressBehavior_veryLongPressCancelsMultiPress() + throws InterruptedException { + mLongPressOnPowerBehavior = false; + mVeryLongPressOnPowerBehavior = false; + + pressKey(KEYCODE_POWER, 0 /* pressTime */); + pressKey(KEYCODE_POWER, mVeryLongPressTime /* pressTime */); + + assertFalse(mMultiPressed.await(mWaitTimeout, TimeUnit.MILLISECONDS)); + } + + @Test + @DisableFlags(FLAG_ABORT_SLOW_MULTI_PRESS) + public void testMultipress_flagDisabled_noLongPressBehavior_longPressDoesNotCancelMultiPress() + throws InterruptedException { + mLongPressOnPowerBehavior = false; + mExpectedMultiPressCount = 2; + + pressKey(KEYCODE_POWER, 0 /* pressTime */); + pressKey(KEYCODE_POWER, mLongPressTime /* pressTime */); + + assertTrue(mMultiPressed.await(mWaitTimeout, TimeUnit.MILLISECONDS)); + } + + @Test public void testMultiPress() throws InterruptedException { // Double presses. mExpectedMultiPressCount = 2; diff --git a/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java b/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java index 3ea3235df0f4..833dd5d768e4 100644 --- a/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java @@ -35,6 +35,7 @@ import android.app.ActivityTaskManager.RootTaskInfo; import android.content.ComponentName; import android.hardware.input.KeyGestureEvent; import android.os.RemoteException; +import android.platform.test.annotations.Presubmit; import android.provider.Settings; import android.view.Display; @@ -47,6 +48,7 @@ import org.junit.Test; * Build/Install/Run: * atest WmTests:StemKeyGestureTests */ +@Presubmit public class StemKeyGestureTests extends ShortcutKeyTestBase { private static final String TEST_TARGET_ACTIVITY = "com.android.server.policy/.TestActivity"; diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java index 4ff3d433632a..f88492477487 100644 --- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java +++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java @@ -925,11 +925,6 @@ class TestPhoneWindowManager { verify(mStatusBarManagerInternal).moveFocusedTaskToStageSplit(anyInt(), eq(leftOrTop)); } - void assertSetSplitscreenFocus(boolean leftOrTop) { - mTestLooper.dispatchAll(); - verify(mStatusBarManagerInternal).setSplitscreenFocus(eq(leftOrTop)); - } - void assertStatusBarStartAssist() { mTestLooper.dispatchAll(); verify(mStatusBarManagerInternal).startAssist(any()); diff --git a/services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java b/services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java index 3ca352cfa60d..9e59bced01f1 100644 --- a/services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java @@ -57,6 +57,7 @@ import android.content.res.Resources; import android.os.PowerManager; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; +import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.view.Display; @@ -83,6 +84,7 @@ import java.util.function.BooleanSupplier; * * <p>Build/Install/Run: atest WmTests:WindowWakeUpPolicyTests */ +@Presubmit public final class WindowWakeUpPolicyTests { @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule(); @Rule public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule(); diff --git a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java index 51706d72cb35..902a58379ae0 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java @@ -589,8 +589,7 @@ public class BackgroundActivityStartControllerTests { + "realCallerApp: null; " + "balAllowedByPiSender: BSP.ALLOW_BAL; " + "realCallerStartMode: MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; " - + "balRequireOptInByPendingIntentCreator: true; " - + "balDontBringExistingBackgroundTaskStackToFg: true]"); + + "balRequireOptInByPendingIntentCreator: true]"); } @Test @@ -692,7 +691,6 @@ public class BackgroundActivityStartControllerTests { + "realCallerApp: null; " + "balAllowedByPiSender: BSP.ALLOW_FGS; " + "realCallerStartMode: MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; " - + "balRequireOptInByPendingIntentCreator: true; " - + "balDontBringExistingBackgroundTaskStackToFg: true]"); + + "balRequireOptInByPendingIntentCreator: true]"); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index 5ac3e483231c..7af4ede05363 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -4461,7 +4461,46 @@ public class SizeCompatTests extends WindowTestsBase { // are aligned to the top of the parentAppBounds assertEquals(new Rect(0, notchHeight, 1000, 1200), appBounds); assertEquals(new Rect(0, 0, 1000, 1200), bounds); + } + + @Test + @DisableCompatChanges({ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED}) + public void testInFreeform_boundsSandboxedToAppBounds() { + final int dw = 2800; + final int dh = 1400; + final int notchHeight = 100; + final DisplayContent display = new TestDisplayContent.Builder(mAtm, dw, dh) + .setNotch(notchHeight) + .build(); + setUpApp(display); + prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); + mTask.mDisplayContent.getDefaultTaskDisplayArea() + .setWindowingMode(WindowConfiguration.WINDOWING_MODE_FREEFORM); + mTask.setWindowingMode(WINDOWING_MODE_FREEFORM); + Rect appBounds = new Rect(0, 0, 1000, 500); + Rect bounds = new Rect(0, 0, 1000, 600); + mTask.getWindowConfiguration().setAppBounds(appBounds); + mTask.getWindowConfiguration().setBounds(bounds); + mActivity.onConfigurationChanged(mTask.getConfiguration()); + + // Bounds are sandboxed to appBounds in freeform. + assertDownScaled(); + assertEquals(mActivity.getWindowConfiguration().getAppBounds(), + mActivity.getWindowConfiguration().getBounds()); + + // Exit freeform. + mTask.mDisplayContent.getDefaultTaskDisplayArea() + .setWindowingMode(WindowConfiguration.WINDOWING_MODE_FULLSCREEN); + mTask.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + mTask.getWindowConfiguration().setBounds(new Rect(0, 0, dw, dh)); + mActivity.onConfigurationChanged(mTask.getConfiguration()); + assertFitted(); + appBounds = mActivity.getWindowConfiguration().getAppBounds(); + bounds = mActivity.getWindowConfiguration().getBounds(); + // Bounds are not sandboxed to appBounds. + assertNotEquals(appBounds, bounds); + assertEquals(notchHeight, appBounds.top - bounds.top); } @Test diff --git a/services/usb/OWNERS b/services/usb/OWNERS index 2dff392d4e34..259261252032 100644 --- a/services/usb/OWNERS +++ b/services/usb/OWNERS @@ -1,9 +1,9 @@ -anothermark@google.com +vmartensson@google.com +nkapron@google.com febinthattil@google.com -aprasath@google.com +shubhankarm@google.com badhri@google.com elaurent@google.com albertccwang@google.com jameswei@google.com howardyen@google.com -kumarashishg@google.com
\ No newline at end of file diff --git a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl index b32379ae4b1e..4d9df4666016 100644 --- a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl +++ b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl @@ -415,4 +415,5 @@ interface ITelecomService { */ boolean hasForegroundServiceDelegation(in PhoneAccountHandle phoneAccountHandle, String callingPackage); + void setMetricsTestMode(boolean enabled); } diff --git a/telephony/java/android/telephony/satellite/SatelliteSubscriberInfo.java b/telephony/java/android/telephony/satellite/SatelliteSubscriberInfo.java index 9d9cac9702bb..d62fd63aeda6 100644 --- a/telephony/java/android/telephony/satellite/SatelliteSubscriberInfo.java +++ b/telephony/java/android/telephony/satellite/SatelliteSubscriberInfo.java @@ -22,8 +22,10 @@ import android.annotation.NonNull; import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; +import android.telephony.Rlog; import com.android.internal.telephony.flags.Flags; +import com.android.internal.telephony.util.TelephonyUtils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -220,7 +222,7 @@ public final class SatelliteSubscriberInfo implements Parcelable { StringBuilder sb = new StringBuilder(); sb.append("SubscriberId:"); - sb.append(mSubscriberId); + sb.append(Rlog.pii(TelephonyUtils.IS_DEBUGGABLE, mSubscriberId)); sb.append(","); sb.append("CarrierId:"); diff --git a/telephony/java/android/telephony/satellite/SatelliteSubscriberProvisionStatus.java b/telephony/java/android/telephony/satellite/SatelliteSubscriberProvisionStatus.java index fb4f89ded547..75d3ec6d3fe6 100644 --- a/telephony/java/android/telephony/satellite/SatelliteSubscriberProvisionStatus.java +++ b/telephony/java/android/telephony/satellite/SatelliteSubscriberProvisionStatus.java @@ -21,8 +21,10 @@ import android.annotation.NonNull; import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; +import android.telephony.Rlog; import com.android.internal.telephony.flags.Flags; +import com.android.internal.telephony.util.TelephonyUtils; import java.util.Objects; @@ -132,7 +134,7 @@ public final class SatelliteSubscriberProvisionStatus implements Parcelable { StringBuilder sb = new StringBuilder(); sb.append("SatelliteSubscriberInfo:"); - sb.append(mSubscriberInfo); + sb.append(Rlog.pii(TelephonyUtils.IS_DEBUGGABLE, mSubscriberInfo)); sb.append(","); sb.append("ProvisionStatus:"); diff --git a/telephony/java/com/android/internal/telephony/util/WorkerThread.java b/telephony/java/com/android/internal/telephony/util/WorkerThread.java new file mode 100644 index 000000000000..f5b653656352 --- /dev/null +++ b/telephony/java/com/android/internal/telephony/util/WorkerThread.java @@ -0,0 +1,130 @@ +/* + * 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.internal.telephony.util; + +import android.annotation.NonNull; +import android.os.Handler; +import android.os.HandlerExecutor; +import android.os.HandlerThread; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; + +/** + * Shared singleton worker thread for each process. + * + * This thread should be used for work that needs to be executed at standard priority + * but not on the main thread. This is suitable for handling asynchronous tasks that + * are ephemeral or require enough work that they shouldn't block the main thread, but + * should not block each other for more than around 100ms. + */ +public final class WorkerThread extends HandlerThread { + private static volatile WorkerThread sInstance; + private static volatile Handler sHandler; + private static volatile HandlerExecutor sHandlerExecutor; + private static final Object sLock = new Object(); + + private CountDownLatch mInitLock = new CountDownLatch(1); + + + private WorkerThread() { + super("android.telephony.worker"); + } + + private static void ensureThread() { + if (sInstance != null) return; + synchronized (sLock) { + if (sInstance != null) return; + + final WorkerThread tmpThread = new WorkerThread(); + tmpThread.start(); + + try { + tmpThread.mInitLock.await(); + } catch (InterruptedException ignored) { + } + + + sHandler = new Handler( + tmpThread.getLooper(), + /* callback= */ null, + /* async= */ false, + /* shared= */ true); + sHandlerExecutor = new HandlerExecutor(sHandler); + sInstance = tmpThread; // Note: order matters here. sInstance must be assigned last. + + } + } + + @Override + protected void onLooperPrepared() { + mInitLock.countDown(); + } + + /** + * Get the worker thread directly. + * + * Users of this thread should take care not to block it for extended periods of + * time. + * + * @return a HandlerThread, never null + */ + @NonNull public static HandlerThread get() { + ensureThread(); + return sInstance; + } + + /** + * Get a Handler that can process Runnables. + * + * @return a Handler, never null + */ + @NonNull public static Handler getHandler() { + ensureThread(); + return sHandler; + } + + /** + * Get an Executor that can process Runnables + * + * @return an Executor, never null + */ + @NonNull public static Executor getExecutor() { + ensureThread(); + return sHandlerExecutor; + } + + /** + * A method to reset the WorkerThread from scratch. + * + * This method should only be used for unit testing. In production it would have + * catastrophic consequences. Do not ever use this outside of tests. + */ + @VisibleForTesting + public static void reset() { + synchronized (sLock) { + if (sInstance == null) return; + sInstance.quitSafely(); + sInstance = null; + sHandler = null; + sHandlerExecutor = null; + ensureThread(); + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/RecentTasksUtils.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/RecentTasksUtils.kt index aa262f9dfd6a..1a5fda7c8324 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/RecentTasksUtils.kt +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/RecentTasksUtils.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.utils +package com.android.server.wm.flicker.helpers import android.app.Instrumentation @@ -24,4 +24,4 @@ object RecentTasksUtils { "dumpsys activity service SystemUIService WMShell recents clearAll" ) } -}
\ No newline at end of file +} diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt index e0532633d40b..eef4e6f58463 100644 --- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt @@ -467,30 +467,6 @@ class KeyGestureControllerTests { intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) ), TestData( - "CTRL + ALT + DPAD_LEFT -> Change Splitscreen Focus Left", - intArrayOf( - KeyEvent.KEYCODE_CTRL_LEFT, - KeyEvent.KEYCODE_ALT_LEFT, - KeyEvent.KEYCODE_DPAD_LEFT - ), - KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT, - intArrayOf(KeyEvent.KEYCODE_DPAD_LEFT), - KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) - ), - TestData( - "CTRL + ALT + DPAD_RIGHT -> Change Splitscreen Focus Right", - intArrayOf( - KeyEvent.KEYCODE_CTRL_LEFT, - KeyEvent.KEYCODE_ALT_LEFT, - KeyEvent.KEYCODE_DPAD_RIGHT - ), - KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT, - intArrayOf(KeyEvent.KEYCODE_DPAD_RIGHT), - KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) - ), - TestData( "META + / -> Open Shortcut Helper", intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_SLASH), KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER, diff --git a/tests/Tracing/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java index 9e029a8d5e57..72b1780ceb06 100644 --- a/tests/Tracing/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java @@ -33,6 +33,7 @@ import org.junit.runners.JUnit4; import perfetto.protos.ProtologCommon; import java.io.File; +import java.io.IOException; @Presubmit @RunWith(JUnit4.class) @@ -159,4 +160,14 @@ public class ProtoLogViewerConfigReaderTest { loadViewerConfig(); unloadViewerConfig(); } + + @Test + public void testMessageHashIsAvailableInFile() throws IOException { + Truth.assertThat(mConfig.messageHashIsAvailableInFile(1)).isTrue(); + Truth.assertThat(mConfig.messageHashIsAvailableInFile(2)).isTrue(); + Truth.assertThat(mConfig.messageHashIsAvailableInFile(3)).isTrue(); + Truth.assertThat(mConfig.messageHashIsAvailableInFile(4)).isTrue(); + Truth.assertThat(mConfig.messageHashIsAvailableInFile(5)).isTrue(); + Truth.assertThat(mConfig.messageHashIsAvailableInFile(6)).isFalse(); + } } diff --git a/tests/testables/src/android/testing/OWNERS b/tests/testables/src/android/testing/OWNERS new file mode 100644 index 000000000000..f31666b43654 --- /dev/null +++ b/tests/testables/src/android/testing/OWNERS @@ -0,0 +1,2 @@ +# MessageQueue-related classes +per-file TestableLooper.java = mfasheh@google.com, shayba@google.com diff --git a/tests/testables/src/android/testing/TestableLooper.java b/tests/testables/src/android/testing/TestableLooper.java index be5c84c0353c..7d07d42b8042 100644 --- a/tests/testables/src/android/testing/TestableLooper.java +++ b/tests/testables/src/android/testing/TestableLooper.java @@ -16,11 +16,13 @@ package android.testing; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.Instrumentation; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.os.MessageQueue; +import android.os.SystemClock; import android.os.TestLooperManager; import android.util.ArrayMap; @@ -32,7 +34,7 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.lang.reflect.Field; +import java.util.LinkedList; import java.util.Map; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; @@ -42,6 +44,12 @@ import java.util.concurrent.atomic.AtomicBoolean; * and provide an easy annotation for use with tests. * * @see TestableLooperTest TestableLooperTest for examples. + * + * @deprecated Use {@link android.os.TestLooperManager} or {@link + * org.robolectric.shadows.ShadowLooper} instead. + * This class is not actively maintained. + * Both of the recommended alternatives allow fine control of execution. + * The Robolectric class also allows advancing time. */ public class TestableLooper { @@ -50,9 +58,6 @@ public class TestableLooper { * catch crashes. */ public static final boolean HOLD_MAIN_THREAD = false; - private static final Field MESSAGE_QUEUE_MESSAGES_FIELD; - private static final Field MESSAGE_NEXT_FIELD; - private static final Field MESSAGE_WHEN_FIELD; private Looper mLooper; private MessageQueue mQueue; @@ -61,19 +66,6 @@ public class TestableLooper { private Handler mHandler; private TestLooperManager mQueueWrapper; - static { - try { - MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages"); - MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true); - MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next"); - MESSAGE_NEXT_FIELD.setAccessible(true); - MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when"); - MESSAGE_WHEN_FIELD.setAccessible(true); - } catch (NoSuchFieldException e) { - throw new RuntimeException("Failed to initialize TestableLooper", e); - } - } - public TestableLooper(Looper l) throws Exception { this(acquireLooperManager(l), l); } @@ -216,29 +208,17 @@ public class TestableLooper { } public void moveTimeForward(long milliSeconds) { - try { - Message msg = getMessageLinkedList(); - while (msg != null) { - long updatedWhen = msg.getWhen() - milliSeconds; - if (updatedWhen < 0) { - updatedWhen = 0; - } - MESSAGE_WHEN_FIELD.set(msg, updatedWhen); - msg = (Message) MESSAGE_NEXT_FIELD.get(msg); + long futureWhen = SystemClock.uptimeMillis() + milliSeconds; + // Find messages in the queue enqueued within the future time, and execute them now. + while (true) { + Long peekWhen = mQueueWrapper.peekWhen(); + if (peekWhen == null || peekWhen > futureWhen) { + break; + } + Message message = mQueueWrapper.poll(); + if (message != null) { + mQueueWrapper.execute(message); } - } catch (IllegalAccessException e) { - throw new RuntimeException("Access failed in TestableLooper: set - Message.when", e); - } - } - - private Message getMessageLinkedList() { - try { - MessageQueue queue = mLooper.getQueue(); - return (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(queue); - } catch (IllegalAccessException e) { - throw new RuntimeException( - "Access failed in TestableLooper: get - MessageQueue.mMessages", - e); } } diff --git a/tests/utils/testutils/java/android/os/test/OWNERS b/tests/utils/testutils/java/android/os/test/OWNERS index 3a9129e1bb69..6448261102fa 100644 --- a/tests/utils/testutils/java/android/os/test/OWNERS +++ b/tests/utils/testutils/java/android/os/test/OWNERS @@ -1 +1,4 @@ per-file FakePermissionEnforcer.java = file:/tests/EnforcePermission/OWNERS + +# MessageQueue-related classes +per-file TestLooper.java = mfasheh@google.com, shayba@google.com diff --git a/tests/utils/testutils/java/android/os/test/TestLooper.java b/tests/utils/testutils/java/android/os/test/TestLooper.java index 56b0a25ed2dd..bca95917b9af 100644 --- a/tests/utils/testutils/java/android/os/test/TestLooper.java +++ b/tests/utils/testutils/java/android/os/test/TestLooper.java @@ -24,31 +24,38 @@ import android.os.Looper; import android.os.Message; import android.os.MessageQueue; import android.os.SystemClock; +import android.os.TestLooperManager; import android.util.Log; +import androidx.test.InstrumentationRegistry; + import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; +import java.util.ArrayDeque; +import java.util.Queue; import java.util.concurrent.Executor; /** - * Creates a looper whose message queue can be manipulated - * This allows testing code that uses a looper to dispatch messages in a deterministic manner - * Creating a TestLooper will also install it as the looper for the current thread + * Creates a looper whose message queue can be manipulated This allows testing code that uses a + * looper to dispatch messages in a deterministic manner Creating a TestLooper will also install it + * as the looper for the current thread + * + * @deprecated Use {@link android.os.TestLooperManager} or {@link + * org.robolectric.shadows.ShadowLooper} instead. + * This class is not actively maintained. + * Both of the recommended alternatives allow fine control of execution. + * The Robolectric class also allows advancing time. */ public class TestLooper { - protected final Looper mLooper; + private final Looper mLooper; + private final TestLooperManager mTestLooperManager; + private final Clock mClock; private static final Constructor<Looper> LOOPER_CONSTRUCTOR; private static final Field THREAD_LOCAL_LOOPER_FIELD; - private static final Field MESSAGE_QUEUE_MESSAGES_FIELD; - private static final Field MESSAGE_NEXT_FIELD; - private static final Field MESSAGE_WHEN_FIELD; - private static final Method MESSAGE_MARK_IN_USE_METHOD; private static final String TAG = "TestLooper"; - private final Clock mClock; private AutoDispatchThread mAutoDispatchThread; @@ -58,14 +65,6 @@ public class TestLooper { LOOPER_CONSTRUCTOR.setAccessible(true); THREAD_LOCAL_LOOPER_FIELD = Looper.class.getDeclaredField("sThreadLocal"); THREAD_LOCAL_LOOPER_FIELD.setAccessible(true); - MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages"); - MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true); - MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next"); - MESSAGE_NEXT_FIELD.setAccessible(true); - MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when"); - MESSAGE_WHEN_FIELD.setAccessible(true); - MESSAGE_MARK_IN_USE_METHOD = Message.class.getDeclaredMethod("markInUse"); - MESSAGE_MARK_IN_USE_METHOD.setAccessible(true); } catch (NoSuchFieldException | NoSuchMethodException e) { throw new RuntimeException("Failed to initialize TestLooper", e); } @@ -100,6 +99,8 @@ public class TestLooper { throw new RuntimeException("Reflection error constructing or accessing looper", e); } + mTestLooperManager = + InstrumentationRegistry.getInstrumentation().acquireLooperManager(mLooper); mClock = clock; } @@ -111,78 +112,61 @@ public class TestLooper { return new HandlerExecutor(new Handler(getLooper())); } - private Message getMessageLinkedList() { - try { - MessageQueue queue = mLooper.getQueue(); - return (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(queue); - } catch (IllegalAccessException e) { - throw new RuntimeException("Access failed in TestLooper: get - MessageQueue.mMessages", - e); - } - } - public void moveTimeForward(long milliSeconds) { - try { - Message msg = getMessageLinkedList(); - while (msg != null) { - long updatedWhen = msg.getWhen() - milliSeconds; - if (updatedWhen < 0) { - updatedWhen = 0; - } - MESSAGE_WHEN_FIELD.set(msg, updatedWhen); - msg = (Message) MESSAGE_NEXT_FIELD.get(msg); + // Drain all Messages from the queue. + Queue<Message> messages = new ArrayDeque<>(); + while (true) { + Message message = mTestLooperManager.poll(); + if (message == null) { + break; } - } catch (IllegalAccessException e) { - throw new RuntimeException("Access failed in TestLooper: set - Message.when", e); - } - } - private long currentTime() { - return mClock.uptimeMillis(); - } + // Adjust the Message's delivery time. + long newWhen = message.when - milliSeconds; + if (newWhen < 0) { + newWhen = 0; + } + message.when = newWhen; + messages.add(message); + } - private Message messageQueueNext() { - try { - long now = currentTime(); - - Message prevMsg = null; - Message msg = getMessageLinkedList(); - if (msg != null && msg.getTarget() == null) { - // Stalled by a barrier. Find the next asynchronous message in - // the queue. - do { - prevMsg = msg; - msg = (Message) MESSAGE_NEXT_FIELD.get(msg); - } while (msg != null && !msg.isAsynchronous()); + // Repost all Messages back to the queuewith a new time. + while (true) { + Message message = messages.poll(); + if (message == null) { + break; } - if (msg != null) { - if (now >= msg.getWhen()) { - // Got a message. - if (prevMsg != null) { - MESSAGE_NEXT_FIELD.set(prevMsg, MESSAGE_NEXT_FIELD.get(msg)); - } else { - MESSAGE_QUEUE_MESSAGES_FIELD.set(mLooper.getQueue(), - MESSAGE_NEXT_FIELD.get(msg)); - } - MESSAGE_NEXT_FIELD.set(msg, null); - MESSAGE_MARK_IN_USE_METHOD.invoke(msg); - return msg; - } + + Runnable callback = message.getCallback(); + Handler handler = message.getTarget(); + long when = message.getWhen(); + + // The Message cannot be re-enqueued because it is marked in use. + // Make a copy of the Message and recycle the original. + // This resets {@link Message#isInUse()} but retains all other content. + { + Message newMessage = Message.obtain(); + newMessage.copyFrom(message); + newMessage.setCallback(callback); + mTestLooperManager.recycle(message); + message = newMessage; } - } catch (IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException("Access failed in TestLooper", e); + + // Send the Message back to its Handler to be re-enqueued. + handler.sendMessageAtTime(message, when); } + } - return null; + private long currentTime() { + return mClock.uptimeMillis(); } /** * @return true if there are pending messages in the message queue */ public synchronized boolean isIdle() { - Message messageList = getMessageLinkedList(); - - return messageList != null && currentTime() >= messageList.getWhen(); + Long when = mTestLooperManager.peekWhen(); + return when != null && currentTime() >= when; } /** @@ -190,7 +174,7 @@ public class TestLooper { */ public synchronized Message nextMessage() { if (isIdle()) { - return messageQueueNext(); + return mTestLooperManager.poll(); } else { return null; } @@ -202,7 +186,7 @@ public class TestLooper { */ public synchronized void dispatchNext() { assertTrue(isIdle()); - Message msg = messageQueueNext(); + Message msg = mTestLooperManager.poll(); if (msg == null) { return; } diff --git a/tools/aapt2/tools/finalize_res.py b/tools/aapt2/tools/finalize_res.py index 0e4d865bc890..059f3b2087cf 100755 --- a/tools/aapt2/tools/finalize_res.py +++ b/tools/aapt2/tools/finalize_res.py @@ -38,13 +38,22 @@ Usage: $ANDROID_BUILD_TOP/frameworks/base/tools/aapt2/tools/finalize_res.py \ import re import sys +import subprocess +from collections import defaultdict resTypes = ["attr", "id", "style", "string", "dimen", "color", "array", "drawable", "layout", "anim", "animator", "interpolator", "mipmap", "integer", "transition", "raw", "bool", "fraction"] +NO_FLAG_MAGIC_CONSTANT = "no_flag" + +_aconfig_map = {} +_not_finalized = defaultdict(list) _type_ids = {} _type = "" +_finalized_flags = defaultdict(list) +_non_finalized_flags = defaultdict(list) + _lowest_staging_first_id = 0x01FFFFFF @@ -53,13 +62,53 @@ _lowest_staging_first_id = 0x01FFFFFF prefixed with removed_. The IDs are assigned without holes starting from the last ID for that type currently finalized in public-final.xml. """ -def finalize_item(raw): - name = raw.group(1) - if re.match(r'_*removed.+', name): - return "" +def finalize_item(comment_and_item): + print("Processing:\n" + comment_and_item) + name = re.search('<public name="(.+?)"',comment_and_item, flags=re.DOTALL).group(1) + if re.match('removed_.+', name): + # Remove it from <staging-public-group> in public-staging.xml + # Include it as is in <staging-public-group-final> in public-final.xml + # Don't assign an id in public-final.xml + return ("", comment_and_item, "") + + comment = re.search(' *<!--.+?-->\n', comment_and_item, flags=re.DOTALL).group(0) + + match = re.search('<!-- @FlaggedApi\((.+?)\)', comment, flags=re.DOTALL) + if match: + flag = match.group(1) + else: + flag = NO_FLAG_MAGIC_CONSTANT + + if flag.startswith("\""): + # Flag is a string value, just remove " + flag = flag.replace("\"", "") + else: + # Flag is a java constant, convert to string value + flag = flag.replace(".Flags.FLAG_", ".").lower() + + if flag not in _aconfig_map: + raise Exception("Unknown flag: " + flag) + + # READ_ONLY-ENABLED is a magic string from printflags output below + if _aconfig_map[flag] != "READ_ONLY-ENABLED": + _non_finalized_flags[flag].append(name) + # Keep it as is in <staging-public-group> in public-staging.xml + # Include as magic constant "removed_" in <staging-public-group-final> in public-final.xml + # Don't assign an id in public-final.xml + return (comment_and_item, " <public name=\"removed_\" />\n", "") + + _finalized_flags[flag].append(name) + id = _type_ids[_type] _type_ids[_type] += 1 - return ' <public type="%s" name="%s" id="%s" />\n' % (_type, name, '0x{0:0{1}x}'.format(id, 8)) + + # Removes one indentation step to align the comment with the item outside the + comment = re.sub("^ ", "", comment, flags=re.MULTILINE) + + # Remove from <staging-public-group> in public-staging.xml + # Include as is in <staging-public-group-final> in public-final.xml + # Assign an id in public-final.xml + return ("", comment_and_item, comment + ' <public type="%s" name="%s" id="%s" />\n' % (_type, name, '0x{0:0{1}x}'.format(id, 8))) """ @@ -72,10 +121,26 @@ def finalize_group(raw): _type = raw.group(1) id = int(raw.group(2), 16) _type_ids[_type] = _type_ids.get(_type, id) - (res, count) = re.subn(' {0,4}<public name="(.+?)" */>\n', finalize_item, raw.group(3)) - if count > 0: - res = raw.group(0).replace("staging-public-group", - "staging-public-group-final") + '\n' + res + + + all = re.findall(' *<!--.*?<public name=".+?" */>\n', raw.group(3), flags=re.DOTALL) + res = "" + group_matches = "" + for match in all: + (staging_group, final_group, final_id_assignment) = finalize_item(match) + + if staging_group: + _not_finalized[_type].append(staging_group) + + if final_group: + group_matches += final_group + + if final_id_assignment: + res += final_id_assignment + + # Only add it to final.xml if new ids were actually assigned + if res: + res = '<staging-public-group-final type="%s" first-id="%s">\n%s </staging-public-group-final>\n\n%s' % (_type, raw.group(2), group_matches, res) _lowest_staging_first_id = min(id, _lowest_staging_first_id) return res @@ -88,6 +153,15 @@ def collect_ids(raw): id = int(m.group(2), 16) _type_ids[type] = max(id + 1, _type_ids.get(type, 0)) +# This is a hack and assumes this script is run from the top directory +output=subprocess.run("printflags --format='{fully_qualified_name} {permission}-{state}'", shell=True, capture_output=True, encoding="utf-8", check=True) +for line in output.stdout.splitlines(): + parts = line.split() + key = parts[0] + value = parts[1] + _aconfig_map[key]=value + +_aconfig_map[NO_FLAG_MAGIC_CONSTANT]="READ_ONLY-DISABLED" with open(sys.argv[1], "r+") as stagingFile: with open(sys.argv[2], "r+") as finalFile: @@ -132,10 +206,25 @@ with open(sys.argv[1], "r+") as stagingFile: nextId = _lowest_staging_first_id - 0x00010000 for resType in resTypes: stagingFile.write(' <staging-public-group type="%s" first-id="%s">\n' - ' </staging-public-group>\n\n' % - (resType, '0x{0:0{1}x}'.format(nextId, 8))) + % (resType, '0x{0:0{1}x}'.format(nextId, 8))) + for item in _not_finalized[resType]: + stagingFile.write(item) + stagingFile.write(' </staging-public-group>\n\n') nextId -= 0x00010000 # Close the resources tag and truncate, since the file will be shorter than the previous stagingFile.write("</resources>\n") stagingFile.truncate() + + +print("\nFlags that had resources that were NOT finalized:") +for flag in sorted(_non_finalized_flags.keys()): + print(f" {flag}") + for value in _non_finalized_flags[flag]: + print(f" {value}") + +print("\nFlags that had resources that were finalized:") +for flag in sorted(_finalized_flags.keys()): + print(f" {flag}") + for value in _finalized_flags[flag]: + print(f" {value}") |