diff options
612 files changed, 14848 insertions, 5079 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 e0fc9590f9f7..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(); @@ -45145,7 +45147,7 @@ package android.telephony { field public static final String KEY_SHOW_ICCID_IN_SIM_STATUS_BOOL = "show_iccid_in_sim_status_bool"; field public static final String KEY_SHOW_IMS_REGISTRATION_STATUS_BOOL = "show_ims_registration_status_bool"; field public static final String KEY_SHOW_ONSCREEN_DIAL_BUTTON_BOOL = "show_onscreen_dial_button_bool"; - field @FlaggedApi("com.android.internal.telephony.flags.hide_roaming_icon") public static final String KEY_SHOW_ROAMING_INDICATOR_BOOL = "show_roaming_indicator_bool"; + field public static final String KEY_SHOW_ROAMING_INDICATOR_BOOL = "show_roaming_indicator_bool"; field public static final String KEY_SHOW_SIGNAL_STRENGTH_IN_SIM_STATUS_BOOL = "show_signal_strength_in_sim_status_bool"; field public static final String KEY_SHOW_VIDEO_CALL_CHARGES_ALERT_DIALOG_BOOL = "show_video_call_charges_alert_dialog_bool"; field public static final String KEY_SHOW_WFC_LOCATION_PRIVACY_POLICY_BOOL = "show_wfc_location_privacy_policy_bool"; 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 6e11cbc0c34b..36ef4f5f06ee 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -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..e030c6c12b4c 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 NAVIGATION_HINT_BACK_DISMISS_IME = 1 << 0; + /** + * The IME is visible. + * + * @hide + */ + public static final int NAVIGATION_HINT_IME_VISIBLE = 1 << 1; + /** + * The IME Switcher button is visible. This only takes effect while the IME is visible. + * + * @hide + */ + public static final int NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE = 1 << 2; + /** + * Navigation bar flags related to the IME state. + * + * @hide + */ + @IntDef(flag = true, prefix = { "NAVIGATION_HINT_" }, value = { + NAVIGATION_HINT_BACK_DISMISS_IME, + NAVIGATION_HINT_IME_VISIBLE, + NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface NavigationHint {} /** @hide */ public static final int WINDOW_STATUS_BAR = 1; @@ -1325,6 +1354,22 @@ public class StatusBarManager { } /** @hide */ + @NonNull + public static String navigationHintsToString(@NavigationHint int hints) { + final var hintStrings = new ArrayList<String>(); + if ((hints & NAVIGATION_HINT_BACK_DISMISS_IME) != 0) { + hintStrings.add("NAVIGATION_HINT_BACK_DISMISS_IME"); + } + if ((hints & NAVIGATION_HINT_IME_VISIBLE) != 0) { + hintStrings.add("NAVIGATION_HINT_IME_VISIBLE"); + } + if ((hints & NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE) != 0) { + hintStrings.add("NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE"); + } + return String.join(" | ", hintStrings); + } + + /** @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 c89cb553762b..8021ab4865af 100644 --- a/core/java/android/app/UiAutomation.java +++ b/core/java/android/app/UiAutomation.java @@ -228,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; @@ -1158,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); + } } } @@ -1983,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/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index 4a579a4c0e85..4e6fb8d3a8e7 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -500,6 +500,16 @@ flag { } flag { + name: "get_user_switchability_permission" + namespace: "multiuser" + description: "Update permissions for getUserSwitchability" + bug: "390458180" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "restrict_quiet_mode_credential_bug_fix_to_managed_profiles" namespace: "profile_experiences" description: "Use user states to check the state of quiet mode for managed profiles only" 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..13352d716ffa 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.NAVIGATION_HINT_BACK_DISMISS_IME; +import static android.app.StatusBarManager.NAVIGATION_HINT_IME_VISIBLE; +import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE; import static android.view.WindowInsets.Type.captionBar; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; @@ -242,10 +242,10 @@ 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 NAVIGATION_HINT_IME_VISIBLE only when necessary. + final int hints = NAVIGATION_HINT_BACK_DISMISS_IME | NAVIGATION_HINT_IME_VISIBLE | (mShouldShowImeSwitcherWhenImeIsShown - ? NAVIGATION_HINT_IME_SWITCHER_SHOWN : 0); + ? NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE : 0); navigationBarView.setNavigationIconHints(hints); navigationBarView.prepareNavButtons(this); } @@ -515,10 +515,10 @@ 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 NAVIGATION_HINT_IME_VISIBLE only when necessary. + final int hints = NAVIGATION_HINT_BACK_DISMISS_IME | NAVIGATION_HINT_IME_VISIBLE | (mShouldShowImeSwitcherWhenImeIsShown - ? NAVIGATION_HINT_IME_SWITCHER_SHOWN : 0); + ? NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE : 0); navigationBarView.setNavigationIconHints(hints); } } else { diff --git a/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java b/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java index e7e46a9482c8..4be98c46300d 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.NAVIGATION_HINT_BACK_DISMISS_IME; +import static android.app.StatusBarManager.NAVIGATION_HINT_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.NavigationHint; 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; + @NavigationHint + private int mNavigationIconHints = 0; private final int mNavBarMode = NAV_BAR_MODE_GESTURAL; private KeyButtonDrawable mBackIcon; @@ -241,10 +245,10 @@ public final class NavigationBarView extends FrameLayout { } private void orientBackButton(KeyButtonDrawable drawable) { - final boolean useAltBack = - (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0; + final boolean isBackDismissIme = + (mNavigationIconHints & NAVIGATION_HINT_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 +260,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, @@ -284,13 +288,16 @@ public final class NavigationBarView extends FrameLayout { * * @param hints bit flags defined in {@link StatusBarManager}. */ - 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 setNavigationIconHints(@NavigationHint int hints) { + if (hints == mNavigationIconHints) { + return; + } + final boolean backDismissIme = + (hints & StatusBarManager.NAVIGATION_HINT_BACK_DISMISS_IME) != 0; + final boolean oldBackDismissIme = + (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_DISMISS_IME) != 0; + if (backDismissIme != oldBackDismissIme) { + //onBackDismissImeChanged(backDismissIme); } if (DEBUG) { @@ -311,10 +318,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 = + (mNavigationIconHints & NAVIGATION_HINT_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/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..1e663342522b 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); @@ -796,7 +781,6 @@ public final class MessageQueue { } } - @NeverInline private Message nextConcurrent() { final long ptr = mPtr; if (ptr == 0) { @@ -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()) { @@ -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/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/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/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/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..a65e69eee5fe 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; @@ -57,6 +58,7 @@ import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; 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 +206,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 +289,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 +324,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 +359,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 +521,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 +542,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 +572,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 +594,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 +617,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. */ 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..5f2b95f7b137 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; @@ -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() { @@ -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/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..ae84f449c0e4 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 @@ -109,7 +109,9 @@ public class BubbleTaskViewHelper { MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); final boolean isShortcutBubble = (mBubble.hasMetadataShortcutId() || (mBubble.getShortcutInfo() != null && Flags.enableBubbleAnything())); - if (mBubble.isAppBubble()) { + 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/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/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/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..6491bf5c8f5b 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" 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 d8d1a74448c5..a92df8026715 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,16 @@ flag { } 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" @@ -1832,6 +1859,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." @@ -1846,13 +1880,6 @@ flag { } flag { - name: "glanceable_hub_shortcut_button" - namespace: "systemui" - description: "Adds a shortcut button to lockscreen to show glanceable hub." - bug: "378173531" -} - -flag { name: "spatial_model_launcher_pushback" namespace: "systemui" description: "Implement the depth push scaling effect on Launcher when users pull down shade." @@ -1922,16 +1949,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." @@ -1949,6 +1966,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)" @@ -1963,4 +1987,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/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/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/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/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/data/quickaffordance/GlanceableHubQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfigTest.kt deleted file mode 100644 index ac06a3b9293c..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfigTest.kt +++ /dev/null @@ -1,155 +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.keyguard.data.quickaffordance - -import android.platform.test.annotations.DisableFlags -import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.FlagsParameterization -import androidx.test.filters.SmallTest -import com.android.systemui.Flags -import com.android.systemui.SysuiTestCase -import com.android.systemui.communal.data.repository.communalSceneRepository -import com.android.systemui.communal.domain.interactor.setCommunalV2Available -import com.android.systemui.communal.domain.interactor.setCommunalV2Enabled -import com.android.systemui.communal.shared.model.CommunalScenes -import com.android.systemui.flags.parameterizeSceneContainerFlag -import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository -import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.collectLastValue -import com.android.systemui.kosmos.runCurrent -import com.android.systemui.kosmos.runTest -import com.android.systemui.scene.data.repository.sceneContainerRepository -import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.testKosmos -import com.google.common.truth.Truth.assertThat -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.MockitoAnnotations -import platform.test.runner.parameterized.ParameterizedAndroidJunit4 -import platform.test.runner.parameterized.Parameters - -@SmallTest -@EnableFlags(Flags.FLAG_GLANCEABLE_HUB_V2) -@RunWith(ParameterizedAndroidJunit4::class) -class GlanceableHubQuickAffordanceConfigTest(flags: FlagsParameterization?) : SysuiTestCase() { - private val kosmos = testKosmos() - private val Kosmos.underTest by Kosmos.Fixture { glanceableHubQuickAffordanceConfig } - - init { - mSetFlagsRule.setFlagsParameterization(flags!!) - } - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - - // Access the class immediately so that flows are instantiated. - // GlanceableHubQuickAffordanceConfig accesses StateFlow.value directly so we need the flows - // to start flowing before runCurrent is called in the tests. - kosmos.underTest - } - - @Test - fun lockscreenState_whenGlanceableHubEnabled_returnsVisible() = - kosmos.runTest { - kosmos.setCommunalV2Available(true) - runCurrent() - - val lockScreenState by collectLastValue(underTest.lockScreenState) - - assertThat(lockScreenState) - .isInstanceOf(KeyguardQuickAffordanceConfig.LockScreenState.Visible::class.java) - } - - @Test - fun lockscreenState_whenGlanceableHubDisabled_returnsHidden() = - kosmos.runTest { - setCommunalV2Enabled(false) - val lockScreenState by collectLastValue(underTest.lockScreenState) - runCurrent() - - assertThat(lockScreenState) - .isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden) - } - - @Test - fun lockscreenState_whenGlanceableHubNotAvailable_returnsHidden() = - kosmos.runTest { - // Hub is enabled, but not available. - setCommunalV2Enabled(true) - fakeKeyguardRepository.setKeyguardShowing(false) - val lockScreenState by collectLastValue(underTest.lockScreenState) - runCurrent() - - assertThat(lockScreenState) - .isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden) - } - - @Test - fun pickerScreenState_whenGlanceableHubEnabled_returnsDefault() = - kosmos.runTest { - setCommunalV2Enabled(true) - runCurrent() - - assertThat(underTest.getPickerScreenState()) - .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.Default()) - } - - @Test - fun pickerScreenState_whenGlanceableHubDisabled_returnsDisabled() = - kosmos.runTest { - setCommunalV2Enabled(false) - runCurrent() - - assertThat( - underTest.getPickerScreenState() - is KeyguardQuickAffordanceConfig.PickerScreenState.Disabled - ) - } - - @Test - @DisableFlags(Flags.FLAG_SCENE_CONTAINER) - fun onTriggered_changesSceneToCommunal() = - kosmos.runTest { - underTest.onTriggered(expandable = null) - runCurrent() - - assertThat(kosmos.communalSceneRepository.currentScene.value) - .isEqualTo(CommunalScenes.Communal) - } - - @Test - @EnableFlags(Flags.FLAG_SCENE_CONTAINER) - fun testTransitionToGlanceableHub_sceneContainer() = - kosmos.runTest { - underTest.onTriggered(expandable = null) - runCurrent() - - assertThat(kosmos.sceneContainerRepository.currentScene.value) - .isEqualTo(Scenes.Communal) - } - - companion object { - @JvmStatic - @Parameters(name = "{0}") - fun getParams(): List<FlagsParameterization> { - return parameterizeSceneContainerFlag() - } - } -} 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..286f8bfd63a2 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 @@ -123,8 +123,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..60a19a4c7d07 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 @@ -198,8 +198,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/navigationbar/views/NavigationBarTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java index 7f9313cbeb5b..17a1c27b029b 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.NAVIGATION_HINT_BACK_DISMISS_IME; +import static android.app.StatusBarManager.NAVIGATION_HINT_IME_VISIBLE; +import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_BUTTON_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_VISIBLE; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING; import static com.google.common.truth.Truth.assertThat; @@ -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 @@ -567,26 +572,28 @@ public class NavigationBarTest extends SysuiTestCase { BACK_DISPOSITION_DEFAULT, true); // 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, + assertEquals(NAVIGATION_HINT_BACK_DISMISS_IME | NAVIGATION_HINT_IME_VISIBLE + | NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE, 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); + assertFalse((externalNavBar.getNavigationIconHints() + & NAVIGATION_HINT_BACK_DISMISS_IME) != 0); + assertFalse((externalNavBar.getNavigationIconHints() & NAVIGATION_HINT_IME_VISIBLE) != 0); + assertFalse((externalNavBar.getNavigationIconHints() + & NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE) != 0); externalNavBar.setImeWindowStatus(EXTERNAL_DISPLAY_ID, IME_VISIBLE, BACK_DISPOSITION_DEFAULT, true); defaultNavBar.setImeWindowStatus(DEFAULT_DISPLAY, 0 /* vis */, BACK_DISPOSITION_DEFAULT, false); // 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, + assertEquals(NAVIGATION_HINT_BACK_DISMISS_IME | NAVIGATION_HINT_IME_VISIBLE + | NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE, 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); + assertFalse((defaultNavBar.getNavigationIconHints() + & NAVIGATION_HINT_BACK_DISMISS_IME) != 0); + assertFalse((defaultNavBar.getNavigationIconHints() & NAVIGATION_HINT_IME_VISIBLE) != 0); + assertFalse((defaultNavBar.getNavigationIconHints() + & NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE) != 0); } @Test @@ -602,20 +609,22 @@ 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); + assertTrue((mNavigationBar.getNavigationIconHints() + & NAVIGATION_HINT_BACK_DISMISS_IME) != 0); + assertTrue((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_IME_VISIBLE) != 0); + assertTrue((mNavigationBar.getNavigationIconHints() + & NAVIGATION_HINT_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); + assertFalse((mNavigationBar.getNavigationIconHints() + & NAVIGATION_HINT_BACK_DISMISS_IME) != 0); + assertFalse((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_IME_VISIBLE) != 0); + assertFalse((mNavigationBar.getNavigationIconHints() + & NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE) != 0); // Verify navbar altered and showing back icon when the keyguard is showing and // requesting IME insets visible. @@ -623,10 +632,11 @@ public class NavigationBarTest extends SysuiTestCase { 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); + assertTrue((mNavigationBar.getNavigationIconHints() + & NAVIGATION_HINT_BACK_DISMISS_IME) != 0); + assertTrue((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_IME_VISIBLE) != 0); + assertTrue((mNavigationBar.getNavigationIconHints() + & NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE) != 0); } @Test 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/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/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/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt index eec23d3ffb1a..55f3717535b7 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 @@ -609,7 +609,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { listOf( activeNotificationModel( key = "notif", - statusBarChipIcon = mock<StatusBarIconView>(), + statusBarChipIcon = createStatusBarIconViewOrNull(), promotedContent = promotedContentBuilder.build(), ) ) 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/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/ic_widgets.xml b/packages/SystemUI/res/drawable/ic_widgets.xml deleted file mode 100644 index 9e05809bfb33..000000000000 --- a/packages/SystemUI/res/drawable/ic_widgets.xml +++ /dev/null @@ -1,26 +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. - --> - -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:tint="?attr/colorControlNormal" - android:viewportHeight="960" - android:viewportWidth="960"> - <path - android:fillColor="@android:color/black" - android:pathData="M666,520L440,294L666,68L892,294L666,520ZM120,440L120,120L440,120L440,440L120,440ZM520,840L520,520L840,520L840,840L520,840ZM120,840L120,520L440,520L440,840L120,840ZM200,360L360,360L360,200L200,200L200,360ZM667,408L780,295L667,182L554,295L667,408ZM600,760L760,760L760,600L600,600L600,760ZM200,760L360,760L360,600L200,600L200,760ZM360,360L360,360L360,360L360,360L360,360ZM554,295L554,295L554,295L554,295L554,295ZM360,600L360,600L360,600L360,600L360,600ZM600,600L600,600L600,600L600,600L600,600Z" /> -</vector> 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/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/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/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..724a12c12490 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> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 866dfe4d8972..64367ef79856 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1351,12 +1351,6 @@ <string name="communal_widgets_disclaimer_text">To open an app using a widget, you\u2019ll need to verify it\u2019s you. Also, keep in mind that anyone can view them, even when your tablet\u2019s locked. Some widgets may not have been intended for your lock screen and may be unsafe to add here.</string> <!-- Button for user to verify they understand the information presented. [CHAR LIMIT=50] --> <string name="communal_widgets_disclaimer_button">Got it</string> - <!-- Label for a lock screen affordance to show widgets on the lock screen. [CHAR LIMIT=20] --> - <string name="glanceable_hub_lockscreen_affordance_label">Widgets</string> - <!-- Text explaining why the lock screen affordance to show widgets on the lockscreen is disabled and how to enable the affordance in settings. [CHAR LIMIT=NONE] --> - <string name="glanceable_hub_lockscreen_affordance_disabled_text">To add the \"Widgets\" shortcut, make sure \"Show widgets on lock screen\" is enabled in settings.</string> - <!-- Label for a button used to open Settings in order to enable showing widgets on the lock screen. [CHAR LIMIT=NONE] --> - <string name="glanceable_hub_lockscreen_affordance_action_button_label">Settings</string> <!-- Content description for a "show screensaver" button on glanceable hub. [CHAR LIMIT=NONE] --> <string name="accessibility_glanceable_hub_to_dream_button">Show screensaver button</string> <!-- Title shown in hub onboarding bottom sheet. [CHAR LIMIT=50] --> @@ -2089,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> @@ -2316,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> @@ -2377,6 +2374,17 @@ <!-- 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> + <!-- SysUI Tuner: Label for screen about do not disturb settings [CHAR LIMIT=60] --> <string name="volume_and_do_not_disturb">Do Not Disturb</string> @@ -4011,13 +4019,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..c95c6dd89353 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -565,16 +565,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/IOverviewProxy.aidl index d363e524a9f2..011458859db4 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl @@ -23,7 +23,7 @@ import android.os.IRemoteCallback; import android.view.MotionEvent; import com.android.systemui.shared.recents.ISystemUiProxy; -// Next ID: 38 +// Next ID: 39 oneway interface IOverviewProxy { void onActiveNavBarRegionChanges(in Region activeRegion) = 11; @@ -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/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..efe758ea0011 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.NAVIGATION_HINT_BACK_DISMISS_IME; +import static android.app.StatusBarManager.NAVIGATION_HINT_IME_VISIBLE; +import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE; import android.annotation.TargetApi; +import android.app.StatusBarManager.NavigationHint; import android.content.Context; import android.content.res.Resources; import android.graphics.Color; @@ -103,35 +104,43 @@ public class Utilities { } /** - * @return updated set of flags from InputMethodService based off {@param oldHints} - * Leaves original hints unmodified + * Gets the updated navigation icon hints, based on the current ones and the given IME state. + * + * @param oldHints current navigation icon hints. + * @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) { + @NavigationHint + public static int calculateNavigationIconHints(@NavigationHint int oldHints, + @BackDispositionMode int backDisposition, boolean isImeVisible, + boolean showImeSwitcher) { int hints = oldHints; 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) { + hints |= NAVIGATION_HINT_BACK_DISMISS_IME; } else { - hints &= ~NAVIGATION_HINT_BACK_ALT; + hints &= ~NAVIGATION_HINT_BACK_DISMISS_IME; } break; case InputMethodService.BACK_DISPOSITION_ADJUST_NOTHING: - hints &= ~NAVIGATION_HINT_BACK_ALT; + hints &= ~NAVIGATION_HINT_BACK_DISMISS_IME; break; } - if (imeShown) { - hints |= NAVIGATION_HINT_IME_SHOWN; + if (isImeVisible) { + hints |= NAVIGATION_HINT_IME_VISIBLE; } else { - hints &= ~NAVIGATION_HINT_IME_SHOWN; + hints &= ~NAVIGATION_HINT_IME_VISIBLE; } - if (showImeSwitcher) { - hints |= NAVIGATION_HINT_IME_SWITCHER_SHOWN; + if (showImeSwitcher && isImeVisible) { + hints |= NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE; } else { - hints &= ~NAVIGATION_HINT_IME_SWITCHER_SHOWN; + hints &= ~NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE; } return hints; 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/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/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/InputGestureMaps.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt index 6a42cdc876ca..8e4c9349c604 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 @@ -39,10 +39,14 @@ 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_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_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 +69,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 +85,13 @@ 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, ) val gestureToInternalKeyboardShortcutGroupLabelResIdMap = @@ -103,7 +113,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 +137,13 @@ 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, ) /** @@ -152,7 +168,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 +184,14 @@ 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, ) 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..0c98f81e7cef 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,20 @@ 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_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.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 +44,68 @@ 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) + } + ) + } + + 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/ui/ShortcutCustomizationDialogStarter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt index f1945e657d52..bd3d46d09f5e 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 @@ -108,22 +108,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/composable/ShortcutCustomizer.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt index 550438aa220f..9d43c48ee274 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 @@ -59,7 +59,12 @@ import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.key.type 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 @@ -244,7 +249,10 @@ 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 + }, ) } } @@ -397,6 +405,7 @@ private fun Description(text: String) { .width(316.dp) .wrapContentSize(Alignment.Center), color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center ) } 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/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt index a45204d41718..80675d373b8e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt @@ -28,7 +28,6 @@ object BuiltInKeyguardQuickAffordanceKeys { const val CREATE_NOTE = "create_note" const val DO_NOT_DISTURB = "do_not_disturb" const val FLASHLIGHT = "flashlight" - const val GLANCEABLE_HUB = "glanceable_hub" const val HOME_CONTROLS = "home" const val MUTE = "mute" const val QR_CODE_SCANNER = "qr_code_scanner" diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfig.kt deleted file mode 100644 index 96b07cc84705..000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfig.kt +++ /dev/null @@ -1,119 +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.keyguard.data.quickaffordance - -import android.content.Context -import android.content.Intent -import android.provider.Settings -import android.util.Log -import com.android.systemui.animation.Expandable -import com.android.systemui.common.shared.model.ContentDescription -import com.android.systemui.common.shared.model.Icon -import com.android.systemui.communal.data.repository.CommunalSceneRepository -import com.android.systemui.communal.domain.interactor.CommunalInteractor -import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor -import com.android.systemui.communal.shared.model.CommunalScenes -import com.android.systemui.communal.shared.model.CommunalTransitionKeys -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.res.R -import com.android.systemui.scene.domain.interactor.SceneInteractor -import com.android.systemui.scene.shared.flag.SceneContainerFlag -import com.android.systemui.scene.shared.model.Scenes -import javax.inject.Inject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map - -/** Lockscreen affordance that opens the glanceable hub. */ -@SysUISingleton -class GlanceableHubQuickAffordanceConfig -@Inject -constructor( - @Application private val context: Context, - private val communalSceneRepository: CommunalSceneRepository, - private val communalInteractor: CommunalInteractor, - private val communalSettingsInteractor: CommunalSettingsInteractor, - private val sceneInteractor: SceneInteractor, -) : KeyguardQuickAffordanceConfig { - - private val pickerNameResourceId = R.string.glanceable_hub_lockscreen_affordance_label - - override val key: String = BuiltInKeyguardQuickAffordanceKeys.GLANCEABLE_HUB - - override fun pickerName(): String = context.getString(pickerNameResourceId) - - override val pickerIconResourceId: Int - get() = R.drawable.ic_widgets - - override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> - get() = - communalInteractor.isCommunalAvailable.map { available -> - if (!communalSettingsInteractor.isV2FlagEnabled()) { - Log.i(TAG, "Button hidden on lockscreen: flag not enabled.") - KeyguardQuickAffordanceConfig.LockScreenState.Hidden - } else if (!available) { - Log.i(TAG, "Button hidden on lockscreen: hub not available.") - KeyguardQuickAffordanceConfig.LockScreenState.Hidden - } else { - KeyguardQuickAffordanceConfig.LockScreenState.Visible( - icon = - Icon.Resource( - pickerIconResourceId, - ContentDescription.Resource(pickerNameResourceId), - ) - ) - } - } - - override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState { - return if (!communalSettingsInteractor.isV2FlagEnabled()) { - Log.i(TAG, "Button unavailable in picker: flag not enabled.") - KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice - } else if (!communalInteractor.isCommunalEnabled.value) { - Log.i(TAG, "Button disabled in picker: hub not enabled in settings.") - KeyguardQuickAffordanceConfig.PickerScreenState.Disabled( - explanation = - context.getString(R.string.glanceable_hub_lockscreen_affordance_disabled_text), - actionText = - context.getString( - R.string.glanceable_hub_lockscreen_affordance_action_button_label - ), - actionIntent = Intent(Settings.ACTION_LOCKSCREEN_SETTINGS), - ) - } else { - KeyguardQuickAffordanceConfig.PickerScreenState.Default() - } - } - - override fun onTriggered( - expandable: Expandable? - ): KeyguardQuickAffordanceConfig.OnTriggeredResult { - if (SceneContainerFlag.isEnabled) { - sceneInteractor.changeScene(Scenes.Communal, "lockscreen to communal from shortcut") - } else { - communalSceneRepository.changeScene( - CommunalScenes.Communal, - transitionKey = CommunalTransitionKeys.SimpleFade, - ) - } - return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(true) - } - - companion object { - private const val TAG = "GlanceableHubQuickAffordanceConfig" - } -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt index 8c6fdb989daf..787a9837b860 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt @@ -36,7 +36,6 @@ interface KeyguardDataQuickAffordanceModule { camera: CameraQuickAffordanceConfig, doNotDisturb: DoNotDisturbQuickAffordanceConfig, flashlight: FlashlightQuickAffordanceConfig, - glanceableHub: GlanceableHubQuickAffordanceConfig, home: HomeControlsKeyguardQuickAffordanceConfig, mute: MuteQuickAffordanceConfig, quickAccessWallet: QuickAccessWalletKeyguardQuickAffordanceConfig, @@ -47,7 +46,6 @@ interface KeyguardDataQuickAffordanceModule { camera, doNotDisturb, flashlight, - glanceableHub, home, mute, quickAccessWallet, 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/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/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/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt index 733d7d71061e..b531c7fa49ec 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>() 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..89dcbf6aa52b 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() 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/navigationbar/NavBarHelper.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java index 173a964cc5d3..0de8c40bddaa 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java @@ -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/TaskbarDelegate.java b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java index 05d8bff2ceb6..de35dd7b52b6 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.NAVIGATION_HINT_BACK_DISMISS_IME; +import static android.app.StatusBarManager.NAVIGATION_HINT_IME_VISIBLE; +import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_BUTTON_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; @@ -30,13 +31,15 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A 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_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_BACK_DISMISS_IME; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_VISIBLE; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_BUTTON_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.NavigationHint; import android.app.StatusBarManager.WindowVisibleState; import android.content.Context; import android.graphics.Rect; @@ -111,6 +114,7 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, private TaskStackChangeListeners mTaskStackChangeListeners; private Optional<Pip> mPipOptional; private int mDefaultDisplayId; + @NavigationHint private int mNavigationIconHints; private final NavBarHelper.NavbarTaskbarStateUpdater mNavbarTaskbarStateUpdater = new NavBarHelper.NavbarTaskbarStateUpdater() { @@ -261,6 +265,20 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, } } + @Override + public void onDisplayRemoveSystemDecorations(int displayId) { + CommandQueue.Callbacks.super.onDisplayRemoveSystemDecorations(displayId); + if (mOverviewProxyService.getProxy() == null) { + return; + } + + try { + mOverviewProxyService.getProxy().onDisplayRemoveSystemDecorations(displayId); + } catch (RemoteException e) { + Log.e(TAG, "onDisplaySystemDecorationsRemoved() failed", e); + } + } + // Separated into a method to keep setDependencies() clean/readable. private LightBarTransitionsController createLightBarTransitionsController() { @@ -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, + (mNavigationIconHints & NAVIGATION_HINT_IME_VISIBLE) != 0) + .setFlag(SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE, + (mNavigationIconHints & NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE) != 0) + .setFlag(SYSUI_STATE_BACK_DISMISS_IME, + (mNavigationIconHints & NAVIGATION_HINT_BACK_DISMISS_IME) != 0) .setFlag(SYSUI_STATE_OVERVIEW_DISABLED, (mDisabledFlags & View.STATUS_BAR_DISABLE_RECENT) != 0) .setFlag(SYSUI_STATE_HOME_DISABLED, @@ -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 hints = Utilities.calculateNavigationIconHints(mNavigationIconHints, + backDisposition, isImeVisible, showImeSwitcher); + if (hints == mNavigationIconHints) { + return; } + + mNavigationIconHints = hints; + updateSysuiFlags(); } @Override 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..f1fe2802286c 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.NAVIGATION_HINT_BACK_DISMISS_IME; +import static android.app.StatusBarManager.NAVIGATION_HINT_IME_VISIBLE; +import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_BUTTON_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.navigationHintsToString; import static android.app.StatusBarManager.windowStateToString; import static android.app.WindowConfiguration.ROTATION_UNDEFINED; import static android.view.InsetsSource.FLAG_SUPPRESS_SCRIM; @@ -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_VISIBLE; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_BUTTON_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.NavigationHint; import android.content.Context; import android.content.res.Configuration; import android.graphics.Insets; @@ -233,6 +237,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements private @WindowVisibleState int mNavigationBarWindowState = WINDOW_STATE_SHOWING; + @NavigationHint private int mNavigationIconHints = 0; private @TransitionMode int mTransitionMode; private boolean mLongPressHomeEnabled; @@ -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,7 +822,6 @@ public class NavigationBar extends ViewController<NavigationBarView> implements if (mSavedState != null) { getBarTransitions().getLightTransitionsController().restoreState(mSavedState); } - setNavigationIconHints(mNavigationIconHints); setWindowVisible(isNavBarWindowVisible()); mView.setBehavior(mBehavior); setNavBarMode(mNavBarMode); @@ -1111,6 +1115,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements pw.println(" mLongPressHomeEnabled=" + mLongPressHomeEnabled); pw.println(" mNavigationBarWindowState=" + windowStateToString(mNavigationBarWindowState)); + pw.println(" mNavigationIconHints=" + navigationHintsToString(mNavigationIconHints)); pw.println(" mTransitionMode=" + BarTransitions.modeToString(mTransitionMode)); pw.println(" mTransientShown=" + mTransientShown); @@ -1135,11 +1140,12 @@ 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 hints = Utilities.calculateNavigationIconHints(mNavigationIconHints, + backDisposition, isImeVisible, showImeSwitcher); + if (hints == mNavigationIconHints) { + return; + } setNavigationIconHints(hints); checkBarModes(); @@ -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, + (mNavigationIconHints & NAVIGATION_HINT_IME_VISIBLE) != 0) + .setFlag(SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE, + (mNavigationIconHints & NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE) != 0) + .setFlag(SYSUI_STATE_BACK_DISMISS_IME, + (mNavigationIconHints & NAVIGATION_HINT_BACK_DISMISS_IME) != 0) .setFlag(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY, allowSystemGestureIgnoringBarVisibility()) .commitUpdate(mDisplayId); @@ -1926,28 +1934,36 @@ public class NavigationBar extends ViewController<NavigationBarView> implements }; @VisibleForTesting + @NavigationHint int getNavigationIconHints() { return mNavigationIconHints; } - private void setNavigationIconHints(int hints) { - if (hints == mNavigationIconHints) return; + /** + * Updates the navigation icons based on {@code hints}. + * + * @param hints bit flags defined in {@link StatusBarManager}. + */ + private void setNavigationIconHints(@NavigationHint int hints) { + if (hints == mNavigationIconHints) { + 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 = + (hints & StatusBarManager.NAVIGATION_HINT_BACK_DISMISS_IME) != 0; + final boolean oldBackDismissIme = + (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_DISMISS_IME) != 0; + if (backDismissIme != oldBackDismissIme) { + mView.onBackDismissImeChanged(backDismissIme); } - mImeVisible = (hints & NAVIGATION_HINT_IME_SHOWN) != 0; + mImeVisible = (hints & NAVIGATION_HINT_IME_VISIBLE) != 0; mView.setNavigationIconHints(hints); } if (DEBUG) { - android.widget.Toast.makeText(mContext, - "Navigation icon hints = " + hints, - 500).show(); + android.widget.Toast.makeText(mContext, "Navigation icon hints = " + hints, 500) + .show(); } mNavigationIconHints = hints; } 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..38f2d42c8869 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.NAVIGATION_HINT_BACK_DISMISS_IME; +import static android.app.StatusBarManager.NAVIGATION_HINT_IME_VISIBLE; +import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_BUTTON_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.NavigationHint; import android.content.Context; import android.content.res.Configuration; import android.graphics.Canvas; @@ -113,6 +116,7 @@ public class NavigationBarView extends FrameLayout { boolean mLongClickableAccessibilityButton; int mDisabledFlags = 0; + @NavigationHint int mNavigationIconHints = 0; private int mNavBarMode; private boolean mImeDrawsImeNavBar; @@ -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,10 @@ public class NavigationBarView extends FrameLayout { } private void orientBackButton(KeyButtonDrawable drawable) { - final boolean useAltBack = - (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0; + final boolean isBackDismissIme = + (mNavigationIconHints & NAVIGATION_HINT_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 +522,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 +563,25 @@ public class NavigationBarView extends FrameLayout { super.setLayoutDirection(layoutDirection); } - void setNavigationIconHints(int hints) { - if (hints == mNavigationIconHints) return; + void setNavigationIconHints(@NavigationHint int hints) { + if (hints == mNavigationIconHints) { + return; + } mNavigationIconHints = hints; 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 +605,8 @@ 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 = + (mNavigationIconHints & NAVIGATION_HINT_BACK_DISMISS_IME) != 0; KeyButtonDrawable backIcon = mBackIcon; orientBackButton(backIcon); KeyButtonDrawable homeIcon = mHomeDefaultIcon; @@ -607,11 +618,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 = + (mNavigationIconHints & NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE) != 0 + && !isImeRenderingNavButtons(); + mContextualButtonGroup.setButtonVisibility(R.id.ime_switcher, isImeSwitcherButtonVisible); mBarTransitions.reapplyDarkIntensity(); @@ -625,7 +637,7 @@ 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(); @@ -665,7 +677,7 @@ public class NavigationBarView extends FrameLayout { boolean isImeRenderingNavButtons() { return mImeDrawsImeNavBar && mImeCanRenderGesturalNavButtons - && (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_IME_SHOWN) != 0; + && (mNavigationIconHints & NAVIGATION_HINT_IME_VISIBLE) != 0; } @VisibleForTesting 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/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/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/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/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/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..a1b4def09ba9 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, 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/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..d82f8e722744 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 @@ -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/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/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/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/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/ShadeHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt index eae828562223..a5cd81ff3116 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, @@ -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/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/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/quickaffordance/GlanceableHubQuickAffordanceConfigKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfigKosmos.kt deleted file mode 100644 index 568324832b33..000000000000 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfigKosmos.kt +++ /dev/null @@ -1,35 +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.keyguard.data.quickaffordance - -import android.content.applicationContext -import com.android.systemui.communal.data.repository.communalSceneRepository -import com.android.systemui.communal.domain.interactor.communalInteractor -import com.android.systemui.communal.domain.interactor.communalSettingsInteractor -import com.android.systemui.kosmos.Kosmos -import com.android.systemui.scene.domain.interactor.sceneInteractor - -val Kosmos.glanceableHubQuickAffordanceConfig by - Kosmos.Fixture { - GlanceableHubQuickAffordanceConfig( - context = applicationContext, - communalInteractor = communalInteractor, - communalSceneRepository = communalSceneRepository, - communalSettingsInteractor = communalSettingsInteractor, - sceneInteractor = sceneInteractor, - ) - } 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/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/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/VolumeDialogSliderHapticsViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt deleted file mode 100644 index d6845b1ff7e3..000000000000 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt +++ /dev/null @@ -1,33 +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 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 - -val Kosmos.volumeDialogSliderHapticsViewBinder by - Kosmos.Fixture { - VolumeDialogSliderHapticsViewBinder( - volumeDialogSliderInputEventsViewModel, - vibratorHelper, - msdlPlayer, - systemClock, - ) - } 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..91775f8eed96 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -5084,39 +5084,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/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..3817ba1a28b9 100644 --- a/services/core/java/com/android/server/am/PendingIntentRecord.java +++ b/services/core/java/com/android/server/am/PendingIntentRecord.java @@ -305,10 +305,6 @@ public final class PendingIntentRecord extends IIntentSender.Stub { this.stringName = null; } - @VisibleForTesting TempAllowListDuration getAllowlistDurationLocked(IBinder allowlistToken) { - return mAllowlistDuration.get(allowlistToken); - } - void setAllowBgActivityStarts(IBinder token, int flags) { if (token == null) return; if ((flags & FLAG_ACTIVITY_SENDER) != 0) { @@ -327,12 +323,6 @@ 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; - } - } } public void registerCancelListenerLocked(IResultReceiver receiver) { @@ -713,7 +703,7 @@ public final class PendingIntentRecord extends IIntentSender.Stub { return res; } - @VisibleForTesting BackgroundStartPrivileges getBackgroundStartPrivilegesForActivitySender( + private BackgroundStartPrivileges getBackgroundStartPrivilegesForActivitySender( IBinder allowlistToken) { return mAllowBgActivityStartsForActivitySender.contains(allowlistToken) ? BackgroundStartPrivileges.allowBackgroundActivityStarts(allowlistToken) 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/backup/SystemBackupAgent.java b/services/core/java/com/android/server/backup/SystemBackupAgent.java index f66c7e115fc0..677e0c055455 100644 --- a/services/core/java/com/android/server/backup/SystemBackupAgent.java +++ b/services/core/java/com/android/server/backup/SystemBackupAgent.java @@ -35,7 +35,7 @@ import android.os.UserHandle; import android.os.UserManager; import android.util.Slog; -import com.android.server.backup.Flags; +import com.android.server.display.DisplayBackupHelper; import com.android.server.notification.NotificationBackupHelper; import com.google.android.collect.Sets; @@ -67,6 +67,7 @@ public class SystemBackupAgent extends BackupAgentHelper { private static final String APP_GENDER_HELPER = "app_gender"; private static final String COMPANION_HELPER = "companion"; private static final String SYSTEM_GENDER_HELPER = "system_gender"; + private static final String DISPLAY_HELPER = "display"; // These paths must match what the WallpaperManagerService uses. The leaf *_FILENAME // are also used in the full-backup file format, so must not change unless steps are @@ -104,7 +105,8 @@ public class SystemBackupAgent extends BackupAgentHelper { APP_LOCALES_HELPER, COMPANION_HELPER, APP_GENDER_HELPER, - SYSTEM_GENDER_HELPER); + SYSTEM_GENDER_HELPER, + DISPLAY_HELPER); /** Helpers that are enabled for full, non-system users. */ private static final Set<String> sEligibleHelpersForNonSystemUser = @@ -146,6 +148,7 @@ public class SystemBackupAgent extends BackupAgentHelper { addHelperIfEligibleForUser(COMPANION_HELPER, new CompanionBackupHelper(mUserId)); addHelperIfEligibleForUser(SYSTEM_GENDER_HELPER, new SystemGrammaticalGenderBackupHelper(mUserId)); + addHelperIfEligibleForUser(DISPLAY_HELPER, new DisplayBackupHelper(mUserId)); } @Override diff --git a/services/core/java/com/android/server/display/DisplayBackupHelper.java b/services/core/java/com/android/server/display/DisplayBackupHelper.java new file mode 100644 index 000000000000..0d3a09fb2305 --- /dev/null +++ b/services/core/java/com/android/server/display/DisplayBackupHelper.java @@ -0,0 +1,137 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.display; + + +import android.annotation.Nullable; +import android.app.backup.BlobBackupHelper; +import android.hardware.display.DisplayManagerInternal; +import android.util.AtomicFile; +import android.util.AtomicFileOutputStream; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.LocalServices; +import com.android.server.display.feature.DisplayManagerFlags; +import com.android.server.display.utils.DebugUtils; + +import java.io.IOException; + +/** + * Display manager specific information backup helper. Backs-up the entire files for the given + * user. + * @hide + */ +public class DisplayBackupHelper extends BlobBackupHelper { + private static final String TAG = "DisplayBackupHelper"; + + // current schema of the backup state blob + private static final int BLOB_VERSION = 1; + + // key under which the data blob is committed to back up + private static final String KEY_DISPLAY = "display"; + + // To enable these logs, run: + // adb shell setprop persist.log.tag.DisplayBackupHelper DEBUG + // adb reboot + private static final boolean DEBUG = DebugUtils.isDebuggable(TAG); + + private final int mUserId; + private final Injector mInjector; + + /** + * Construct a helper to manage backup/restore of entire files within Display Manager. + * + * @param userId id of the user for which backup will be done. + */ + public DisplayBackupHelper(int userId) { + this(userId, new Injector()); + } + + @VisibleForTesting + DisplayBackupHelper(int userId, Injector injector) { + super(BLOB_VERSION, KEY_DISPLAY); + mUserId = userId; + mInjector = injector; + } + + @Override + protected byte[] getBackupPayload(String key) { + if (!KEY_DISPLAY.equals(key) || !mInjector.isDisplayTopologyFlagEnabled()) { + return null; + } + try { + var result = mInjector.readTopologyFile(mUserId); + Slog.i(TAG, "getBackupPayload for " + key + " done, size=" + result.length); + return result; + } catch (IOException e) { + if (DEBUG) Slog.d(TAG, "Skip topology backup", e); + return null; + } + } + + @Override + protected void applyRestoredPayload(String key, byte[] payload) { + if (!KEY_DISPLAY.equals(key) || !mInjector.isDisplayTopologyFlagEnabled()) { + return; + } + try (var oStream = mInjector.writeTopologyFile(mUserId)) { + oStream.write(payload); + oStream.markSuccess(); + Slog.i(TAG, "applyRestoredPayload for " + key + " size=" + payload.length + + " to " + oStream); + } catch (IOException e) { + Slog.e(TAG, "applyRestoredPayload failed", e); + return; + } + var displayManagerInternal = mInjector.getDisplayManagerInternal(); + if (displayManagerInternal == null) { + Slog.e(TAG, "DisplayManagerInternal is null"); + return; + } + + displayManagerInternal.reloadTopologies(mUserId); + } + + @VisibleForTesting + static class Injector { + private final boolean mIsDisplayTopologyEnabled = + new DisplayManagerFlags().isDisplayTopologyEnabled(); + + boolean isDisplayTopologyFlagEnabled() { + return mIsDisplayTopologyEnabled; + } + + @Nullable + DisplayManagerInternal getDisplayManagerInternal() { + return LocalServices.getService(DisplayManagerInternal.class); + } + + byte[] readTopologyFile(int userId) throws IOException { + return getTopologyFile(userId).readFully(); + } + + AtomicFileOutputStream writeTopologyFile(int userId) throws IOException { + return new AtomicFileOutputStream(getTopologyFile(userId)); + } + + private AtomicFile getTopologyFile(int userId) { + return new AtomicFile(DisplayTopologyXmlStore.getUserTopologyFile(userId), + /*commitTag=*/ "topology-state"); + } + }; +} 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/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/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 57ccab027c29..8cbccf5feead 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -2922,7 +2922,16 @@ public class UserManagerService extends IUserManager.Stub { * switchable. */ public @UserManager.UserSwitchabilityResult int getUserSwitchability(int userId) { - checkManageOrInteractPermissionIfCallerInOtherProfileGroup(userId, "getUserSwitchability"); + if (Flags.getUserSwitchabilityPermission()) { + if (!hasManageUsersOrPermission(android.Manifest.permission.INTERACT_ACROSS_USERS)) { + throw new SecurityException( + "You need MANAGE_USERS or INTERACT_ACROSS_USERS permission to " + + "getUserSwitchability"); + } + } else { + checkManageOrInteractPermissionIfCallerInOtherProfileGroup(userId, + "getUserSwitchability"); + } final TimingsTraceAndSlog t = new TimingsTraceAndSlog(); t.traceBegin("getUserSwitchability-" + userId); @@ -3581,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 @@ -6266,9 +6273,6 @@ public class UserManagerService extends IUserManager.Stub { } } - /** - * @hide - */ @Override public @NonNull UserInfo createRestrictedProfileWithThrow( @Nullable String name, @UserIdInt int parentUserId) @@ -8495,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/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/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/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/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..4f9859d33d74 100644 --- a/services/supervision/java/com/android/server/supervision/SupervisionService.java +++ b/services/supervision/java/com/android/server/supervision/SupervisionService.java @@ -51,7 +51,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,17 +61,17 @@ 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 Context mContext; private final DevicePolicyManagerInternal mDpmInternal; private final PackageManager mPackageManager; private final UserManagerInternal mUserManagerInternal; + final SupervisionManagerInternal mInternal = new SupervisionManagerInternalImpl(); public SupervisionService(Context context) { mContext = context.createAttributionContext(LOG_TAG); @@ -82,6 +81,12 @@ public class SupervisionService extends ISupervisionManager.Stub { mUserManagerInternal.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 +97,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, @@ -140,35 +159,53 @@ 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 ComponentName po = + mDpmInternal != null ? mDpmInternal.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. */ @@ -228,19 +265,21 @@ public class SupervisionService extends ISupervisionManager.Stub { } } - final SupervisionManagerInternal mInternal = new SupervisionManagerInternalImpl(); - 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 = mPackageManager.getPackagesForUid(uid); + if (packages != null) { + for (var packageName : packages) { + if (supervisionAppPackage.equals(packageName)) { + return true; + } } } return false; @@ -253,7 +292,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 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/DisplayBackupHelperTest.kt b/services/tests/displayservicetests/src/com/android/server/display/DisplayBackupHelperTest.kt new file mode 100644 index 000000000000..26060a406aa0 --- /dev/null +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayBackupHelperTest.kt @@ -0,0 +1,86 @@ +/* + * Copyright 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.server.display + +import androidx.test.filters.SmallTest +import android.hardware.display.DisplayManagerInternal +import android.util.AtomicFileOutputStream +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.mockito.kotlin.verify +import org.mockito.kotlin.never + +@SmallTest +class DisplayBackupHelperTest { + private val mockInjector = mock<DisplayBackupHelper.Injector>() + private val mockDmsInternal = mock<DisplayManagerInternal>() + private val mockWriteTopologyFile = mock<AtomicFileOutputStream>() + private val byteArray = byteArrayOf(0b00000001, 0b00000010, 0b00000011) + private val helper = createBackupHelper(0, byteArray) + + @Test + fun testBackupDisplayReturnsBytes() { + assertThat(helper.getBackupPayload("display")).isEqualTo(byteArray) + } + + @Test + fun testBackupSomethingReturnsNull() { + assertThat(helper.getBackupPayload("something")).isNull() + } + + @Test + fun testBackupDisplayReturnsNullWhenFlagDisabled() { + whenever(mockInjector.isDisplayTopologyFlagEnabled()).thenReturn(false) + assertThat(helper.getBackupPayload("display")).isNull() + } + + @Test + fun testRestoreDisplay() { + helper.applyRestoredPayload("display", byteArray) + verify(mockWriteTopologyFile).write(byteArray) + verify(mockWriteTopologyFile).markSuccess() + verify(mockDmsInternal).reloadTopologies(0) + } + + @Test + fun testRestoreSomethingDoesNothing() { + helper.applyRestoredPayload("something", byteArray) + verify(mockWriteTopologyFile, never()).write(byteArray) + verify(mockWriteTopologyFile, never()).markSuccess() + verify(mockDmsInternal, never()).reloadTopologies(0) + } + + @Test + fun testRestoreDisplayDoesNothingWhenFlagDisabled() { + whenever(mockInjector.isDisplayTopologyFlagEnabled()).thenReturn(false) + helper.applyRestoredPayload("display", byteArray) + verify(mockWriteTopologyFile, never()).write(byteArray) + verify(mockWriteTopologyFile, never()).markSuccess() + verify(mockDmsInternal, never()).reloadTopologies(0) + } + + fun createBackupHelper(userId: Int, topologyToBackup: ByteArray): DisplayBackupHelper { + whenever(mockInjector.getDisplayManagerInternal()).thenReturn(mockDmsInternal) + whenever(mockInjector.readTopologyFile(userId)).thenReturn(topologyToBackup) + whenever(mockInjector.writeTopologyFile(userId)).thenReturn(mockWriteTopologyFile) + whenever(mockInjector.isDisplayTopologyFlagEnabled()).thenReturn(true) + + return DisplayBackupHelper(userId, mockInjector) + } +}
\ No newline at end of file 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..89b48bad2358 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java @@ -16,8 +16,6 @@ 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.Process.INVALID_UID; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; @@ -29,11 +27,9 @@ import static com.android.server.am.PendingIntentRecord.CANCEL_REASON_OWNER_CANC import static com.android.server.am.PendingIntentRecord.CANCEL_REASON_OWNER_FORCE_STOPPED; 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 org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; @@ -43,11 +39,9 @@ import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.AppGlobals; -import android.app.BackgroundStartPrivileges; import android.app.PendingIntent; import android.content.Intent; import android.content.pm.IPackageManager; -import android.os.Binder; import android.os.Looper; import android.os.UserHandle; @@ -185,34 +179,6 @@ public class PendingIntentControllerTest { } } - @Test - public void testClearAllowBgActivityStartsClearsToken() { - final PendingIntentRecord pir = createPendingIntentRecord(0); - Binder token = new Binder(); - pir.setAllowBgActivityStarts(token, FLAG_ACTIVITY_SENDER); - assertEquals(BackgroundStartPrivileges.allowBackgroundActivityStarts(token), - pir.getBackgroundStartPrivilegesForActivitySender(token)); - pir.clearAllowBgActivityStarts(token); - assertEquals(BackgroundStartPrivileges.NONE, - pir.getBackgroundStartPrivilegesForActivitySender(token)); - } - - @Test - public void testClearAllowBgActivityStartsClearsDuration() { - final PendingIntentRecord pir = createPendingIntentRecord(0); - Binder token = new Binder(); - pir.setAllowlistDurationLocked(token, 1000, - TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED, REASON_NOTIFICATION_SERVICE, - "NotificationManagerService"); - PendingIntentRecord.TempAllowListDuration allowlistDurationLocked = - pir.getAllowlistDurationLocked(token); - assertEquals(1000, allowlistDurationLocked.duration); - pir.clearAllowBgActivityStarts(token); - PendingIntentRecord.TempAllowListDuration allowlistDurationLockedAfterClear = - pir.getAllowlistDurationLocked(token); - assertNull(allowlistDurationLockedAfterClear); - } - private void assertCancelReason(int expectedReason, int actualReason) { final String errMsg = "Expected: " + cancelReasonToString(expectedReason) + "; Actual: " + cancelReasonToString(actualReason); diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java index 7e179095d99b..86bf203771ba 100644 --- a/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java @@ -93,7 +93,8 @@ public class SystemBackupAgentTest { "app_locales", "app_gender", "companion", - "system_gender"); + "system_gender", + "display"); } @Test @@ -118,7 +119,8 @@ public class SystemBackupAgentTest { "app_locales", "app_gender", "companion", - "system_gender"); + "system_gender", + "display"); } @Test @@ -136,7 +138,8 @@ public class SystemBackupAgentTest { "app_locales", "companion", "app_gender", - "system_gender"); + "system_gender", + "display"); } @Test @@ -158,7 +161,8 @@ public class SystemBackupAgentTest { "shortcut_manager", "companion", "app_gender", - "system_gender"); + "system_gender", + "display"); } @Test 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/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/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/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeUserActionsViewModelKosmos.kt b/services/tests/uiservicestests/src/android/app/ExampleActivity.java index 6345c4076412..58395e4d75e1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeUserActionsViewModelKosmos.kt +++ b/services/tests/uiservicestests/src/android/app/ExampleActivity.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. @@ -14,11 +14,7 @@ * limitations under the License. */ -package com.android.systemui.shade.ui.viewmodel +package android.app; -import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.Kosmos.Fixture -import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeUserActionsViewModel - -val Kosmos.notificationsShadeUserActionsViewModel: - NotificationsShadeUserActionsViewModel by Fixture { NotificationsShadeUserActionsViewModel() } +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/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/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index d3f98d1a1d70..0b3d720bf52a 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -3219,7 +3219,6 @@ public class CarrierConfigManager { * The roaming indicator will be shown if this is {@code true} and will not be shown if this is * {@code false}. */ - @FlaggedApi(Flags.FLAG_HIDE_ROAMING_ICON) public static final String KEY_SHOW_ROAMING_INDICATOR_BOOL = "show_roaming_indicator_bool"; /** 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..3ee6dc48bfa3 100644 --- a/tests/testables/src/android/testing/TestableLooper.java +++ b/tests/testables/src/android/testing/TestableLooper.java @@ -42,6 +42,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 { 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..83d22d923c78 100644 --- a/tests/utils/testutils/java/android/os/test/TestLooper.java +++ b/tests/utils/testutils/java/android/os/test/TestLooper.java @@ -33,9 +33,15 @@ import java.lang.reflect.Method; 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; 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}") |