diff options
265 files changed, 7636 insertions, 1890 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index d582cb79fba1..c6ce799f0a24 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -65,6 +65,7 @@ aconfig_declarations_group { "android.server.app.flags-aconfig-java", "android.service.autofill.flags-aconfig-java", "android.service.chooser.flags-aconfig-java", + "android.service.compat.flags-aconfig-java", "android.service.controls.flags-aconfig-java", "android.service.dreams.flags-aconfig-java", "android.service.notification.flags-aconfig-java", @@ -863,6 +864,21 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } +aconfig_declarations { + name: "android.service.compat.flags-aconfig", + package: "com.android.server.compat", + container: "system", + srcs: [ + "services/core/java/com/android/server/compat/*.aconfig", + ], +} + +java_aconfig_library { + name: "android.service.compat.flags-aconfig-java", + aconfig_declarations: "android.service.compat.flags-aconfig", + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} + // Multi user aconfig_declarations { name: "android.multiuser.flags-aconfig", diff --git a/apex/jobscheduler/service/aconfig/job.aconfig b/apex/jobscheduler/service/aconfig/job.aconfig index e5389b4f96fb..11c5b51e23ae 100644 --- a/apex/jobscheduler/service/aconfig/job.aconfig +++ b/apex/jobscheduler/service/aconfig/job.aconfig @@ -75,3 +75,10 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "enforce_quota_policy_to_fgs_jobs" + namespace: "backstage_power" + description: "Applies the normal quota policy to FGS jobs" + bug: "341201311" +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java index a1c72fb4c06c..03a3a0d51891 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java @@ -99,10 +99,10 @@ import java.util.function.Predicate; * the number of jobs or sessions that can run within the window. Regardless of bucket, apps will * not be allowed to run more than 20 jobs within the past 10 minutes. * - * Jobs are throttled while an app is not in a foreground state. All jobs are allowed to run - * freely when an app enters the foreground state and are restricted when the app leaves the - * foreground state. However, jobs that are started while the app is in the TOP state do not count - * towards any quota and are not restricted regardless of the app's state change. + * Jobs are throttled while an app is not in a TOP or BOUND_TOP state. All jobs are allowed to run + * freely when an app enters the TOP or BOUND_TOP state and are restricted when the app leaves those + * states. However, jobs that are started while the app is in the TOP state do not count towards any + * quota and are not restricted regardless of the app's state change. * * Jobs will not be throttled when the device is charging. The device is considered to be charging * once the {@link BatteryManager#ACTION_CHARGING} intent has been broadcast. @@ -567,6 +567,11 @@ public final class QuotaController extends StateController { ActivityManager.getService().registerUidObserver(new QcUidObserver(), ActivityManager.UID_OBSERVER_PROCSTATE, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, null); + if (Flags.enforceQuotaPolicyToFgsJobs()) { + ActivityManager.getService().registerUidObserver(new QcUidObserver(), + ActivityManager.UID_OBSERVER_PROCSTATE, + ActivityManager.PROCESS_STATE_BOUND_TOP, null); + } ActivityManager.getService().registerUidObserver(new QcUidObserver(), ActivityManager.UID_OBSERVER_PROCSTATE, ActivityManager.PROCESS_STATE_TOP, null); @@ -2706,6 +2711,12 @@ public final class QuotaController extends StateController { } } + @VisibleForTesting + int getProcessStateQuotaFreeThreshold() { + return Flags.enforceQuotaPolicyToFgsJobs() ? ActivityManager.PROCESS_STATE_BOUND_TOP : + ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE; + } + private class QcHandler extends Handler { QcHandler(Looper looper) { @@ -2832,15 +2843,15 @@ public final class QuotaController extends StateController { mTopAppCache.put(uid, true); mTopAppGraceCache.delete(uid); if (mForegroundUids.get(uid)) { - // Went from FGS to TOP. We don't need to reprocess timers or - // jobs. + // Went from a process state with quota free to TOP. We don't + // need to reprocess timers or jobs. break; } mForegroundUids.put(uid, true); isQuotaFree = true; } else { final boolean reprocess; - if (procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) { + if (procState <= getProcessStateQuotaFreeThreshold()) { reprocess = !mForegroundUids.get(uid); mForegroundUids.put(uid, true); isQuotaFree = true; diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index 96700a9a3c18..70211bfd77b1 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -1135,8 +1135,11 @@ public final class VirtualDeviceManager { /** * Sets the visibility of the pointer icon for this VirtualDevice's associated displays. * + * <p>Only applicable to trusted displays.</p> + * * @param showPointerIcon True if the pointer should be shown; false otherwise. The default * visibility is true. + * @see DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED */ @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void setShowPointerIcon(boolean showPointerIcon) { diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java index 03b72bdb8823..65f9cbefb052 100644 --- a/core/java/android/companion/virtual/VirtualDeviceParams.java +++ b/core/java/android/companion/virtual/VirtualDeviceParams.java @@ -159,7 +159,7 @@ public final class VirtualDeviceParams implements Parcelable { * @hide */ @IntDef(prefix = "POLICY_TYPE_", value = {POLICY_TYPE_SENSORS, POLICY_TYPE_AUDIO, - POLICY_TYPE_RECENTS, POLICY_TYPE_ACTIVITY, POLICY_TYPE_CAMERA, + POLICY_TYPE_RECENTS, POLICY_TYPE_ACTIVITY, POLICY_TYPE_CLIPBOARD, POLICY_TYPE_CAMERA, POLICY_TYPE_BLOCKED_ACTIVITY}) @Retention(RetentionPolicy.SOURCE) @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) @@ -220,11 +220,16 @@ public final class VirtualDeviceParams implements Parcelable { * Tells the activity manager how to handle recents entries for activities run on this device. * * <ul> - * <li>{@link #DEVICE_POLICY_DEFAULT}: Activities launched on VirtualDisplays owned by this + * <li>{@link #DEVICE_POLICY_DEFAULT}: Activities launched on trusted displays owned by this * device will appear in the host device recents. - * <li>{@link #DEVICE_POLICY_CUSTOM}: Activities launched on VirtualDisplays owned by this + * <li>{@link #DEVICE_POLICY_CUSTOM}: Activities launched on trusted displays owned by this * device will not appear in recents. * </ul> + * + * <p>Activities launched on untrusted displays will always show in the host device recents, + * regardless of the policy.</p> + * + * @see android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED */ public static final int POLICY_TYPE_RECENTS = 2; @@ -254,8 +259,10 @@ public final class VirtualDeviceParams implements Parcelable { * not shared with other devices' clipboards, including the clipboard of the default device. * <li>{@link #DEVICE_POLICY_CUSTOM}: The device's clipboard is shared with the default * device's clipboard. Any clipboard operation on the virtual device is as if it was done on - * the default device. + * the default device. Requires all displays of the virtual device to be trusted. * </ul> + * + * @see android.hardware.display.DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED */ @FlaggedApi(Flags.FLAG_CROSS_DEVICE_CLIPBOARD) public static final int POLICY_TYPE_CLIPBOARD = 4; @@ -821,8 +828,8 @@ public final class VirtualDeviceParams implements Parcelable { } /** - * Specifies a component to be used as input method on all displays owned by this virtual - * device. + * Specifies a component to be used as input method on all trusted displays owned by this + * virtual device. * * @param inputMethodComponent The component name to be used as input method. Must comply to * all general input method requirements described in the guide to @@ -831,6 +838,7 @@ public final class VirtualDeviceParams implements Parcelable { * may interact with the virtual device, then there will effectively be no IME on this * device's displays for that user. * + * @see android.hardware.display.DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED * @see android.inputmethodservice.InputMethodService * @attr ref android.R.styleable#InputMethod_isVirtualDeviceOnly * @attr ref android.R.styleable#InputMethod_showInInputMethodPicker diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index dcf82bf35828..ff0a3ddc746e 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -47,13 +47,6 @@ flag { } flag { - name: "start_user_before_scheduled_alarms" - namespace: "multiuser" - description: "Persist list of users with alarms scheduled and wakeup stopped users before alarms are due" - bug: "314907186" -} - -flag { name: "add_ui_for_sounds_from_background_users" namespace: "multiuser" description: "Allow foreground user to dismiss sounds that are coming from background users" diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java index 6c1aa90c831b..75ffcc3a8863 100644 --- a/core/java/android/hardware/display/DisplayManagerInternal.java +++ b/core/java/android/hardware/display/DisplayManagerInternal.java @@ -461,6 +461,16 @@ public abstract class DisplayManagerInternal { public abstract void stylusGestureStarted(long eventTime); /** + * Called by {@link com.android.server.wm.ContentRecorder} to verify whether + * the display is allowed to mirror primary display's content. + * @param displayId the id of the display where we mirror to. + * @return true if the mirroring dialog is confirmed (display is enabled), or + * {@link com.android.server.display.ExternalDisplayPolicy#ENABLE_ON_CONNECT} + * system property is enabled. + */ + public abstract boolean isDisplayReadyForMirroring(int displayId); + + /** * Describes the requested power state of the display. * * This object is intended to describe the general characteristics of the diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java index 177ee6f1540a..897ce4a7b022 100644 --- a/core/java/android/hardware/input/InputSettings.java +++ b/core/java/android/hardware/input/InputSettings.java @@ -24,6 +24,8 @@ import static com.android.hardware.input.Flags.keyboardA11yBounceKeysFlag; import static com.android.hardware.input.Flags.keyboardA11ySlowKeysFlag; import static com.android.hardware.input.Flags.keyboardA11yStickyKeysFlag; import static com.android.hardware.input.Flags.keyboardA11yMouseKeys; +import static com.android.hardware.input.Flags.mouseReverseVerticalScrolling; +import static com.android.hardware.input.Flags.mouseSwapPrimaryButton; import static com.android.hardware.input.Flags.touchpadTapDragging; import static com.android.hardware.input.Flags.touchpadVisualizer; import static com.android.input.flags.Flags.enableInputFilterRustImpl; @@ -363,6 +365,22 @@ public class InputSettings { } /** + * Returns true if the feature flag for mouse reverse vertical scrolling is enabled. + * @hide + */ + public static boolean isMouseReverseVerticalScrollingFeatureFlagEnabled() { + return mouseReverseVerticalScrolling(); + } + + /** + * Returns true if the feature flag for mouse swap primary button is enabled. + * @hide + */ + public static boolean isMouseSwapPrimaryButtonFeatureFlagEnabled() { + return mouseSwapPrimaryButton(); + } + + /** * Returns true if the touchpad visualizer is allowed to appear. * * @param context The application context. @@ -501,6 +519,86 @@ public class InputSettings { } /** + * Whether mouse vertical scrolling is enabled, this applies only to connected mice. + * + * @param context The application context. + * @return Whether the mouse will have its vertical scrolling reversed + * (scroll down to move up). + * + * @hide + */ + public static boolean isMouseReverseVerticalScrollingEnabled(@NonNull Context context) { + if (!isMouseReverseVerticalScrollingFeatureFlagEnabled()) { + return false; + } + + return Settings.System.getIntForUser(context.getContentResolver(), + Settings.System.MOUSE_REVERSE_VERTICAL_SCROLLING, 0, UserHandle.USER_CURRENT) + != 0; + } + + /** + * Sets whether the connected mouse will have its vertical scrolling reversed. + * + * @param context The application context. + * @param reverseScrolling Whether reverse scrolling is enabled. + * + * @hide + */ + @RequiresPermission(Manifest.permission.WRITE_SETTINGS) + public static void setMouseReverseVerticalScrolling(@NonNull Context context, + boolean reverseScrolling) { + if (!isMouseReverseVerticalScrollingFeatureFlagEnabled()) { + return; + } + + Settings.System.putIntForUser(context.getContentResolver(), + Settings.System.MOUSE_REVERSE_VERTICAL_SCROLLING, reverseScrolling ? 1 : 0, + UserHandle.USER_CURRENT); + } + + /** + * Whether the primary mouse button is swapped on connected mice. + * + * @param context The application context. + * @return Whether mice will have their primary buttons swapped, so that left clicking will + * perform the secondary action (e.g. show menu) and right clicking will perform the primary + * action. + * + * @hide + */ + public static boolean isMouseSwapPrimaryButtonEnabled(@NonNull Context context) { + if (!isMouseSwapPrimaryButtonFeatureFlagEnabled()) { + return false; + } + + return Settings.System.getIntForUser(context.getContentResolver(), + Settings.System.MOUSE_SWAP_PRIMARY_BUTTON, 0, UserHandle.USER_CURRENT) + != 0; + } + + /** + * Sets whether mice will have their primary buttons swapped between left and right + * clicks. + * + * @param context The application context. + * @param swapPrimaryButton Whether swapping the primary button is enabled. + * + * @hide + */ + @RequiresPermission(Manifest.permission.WRITE_SETTINGS) + public static void setMouseSwapPrimaryButton(@NonNull Context context, + boolean swapPrimaryButton) { + if (!isMouseSwapPrimaryButtonFeatureFlagEnabled()) { + return; + } + + Settings.System.putIntForUser(context.getContentResolver(), + Settings.System.MOUSE_SWAP_PRIMARY_BUTTON, swapPrimaryButton ? 1 : 0, + UserHandle.USER_CURRENT); + } + + /** * Whether Accessibility bounce keys feature is enabled. * * <p> diff --git a/core/java/android/hardware/input/VirtualInputDeviceConfig.java b/core/java/android/hardware/input/VirtualInputDeviceConfig.java index e8ef8cd11585..3b74d7f5253d 100644 --- a/core/java/android/hardware/input/VirtualInputDeviceConfig.java +++ b/core/java/android/hardware/input/VirtualInputDeviceConfig.java @@ -163,7 +163,6 @@ public abstract class VirtualInputDeviceConfig { return self(); } - /** * Sets the product id of the device, uniquely identifying the device within the address * space of a given vendor, identified by the device's vendor id. @@ -179,6 +178,10 @@ public abstract class VirtualInputDeviceConfig { * * <p>The input device is restricted to the display with the given ID and may not send * events to any other display.</p> + * <p>The corresponding display must be trusted or mirror display.</p> + * + * @see android.hardware.display.DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED + * @see android.hardware.display.DisplayManager#VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR */ @NonNull public T setAssociatedDisplayId(int displayId) { diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 3ae951170759..f1964e7bc024 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -6368,8 +6368,12 @@ public class UserManager { Settings.Global.DEVICE_DEMO_MODE, 0) > 0; } - /** @hide */ - public static final void invalidateUserSerialNumberCache() { + + /** + * This method is used to invalidate caches, when user was added or removed. + * @hide + */ + public static final void invalidateCacheOnUserListChange() { UserManagerCache.invalidateUserSerialNumber(); } @@ -6382,7 +6386,7 @@ public class UserManager { * @hide */ @UnsupportedAppUsage - @CachedProperty(modsFlagOnOrNone = {}) + @CachedProperty(modsFlagOnOrNone = {}, api = "user_manager_users") public int getUserSerialNumber(@UserIdInt int userId) { // Read only flag should is to fix early access to this API // cacheUserSerialNumber to be removed after the diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig index a1bfe39c0fc4..81987907452f 100644 --- a/core/java/android/os/flags.aconfig +++ b/core/java/android/os/flags.aconfig @@ -232,3 +232,19 @@ flag { bug: "361329788" is_exported: true } + +flag { + name: "enable_angle_allow_list" + namespace: "gpu" + description: "Whether to read from angle allowlist to determine if app should use ANGLE" + is_fixed_read_only: true + bug: "370845648" +} + +flag { + name: "api_for_backported_fixes" + namespace: "media_reliability" + description: "Public API app developers use to check if a known issue is fixed on a device." + bug: "308461809" + is_exported: true +} diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index 271970b98542..1d8fcec8cf31 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -261,7 +261,7 @@ flag { is_fixed_read_only: true namespace: "permissions" description: "If proc state is decreasing over the restriction threshold and capability is changed, delay if no new capabilities are added" - bug: "308573169" + bug: "347891382" metadata { purpose: PURPOSE_BUGFIX } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 1a15d09c0a50..594005c3ebd6 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -2351,6 +2351,11 @@ public final class Settings { /** * Activity Action: Show the permission screen for allowing apps to post promoted notifications. + * Properly formatted priority notifications are elevated in appearance. For example they may be + * able to use colors, have richer progress bars, show as chips in the status bar, and/or + * permanently appear on always-on-displays. This functionality is intended to be reserved for + * user initiated ongoing activities like navigation, phone calls, and ride sharing. + * * <p> * Input: {@link #EXTRA_APP_PACKAGE}, the package to display. * <p> @@ -6205,6 +6210,25 @@ public final class Settings { public static final String TOUCHPAD_RIGHT_CLICK_ZONE = "touchpad_right_click_zone"; /** + * Whether to enable reversed vertical scrolling for connected mice. + * + * When enabled, scrolling down on the mouse wheel will move the screen up and vice versa. + * @hide + */ + public static final String MOUSE_REVERSE_VERTICAL_SCROLLING = + "mouse_reverse_vertical_scrolling"; + + /** + * Whether to enable swapping the primary button for connected mice. + * + * When enabled, right clicking will be the primary button and left clicking will be the + * secondary button (e.g. show menu). + * @hide + */ + public static final String MOUSE_SWAP_PRIMARY_BUTTON = + "mouse_swap_primary_button"; + + /** * Pointer fill style, specified by * {@link android.view.PointerIcon.PointerIconVectorStyleFill} constants. * @@ -6442,6 +6466,8 @@ public final class Settings { PRIVATE_SETTINGS.add(SCREEN_FLASH_NOTIFICATION); PRIVATE_SETTINGS.add(SCREEN_FLASH_NOTIFICATION_COLOR); PRIVATE_SETTINGS.add(DEFAULT_DEVICE_FONT_SCALE); + PRIVATE_SETTINGS.add(MOUSE_REVERSE_VERTICAL_SCROLLING); + PRIVATE_SETTINGS.add(MOUSE_SWAP_PRIMARY_BUTTON); } /** diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java index 6fb82af2b292..8e35843e2193 100644 --- a/core/java/android/window/DesktopModeFlags.java +++ b/core/java/android/window/DesktopModeFlags.java @@ -65,7 +65,9 @@ public enum DesktopModeFlags { ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS( Flags::enableDesktopWindowingTaskbarRunningApps, true), ENABLE_DESKTOP_WINDOWING_TRANSITIONS(Flags::enableDesktopWindowingTransitions, false), - ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS(Flags::enableDesktopWindowingExitTransitions, false); + ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS(Flags::enableDesktopWindowingExitTransitions, false), + ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS( + Flags::enableWindowingTransitionHandlersObservers, false); private static final String TAG = "DesktopModeFlagsUtil"; // Function called to obtain aconfig flag value. diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 31bb3a610376..155494fb3b25 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -228,7 +228,7 @@ flag { name: "enable_desktop_windowing_app_handle_education" namespace: "lse_desktop_experience" description: "Enables desktop windowing app handle education" - bug: "348208342" + bug: "316006079" } flag { diff --git a/core/proto/android/providers/settings/system.proto b/core/proto/android/providers/settings/system.proto index e795e8096641..9779dc0e00b8 100644 --- a/core/proto/android/providers/settings/system.proto +++ b/core/proto/android/providers/settings/system.proto @@ -220,6 +220,15 @@ message SystemSettingsProto { } optional Touchpad touchpad = 36; + message Mouse { + option (android.msg_privacy).dest = DEST_EXPLICIT; + + optional SettingProto reverse_vertical_scrolling = 1 [ (android.privacy).dest = DEST_AUTOMATIC ]; + optional SettingProto swap_primary_button = 2 [ (android.privacy).dest = DEST_AUTOMATIC ]; + } + + optional Mouse mouse = 38; + optional SettingProto tty_mode = 31 [ (android.privacy).dest = DEST_AUTOMATIC ]; message Vibrate { @@ -277,5 +286,5 @@ message SystemSettingsProto { // Please insert fields in alphabetical order and group them into messages // if possible (to avoid reaching the method limit). - // Next tag = 38; + // Next tag = 39; } diff --git a/core/tests/coretests/src/android/app/ActivityManagerTest.java b/core/tests/coretests/src/android/app/ActivityManagerTest.java index d850f86070bc..85ff8463725e 100644 --- a/core/tests/coretests/src/android/app/ActivityManagerTest.java +++ b/core/tests/coretests/src/android/app/ActivityManagerTest.java @@ -60,7 +60,6 @@ public class ActivityManagerTest { public void testProcState() throws Exception { // For the moment mostly want to confirm we don't crash assertNotNull(ActivityManager.procStateToString(PROCESS_STATE_SERVICE)); - assertNotNull(ActivityManager.processStateAmToProto(PROCESS_STATE_SERVICE)); assertTrue(ActivityManager.isProcStateBackground(PROCESS_STATE_SERVICE)); assertFalse(ActivityManager.isProcStateCached(PROCESS_STATE_SERVICE)); assertFalse(ActivityManager.isForegroundService(PROCESS_STATE_SERVICE)); diff --git a/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java b/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java index b972882e68e6..cd524214e6af 100644 --- a/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java +++ b/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java @@ -111,12 +111,6 @@ public class ActivityManagerTest extends AndroidTestCase { assertEquals(config.reqKeyboardType, vconfig.keyboard); assertEquals(config.reqTouchScreen, vconfig.touchscreen); assertEquals(config.reqNavigation, vconfig.navigation); - if (vconfig.navigation == Configuration.NAVIGATION_NONAV) { - assertNotNull(config.reqInputFeatures & ConfigurationInfo.INPUT_FEATURE_FIVE_WAY_NAV); - } - if (vconfig.keyboard != Configuration.KEYBOARD_UNDEFINED) { - assertNotNull(config.reqInputFeatures & ConfigurationInfo.INPUT_FEATURE_HARD_KEYBOARD); - } } @SmallTest 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 5a277316ffd4..379e052e7b38 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 @@ -156,6 +156,21 @@ class DesktopModeEventLogger { ) } + fun logTaskInfoStateInit() { + logTaskUpdate( + FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INIT_STATSD, + /* session_id */ 0, + TaskUpdate( + visibleTaskCount = 0, + instanceId = 0, + uid = 0, + taskHeight = 0, + taskWidth = 0, + taskX = 0, + taskY = 0) + ) + } + private fun logTaskUpdate(taskEvent: Int, sessionId: Int, taskUpdate: TaskUpdate) { FrameworkStatsLog.write( DESKTOP_MODE_TASK_UPDATE_ATOM_ID, 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 b8507e3b2764..f847aa8918c2 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 @@ -102,6 +102,7 @@ class DesktopModeLoggerTransitionObserver( SystemProperties.set( VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY, VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY_DEFAULT_VALUE) + desktopModeEventLogger.logTaskInfoStateInit() } override fun onTransitionReady( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt index d7a132dfa1be..dde9fda13ea9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.desktopmode -import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.SetFlagsRule import com.android.dx.mockito.inline.extended.ExtendedMockito.verify @@ -397,6 +396,37 @@ class DesktopModeEventLoggerTest : ShellTestCase() { } } + @Test + fun logTaskInfoStateInit_logsTaskInfoChangedStateInit() { + desktopModeEventLogger.logTaskInfoStateInit() + verify { + FrameworkStatsLog.write(eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), + /* task_event */ + eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INIT_STATSD), + /* instance_id */ + eq(0), + /* uid */ + eq(0), + /* task_height */ + eq(0), + /* task_width */ + eq(0), + /* task_x */ + eq(0), + /* task_y */ + eq(0), + /* session_id */ + eq(0), + /* minimize_reason */ + eq(UNSET_MINIMIZE_REASON), + /* unminimize_reason */ + eq(UNSET_UNMINIMIZE_REASON), + /* visible_task_count */ + eq(0) + ) + } + } + private companion object { private const val SESSION_ID = 1 private const val TASK_ID = 1 diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt index daf7e7d5397b..e7593b5b9324 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt @@ -115,6 +115,9 @@ class DesktopModeLoggerTransitionObserverTest : ShellTestCase() { val initRunnableCaptor = ArgumentCaptor.forClass(Runnable::class.java) verify(mockShellInit).addInitCallback(initRunnableCaptor.capture(), same(transitionObserver)) initRunnableCaptor.value.run() + // verify this initialisation interaction to leave the desktopmodeEventLogger mock in a + // consistent state with no outstanding interactions when test cases start executing. + verify(desktopModeEventLogger).logTaskInfoStateInit() } @Test diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt index 4428adee818d..24e14e69637b 100644 --- a/nfc/api/system-current.txt +++ b/nfc/api/system-current.txt @@ -63,7 +63,7 @@ package android.nfc { method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean isAutoChangeEnabled(); method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean isTagPresent(); method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void maybeTriggerFirmwareUpdate(); - method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void overwriteRoutingTable(int, int, int); + method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void overwriteRoutingTable(int, int, int, int); method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void pausePolling(int); method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void registerCallback(@NonNull java.util.concurrent.Executor, @NonNull android.nfc.NfcOemExtension.Callback); method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void resumePolling(); diff --git a/nfc/java/android/nfc/INfcCardEmulation.aidl b/nfc/java/android/nfc/INfcCardEmulation.aidl index 1eae3c6f30f1..8535e4a9cfd2 100644 --- a/nfc/java/android/nfc/INfcCardEmulation.aidl +++ b/nfc/java/android/nfc/INfcCardEmulation.aidl @@ -54,5 +54,5 @@ interface INfcCardEmulation void setAutoChangeStatus(boolean state); boolean isAutoChangeEnabled(); List<String> getRoutingStatus(); - void overwriteRoutingTable(int userHandle, String emptyAid, String protocol, String tech); + void overwriteRoutingTable(int userHandle, String emptyAid, String protocol, String tech, String sc); } diff --git a/nfc/java/android/nfc/NfcOemExtension.java b/nfc/java/android/nfc/NfcOemExtension.java index fb63b5c03d00..bc410c7b8ba5 100644 --- a/nfc/java/android/nfc/NfcOemExtension.java +++ b/nfc/java/android/nfc/NfcOemExtension.java @@ -647,24 +647,29 @@ public final class NfcOemExtension { * {@link ProtocolAndTechnologyRoute} * @param emptyAid Zero-length AID route destination, where the possible inputs are defined in * {@link ProtocolAndTechnologyRoute} + * @param systemCode System Code route destination, where the possible inputs are defined in + * {@link ProtocolAndTechnologyRoute} */ @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS) @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION) public void overwriteRoutingTable( @CardEmulation.ProtocolAndTechnologyRoute int protocol, @CardEmulation.ProtocolAndTechnologyRoute int technology, - @CardEmulation.ProtocolAndTechnologyRoute int emptyAid) { + @CardEmulation.ProtocolAndTechnologyRoute int emptyAid, + @CardEmulation.ProtocolAndTechnologyRoute int systemCode) { String protocolRoute = routeIntToString(protocol); String technologyRoute = routeIntToString(technology); String emptyAidRoute = routeIntToString(emptyAid); + String systemCodeRoute = routeIntToString(systemCode); NfcAdapter.callService(() -> NfcAdapter.sCardEmulationService.overwriteRoutingTable( mContext.getUser().getIdentifier(), emptyAidRoute, protocolRoute, - technologyRoute + technologyRoute, + systemCodeRoute )); } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastCallbackExt.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastCallbackExt.kt index 0bcf7fed5c80..07abb6b912b6 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastCallbackExt.kt +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastCallbackExt.kt @@ -68,3 +68,44 @@ val LocalBluetoothLeBroadcast.onBroadcastStartedOrStopped: Flow<Unit> awaitClose { unregisterServiceCallBack(listener) } } .buffer(capacity = Channel.CONFLATED) + +/** [Flow] for [BluetoothLeBroadcast.Callback] onPlaybackStarted event */ +val LocalBluetoothLeBroadcast.onPlaybackStarted: Flow<Unit> + get() = + callbackFlow { + val listener = + object : BluetoothLeBroadcast.Callback { + override fun onBroadcastStarted(reason: Int, broadcastId: Int) {} + + override fun onBroadcastStartFailed(reason: Int) { + } + + override fun onBroadcastStopped(reason: Int, broadcastId: Int) { + } + + override fun onBroadcastStopFailed(reason: Int) { + } + + override fun onPlaybackStarted(reason: Int, broadcastId: Int) { + launch { trySend(Unit) } + } + + override fun onPlaybackStopped(reason: Int, broadcastId: Int) { + } + + override fun onBroadcastUpdated(reason: Int, broadcastId: Int) {} + + override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) {} + + override fun onBroadcastMetadataChanged( + broadcastId: Int, + metadata: BluetoothLeBroadcastMetadata + ) {} + } + registerServiceCallBack( + ConcurrentUtils.DIRECT_EXECUTOR, + listener, + ) + awaitClose { unregisterServiceCallBack(listener) } + } + .buffer(capacity = Channel.CONFLATED) diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt index 2f8105ae461d..b41e9703d427 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt @@ -74,6 +74,8 @@ interface AudioSharingRepository { /** The headset groupId to volume map during audio sharing. */ val volumeMap: StateFlow<GroupIdToVolumes> + suspend fun audioSharingAvailable(): Boolean + /** Set the volume of secondary headset during audio sharing. */ suspend fun setSecondaryVolume( @IntRange(from = AUDIO_SHARING_VOLUME_MIN.toLong(), to = AUDIO_SHARING_VOLUME_MAX.toLong()) @@ -216,6 +218,12 @@ class AudioSharingRepositoryImpl( } .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyMap()) + override suspend fun audioSharingAvailable(): Boolean { + return withContext(backgroundCoroutineContext) { + BluetoothUtils.isAudioSharingEnabled() + } + } + override suspend fun setSecondaryVolume( @IntRange(from = AUDIO_SHARING_VOLUME_MIN.toLong(), to = AUDIO_SHARING_VOLUME_MAX.toLong()) volume: Int @@ -262,6 +270,8 @@ class AudioSharingRepositoryEmptyImpl : AudioSharingRepository { MutableStateFlow(BluetoothCsipSetCoordinator.GROUP_ID_INVALID) override val volumeMap: StateFlow<GroupIdToVolumes> = MutableStateFlow(emptyMap()) + override suspend fun audioSharingAvailable(): Boolean = false + override suspend fun setSecondaryVolume( @IntRange(from = AUDIO_SHARING_VOLUME_MIN.toLong(), to = AUDIO_SHARING_VOLUME_MAX.toLong()) volume: Int diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java index 529301138da3..d8b6707b9118 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java @@ -207,20 +207,6 @@ public class WifiUtilsTest { } @Test - public void getHotspotIconResource_deviceTypeExists_shouldNotNull() { - assertThat(WifiUtils.getHotspotIconResource(NetworkProviderInfo.DEVICE_TYPE_PHONE)) - .isNotNull(); - assertThat(WifiUtils.getHotspotIconResource(NetworkProviderInfo.DEVICE_TYPE_TABLET)) - .isNotNull(); - assertThat(WifiUtils.getHotspotIconResource(NetworkProviderInfo.DEVICE_TYPE_LAPTOP)) - .isNotNull(); - assertThat(WifiUtils.getHotspotIconResource(NetworkProviderInfo.DEVICE_TYPE_WATCH)) - .isNotNull(); - assertThat(WifiUtils.getHotspotIconResource(NetworkProviderInfo.DEVICE_TYPE_AUTO)) - .isNotNull(); - } - - @Test public void testInternetIconInjector_getIcon_returnsCorrectValues() { WifiUtils.InternetIconInjector iconInjector = new WifiUtils.InternetIconInjector(mContext); diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java index 2cdd0aee3d85..3530e0f5f9de 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java @@ -106,6 +106,8 @@ public class SystemSettings { Settings.System.UNREAD_NOTIFICATION_DOT_INDICATOR, Settings.System.AUTO_LAUNCH_MEDIA_CONTROLS, Settings.System.LOCALE_PREFERENCES, + Settings.System.MOUSE_REVERSE_VERTICAL_SCROLLING, + Settings.System.MOUSE_SWAP_PRIMARY_BUTTON, Settings.System.TOUCHPAD_POINTER_SPEED, Settings.System.TOUCHPAD_NATURAL_SCROLLING, Settings.System.TOUCHPAD_TAP_TO_CLICK, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java index 282327739fc6..509b88b257fe 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java @@ -221,6 +221,8 @@ public class SystemSettingsValidators { POINTER_ICON_VECTOR_STYLE_STROKE_END)); VALIDATORS.put(System.POINTER_SCALE, new InclusiveFloatRangeValidator(DEFAULT_POINTER_SCALE, LARGE_POINTER_SCALE)); + VALIDATORS.put(System.MOUSE_REVERSE_VERTICAL_SCROLLING, BOOLEAN_VALIDATOR); + VALIDATORS.put(System.MOUSE_SWAP_PRIMARY_BUTTON, BOOLEAN_VALIDATOR); VALIDATORS.put(System.TOUCHPAD_POINTER_SPEED, new InclusiveIntegerRangeValidator(-7, 7)); VALIDATORS.put(System.TOUCHPAD_NATURAL_SCROLLING, BOOLEAN_VALIDATOR); VALIDATORS.put(System.TOUCHPAD_TAP_TO_CLICK, BOOLEAN_VALIDATOR); diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 1f10eadd115b..c4a45d06243b 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -425,8 +425,8 @@ filegroup { "tests/src/**/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt", "tests/src/**/systemui/shared/system/RemoteTransitionTest.java", "tests/src/**/systemui/navigationbar/NavigationBarControllerImplTest.java", - "tests/src/**/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt", - "tests/src/**/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt", + "tests/src/**/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelTest.kt", + "tests/src/**/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt", "tests/src/**/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt", "tests/src/**/systemui/notetask/LaunchNotesRoleSettingsTrampolineActivityTest.kt", "tests/src/**/systemui/notetask/shortcut/LaunchNoteTaskActivityTest.kt", diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING index 07a1e630e1ad..380344a23c99 100644 --- a/packages/SystemUI/TEST_MAPPING +++ b/packages/SystemUI/TEST_MAPPING @@ -148,5 +148,10 @@ { "name": "SystemUIGoogleRobo2RNGTests" } + ], + "imports": [ + { + "path": "cts/tests/tests/multiuser" + } ] } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt new file mode 100644 index 000000000000..9b94c91a348c --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt @@ -0,0 +1,203 @@ +/* + * 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.shared.clocks + +import android.content.Context +import android.content.res.Resources +import android.graphics.Rect +import androidx.annotation.VisibleForTesting +import com.android.systemui.log.core.Logger +import com.android.systemui.log.core.MessageBuffer +import com.android.systemui.plugins.clocks.AlarmData +import com.android.systemui.plugins.clocks.ClockAnimations +import com.android.systemui.plugins.clocks.ClockEvents +import com.android.systemui.plugins.clocks.ClockFaceConfig +import com.android.systemui.plugins.clocks.ClockFaceEvents +import com.android.systemui.plugins.clocks.ClockReactiveSetting +import com.android.systemui.plugins.clocks.WeatherData +import com.android.systemui.plugins.clocks.ZenData +import com.android.systemui.shared.clocks.view.DigitalClockFaceView +import com.android.systemui.shared.clocks.view.FlexClockView +import java.util.Locale +import java.util.TimeZone + +class ComposedDigitalLayerController( + private val ctx: Context, + private val assets: AssetLoader, + private val layer: ComposedDigitalHandLayer, + private val isLargeClock: Boolean, + messageBuffer: MessageBuffer, +) : SimpleClockLayerController { + private val logger = Logger(messageBuffer, ComposedDigitalLayerController::class.simpleName!!) + + val layerControllers = mutableListOf<SimpleClockLayerController>() + val dozeState = DefaultClockController.AnimationState(1F) + var isRegionDark = true + + override var view: DigitalClockFaceView = + when (layer.customizedView) { + "FlexClockView" -> FlexClockView(ctx, assets, messageBuffer) + else -> { + throw IllegalStateException("CustomizedView string is not valid") + } + } + + // Matches LayerControllerConstructor + internal constructor( + ctx: Context, + assets: AssetLoader, + layer: ClockLayer, + isLargeClock: Boolean, + messageBuffer: MessageBuffer, + ) : this(ctx, assets, layer as ComposedDigitalHandLayer, isLargeClock, messageBuffer) + + init { + layer.digitalLayers.forEach { + val controller = + SimpleClockLayerController.Factory.create( + ctx, + assets, + it, + isLargeClock, + messageBuffer, + ) + view.addView(controller.view) + layerControllers.add(controller) + } + } + + private fun refreshTime() { + layerControllers.forEach { it.faceEvents.onTimeTick() } + view.refreshTime() + } + + override val events = + object : ClockEvents { + override fun onTimeZoneChanged(timeZone: TimeZone) { + layerControllers.forEach { it.events.onTimeZoneChanged(timeZone) } + refreshTime() + } + + override fun onTimeFormatChanged(is24Hr: Boolean) { + layerControllers.forEach { it.events.onTimeFormatChanged(is24Hr) } + refreshTime() + } + + override fun onLocaleChanged(locale: Locale) { + layerControllers.forEach { it.events.onLocaleChanged(locale) } + view.onLocaleChanged(locale) + refreshTime() + } + + override fun onWeatherDataChanged(data: WeatherData) { + view.onWeatherDataChanged(data) + } + + override fun onAlarmDataChanged(data: AlarmData) { + view.onAlarmDataChanged(data) + } + + override fun onZenDataChanged(data: ZenData) { + view.onZenDataChanged(data) + } + + override fun onColorPaletteChanged(resources: Resources) {} + + override fun onSeedColorChanged(seedColor: Int?) {} + + override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) {} + + override var isReactiveTouchInteractionEnabled + get() = view.isReactiveTouchInteractionEnabled + set(value) { + view.isReactiveTouchInteractionEnabled = value + } + } + + override fun updateColors() { + view.updateColors(assets, isRegionDark) + } + + override val animations = + object : ClockAnimations { + override fun enter() { + refreshTime() + } + + override fun doze(fraction: Float) { + val (hasChanged, hasJumped) = dozeState.update(fraction) + if (hasChanged) view.animateDoze(dozeState.isActive, !hasJumped) + view.dozeFraction = fraction + view.invalidate() + } + + override fun fold(fraction: Float) { + refreshTime() + } + + override fun charge() { + view.animateCharge() + } + + override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) { + view.onPositionUpdated(fromLeft, direction, fraction) + } + + override fun onPositionUpdated(distance: Float, fraction: Float) {} + + override fun onPickerCarouselSwiping(swipingFraction: Float) { + view.onPickerCarouselSwiping(swipingFraction) + } + } + + override val faceEvents = + object : ClockFaceEvents { + override fun onTimeTick() { + refreshTime() + } + + override fun onRegionDarknessChanged(isRegionDark: Boolean) { + this@ComposedDigitalLayerController.isRegionDark = isRegionDark + updateColors() + } + + override fun onFontSettingChanged(fontSizePx: Float) { + view.onFontSettingChanged(fontSizePx) + } + + override fun onTargetRegionChanged(targetRegion: Rect?) {} + + override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) {} + } + + override val config = + ClockFaceConfig( + hasCustomWeatherDataDisplay = view.hasCustomWeatherDataDisplay, + hasCustomPositionUpdatedAnimation = view.hasCustomPositionUpdatedAnimation, + useCustomClockScene = view.useCustomClockScene, + ) + + @VisibleForTesting + override var fakeTimeMills: Long? = null + get() = field + set(timeInMills) { + field = timeInMills + for (layerController in layerControllers) { + layerController.fakeTimeMills = timeInMills + } + } +} 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 07191c671a34..ac268420fb75 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 @@ -24,6 +24,8 @@ import com.android.systemui.plugins.clocks.ClockMetadata import com.android.systemui.plugins.clocks.ClockPickerConfig import com.android.systemui.plugins.clocks.ClockProvider import com.android.systemui.plugins.clocks.ClockSettings +import com.android.systemui.shared.clocks.view.HorizontalAlignment +import com.android.systemui.shared.clocks.view.VerticalAlignment private val TAG = DefaultClockProvider::class.simpleName const val DEFAULT_CLOCK_ID = "DEFAULT" @@ -33,8 +35,9 @@ class DefaultClockProvider( val ctx: Context, val layoutInflater: LayoutInflater, val resources: Resources, - val hasStepClockAnimation: Boolean = false, - val migratedClocks: Boolean = false, + private val hasStepClockAnimation: Boolean = false, + private val migratedClocks: Boolean = false, + private val clockReactiveVariants: Boolean = false, ) : ClockProvider { private var messageBuffers: ClockMessageBuffers? = null @@ -49,15 +52,23 @@ class DefaultClockProvider( throw IllegalArgumentException("${settings.clockId} is unsupported by $TAG") } - return DefaultClockController( - ctx, - layoutInflater, - resources, - settings, - hasStepClockAnimation, - migratedClocks, - messageBuffers, - ) + return if (clockReactiveVariants) { + // TODO handle the case here where only the smallClock message buffer is added + val assetLoader = + AssetLoader(ctx, ctx, "clocks/", messageBuffers?.smallClockMessageBuffer!!) + + SimpleClockController(ctx, assetLoader, FLEX_DESIGN, messageBuffers) + } else { + DefaultClockController( + ctx, + layoutInflater, + resources, + settings, + hasStepClockAnimation, + migratedClocks, + messageBuffers, + ) + } } override fun getClockPickerConfig(id: ClockId): ClockPickerConfig { @@ -73,4 +84,163 @@ class DefaultClockProvider( resources.getDrawable(R.drawable.clock_default_thumbnail, null), ) } + + companion object { + val FLEX_DESIGN = run { + val largeLayer = + listOf( + ComposedDigitalHandLayer( + layerBounds = LayerBounds.FIT, + customizedView = "FlexClockView", + digitalLayers = + listOf( + DigitalHandLayer( + layerBounds = LayerBounds.FIT, + timespec = DigitalTimespec.FIRST_DIGIT, + style = + FontTextStyle( + fontFamily = "google_sans_flex.ttf", + lineHeight = 147.25f, + fontVariation = + "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100", + ), + aodStyle = + FontTextStyle( + fontVariation = + "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100", + fontFamily = "google_sans_flex.ttf", + fillColorLight = "#FFFFFFFF", + outlineColor = "#00000000", + renderType = RenderType.CHANGE_WEIGHT, + transitionInterpolator = InterpolatorEnum.EMPHASIZED, + transitionDuration = 750, + ), + alignment = + DigitalAlignment( + HorizontalAlignment.CENTER, + VerticalAlignment.CENTER + ), + dateTimeFormat = "hh" + ), + DigitalHandLayer( + layerBounds = LayerBounds.FIT, + timespec = DigitalTimespec.SECOND_DIGIT, + style = + FontTextStyle( + fontFamily = "google_sans_flex.ttf", + lineHeight = 147.25f, + fontVariation = + "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100", + ), + aodStyle = + FontTextStyle( + fontVariation = + "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100", + fontFamily = "google_sans_flex.ttf", + fillColorLight = "#FFFFFFFF", + outlineColor = "#00000000", + renderType = RenderType.CHANGE_WEIGHT, + transitionInterpolator = InterpolatorEnum.EMPHASIZED, + transitionDuration = 750, + ), + alignment = + DigitalAlignment( + HorizontalAlignment.CENTER, + VerticalAlignment.CENTER + ), + dateTimeFormat = "hh" + ), + DigitalHandLayer( + layerBounds = LayerBounds.FIT, + timespec = DigitalTimespec.FIRST_DIGIT, + style = + FontTextStyle( + fontFamily = "google_sans_flex.ttf", + lineHeight = 147.25f, + fontVariation = + "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100", + ), + aodStyle = + FontTextStyle( + fontVariation = + "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100", + fontFamily = "google_sans_flex.ttf", + fillColorLight = "#FFFFFFFF", + outlineColor = "#00000000", + renderType = RenderType.CHANGE_WEIGHT, + transitionInterpolator = InterpolatorEnum.EMPHASIZED, + transitionDuration = 750, + ), + alignment = + DigitalAlignment( + HorizontalAlignment.CENTER, + VerticalAlignment.CENTER + ), + dateTimeFormat = "mm" + ), + DigitalHandLayer( + layerBounds = LayerBounds.FIT, + timespec = DigitalTimespec.SECOND_DIGIT, + style = + FontTextStyle( + fontFamily = "google_sans_flex.ttf", + lineHeight = 147.25f, + fontVariation = + "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100", + ), + aodStyle = + FontTextStyle( + fontVariation = + "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100", + fontFamily = "google_sans_flex.ttf", + fillColorLight = "#FFFFFFFF", + outlineColor = "#00000000", + renderType = RenderType.CHANGE_WEIGHT, + transitionInterpolator = InterpolatorEnum.EMPHASIZED, + transitionDuration = 750, + ), + alignment = + DigitalAlignment( + HorizontalAlignment.CENTER, + VerticalAlignment.CENTER + ), + dateTimeFormat = "mm" + ) + ) + ) + ) + + val smallLayer = + listOf( + DigitalHandLayer( + layerBounds = LayerBounds.FIT, + timespec = DigitalTimespec.TIME_FULL_FORMAT, + style = + FontTextStyle( + fontFamily = "google_sans_flex.ttf", + fontVariation = "'wght' 600, 'wdth' 100, 'opsz' 144, 'ROND' 100", + fontSizeScale = 0.98f, + ), + aodStyle = + FontTextStyle( + fontFamily = "google_sans_flex.ttf", + fontVariation = "'wght' 133, 'wdth' 43, 'opsz' 144, 'ROND' 100", + fillColorLight = "#FFFFFFFF", + outlineColor = "#00000000", + renderType = RenderType.CHANGE_WEIGHT, + ), + alignment = DigitalAlignment(HorizontalAlignment.LEFT, null), + dateTimeFormat = "h:mm" + ) + ) + + ClockDesign( + id = DEFAULT_CLOCK_ID, + name = "@string/clock_default_name", + description = "@string/clock_default_description", + large = ClockFace(layers = largeLayer), + small = ClockFace(layers = smallLayer) + ) + } + } } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/LayoutUtils.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/LayoutUtils.kt new file mode 100644 index 000000000000..ef8bee0875d2 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/LayoutUtils.kt @@ -0,0 +1,35 @@ +/* + * 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.shared.clocks + +import android.graphics.Rect +import android.view.View + +fun computeLayoutDiff( + view: View, + targetRegion: Rect, + isLargeClock: Boolean, +): Pair<Float, Float> { + val parent = view.parent + if (parent is View && parent.isLaidOut() && isLargeClock) { + return Pair( + targetRegion.centerX() - parent.width / 2f, + targetRegion.centerY() - parent.height / 2f + ) + } + return Pair(0f, 0f) +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockController.kt new file mode 100644 index 000000000000..ec7779825bda --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockController.kt @@ -0,0 +1,152 @@ +/* + * 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.shared.clocks + +import android.content.Context +import android.content.res.Resources +import com.android.systemui.monet.Style as MonetStyle +import com.android.systemui.plugins.clocks.AlarmData +import com.android.systemui.plugins.clocks.ClockConfig +import com.android.systemui.plugins.clocks.ClockController +import com.android.systemui.plugins.clocks.ClockEvents +import com.android.systemui.plugins.clocks.ClockMessageBuffers +import com.android.systemui.plugins.clocks.ClockReactiveSetting +import com.android.systemui.plugins.clocks.WeatherData +import com.android.systemui.plugins.clocks.ZenData +import java.io.PrintWriter +import java.util.Locale +import java.util.TimeZone + +/** Controller for a simple json specified clock */ +class SimpleClockController( + private val ctx: Context, + private val assets: AssetLoader, + val design: ClockDesign, + val messageBuffers: ClockMessageBuffers?, +) : ClockController { + override val smallClock = run { + val buffer = messageBuffers?.smallClockMessageBuffer ?: LogUtil.DEFAULT_MESSAGE_BUFFER + SimpleClockFaceController( + ctx, + assets.copy(messageBuffer = buffer), + design.small ?: design.large!!, + false, + buffer, + ) + } + + override val largeClock = run { + val buffer = messageBuffers?.largeClockMessageBuffer ?: LogUtil.DEFAULT_MESSAGE_BUFFER + SimpleClockFaceController( + ctx, + assets.copy(messageBuffer = buffer), + design.large ?: design.small!!, + true, + buffer, + ) + } + + override val config: ClockConfig by lazy { + ClockConfig( + design.id, + design.name?.let { assets.tryReadString(it) ?: it } ?: "", + design.description?.let { assets.tryReadString(it) ?: it } ?: "", + isReactiveToTone = + design.colorPalette == null || design.colorPalette == MonetStyle.CLOCK, + useAlternateSmartspaceAODTransition = + smallClock.config.hasCustomWeatherDataDisplay || + largeClock.config.hasCustomWeatherDataDisplay, + useCustomClockScene = + smallClock.config.useCustomClockScene || largeClock.config.useCustomClockScene, + ) + } + + override val events = + object : ClockEvents { + override var isReactiveTouchInteractionEnabled = false + set(value) { + field = value + smallClock.events.isReactiveTouchInteractionEnabled = value + largeClock.events.isReactiveTouchInteractionEnabled = value + } + + override fun onTimeZoneChanged(timeZone: TimeZone) { + smallClock.events.onTimeZoneChanged(timeZone) + largeClock.events.onTimeZoneChanged(timeZone) + } + + override fun onTimeFormatChanged(is24Hr: Boolean) { + smallClock.events.onTimeFormatChanged(is24Hr) + largeClock.events.onTimeFormatChanged(is24Hr) + } + + override fun onLocaleChanged(locale: Locale) { + smallClock.events.onLocaleChanged(locale) + largeClock.events.onLocaleChanged(locale) + } + + override fun onColorPaletteChanged(resources: Resources) { + assets.refreshColorPalette(design.colorPalette) + smallClock.assets.refreshColorPalette(design.colorPalette) + largeClock.assets.refreshColorPalette(design.colorPalette) + + smallClock.events.onColorPaletteChanged(resources) + largeClock.events.onColorPaletteChanged(resources) + } + + override fun onSeedColorChanged(seedColor: Int?) { + assets.setSeedColor(seedColor, design.colorPalette) + smallClock.assets.setSeedColor(seedColor, design.colorPalette) + largeClock.assets.setSeedColor(seedColor, design.colorPalette) + + smallClock.events.onSeedColorChanged(seedColor) + largeClock.events.onSeedColorChanged(seedColor) + } + + override fun onWeatherDataChanged(data: WeatherData) { + smallClock.events.onWeatherDataChanged(data) + largeClock.events.onWeatherDataChanged(data) + } + + override fun onAlarmDataChanged(data: AlarmData) { + smallClock.events.onAlarmDataChanged(data) + largeClock.events.onAlarmDataChanged(data) + } + + override fun onZenDataChanged(data: ZenData) { + smallClock.events.onZenDataChanged(data) + largeClock.events.onZenDataChanged(data) + } + + override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) { + smallClock.events.onReactiveAxesChanged(axes) + largeClock.events.onReactiveAxesChanged(axes) + } + } + + override fun initialize(resources: Resources, dozeFraction: Float, foldFraction: Float) { + events.onColorPaletteChanged(resources) + smallClock.animations.doze(dozeFraction) + largeClock.animations.doze(dozeFraction) + smallClock.animations.fold(foldFraction) + largeClock.animations.fold(foldFraction) + smallClock.events.onTimeTick() + largeClock.events.onTimeTick() + } + + override fun dump(pw: PrintWriter) {} +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockFaceController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockFaceController.kt new file mode 100644 index 000000000000..ef398d1a52a0 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockFaceController.kt @@ -0,0 +1,314 @@ +/* + * 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.shared.clocks + +import android.content.Context +import android.content.res.Resources +import android.graphics.Rect +import android.view.Gravity +import android.view.View +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.widget.FrameLayout +import com.android.systemui.log.core.MessageBuffer +import com.android.systemui.plugins.clocks.AlarmData +import com.android.systemui.plugins.clocks.ClockAnimations +import com.android.systemui.plugins.clocks.ClockEvents +import com.android.systemui.plugins.clocks.ClockFaceConfig +import com.android.systemui.plugins.clocks.ClockFaceController +import com.android.systemui.plugins.clocks.ClockFaceEvents +import com.android.systemui.plugins.clocks.ClockFaceLayout +import com.android.systemui.plugins.clocks.ClockReactiveSetting +import com.android.systemui.plugins.clocks.ClockTickRate +import com.android.systemui.plugins.clocks.DefaultClockFaceLayout +import com.android.systemui.plugins.clocks.WeatherData +import com.android.systemui.plugins.clocks.ZenData +import com.android.systemui.shared.clocks.view.DigitalClockFaceView +import java.util.Locale +import java.util.TimeZone +import kotlin.math.max + +interface ClockEventUnion : ClockEvents, ClockFaceEvents + +class SimpleClockFaceController( + ctx: Context, + val assets: AssetLoader, + face: ClockFace, + isLargeClock: Boolean, + messageBuffer: MessageBuffer, +) : ClockFaceController { + override val view: View + override val config: ClockFaceConfig by lazy { + ClockFaceConfig( + hasCustomWeatherDataDisplay = layers.any { it.config.hasCustomWeatherDataDisplay }, + hasCustomPositionUpdatedAnimation = + layers.any { it.config.hasCustomPositionUpdatedAnimation }, + tickRate = getTickRate(), + useCustomClockScene = layers.any { it.config.useCustomClockScene }, + ) + } + + val layers = mutableListOf<SimpleClockLayerController>() + + val timespecHandler = DigitalTimespecHandler(DigitalTimespec.TIME_FULL_FORMAT, "hh:mm") + + init { + val lp = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) + lp.gravity = Gravity.CENTER + view = + if (face.layers.size == 1) { + // Optimize a clocks with a single layer by excluding the face level view group. We + // expect the view container from the host process to always be a FrameLayout. + val layer = face.layers[0] + val controller = + SimpleClockLayerController.Factory.create( + ctx, + assets, + layer, + isLargeClock, + messageBuffer, + ) + layers.add(controller) + controller.view.layoutParams = lp + controller.view + } else { + // For multiple views, we use an intermediate RelativeLayout so that we can do some + // intelligent laying out between the children views. + val group = SimpleClockRelativeLayout(ctx, face.faceLayout) + group.layoutParams = lp + group.gravity = Gravity.CENTER + group.clipChildren = false + for (layer in face.layers) { + face.faceLayout?.let { + if (layer is DigitalHandLayer) { + layer.faceLayout = it + } + } + val controller = + SimpleClockLayerController.Factory.create( + ctx, + assets, + layer, + isLargeClock, + messageBuffer, + ) + group.addView(controller.view) + layers.add(controller) + } + group + } + } + + override val layout: ClockFaceLayout = + DefaultClockFaceLayout(view).apply { + views[0].id = + if (isLargeClock) { + assets.getResourcesId("lockscreen_clock_view_large") + } else { + assets.getResourcesId("lockscreen_clock_view") + } + } + + override val events = + object : ClockEventUnion { + override var isReactiveTouchInteractionEnabled = false + get() = field + set(value) { + field = value + layers.forEach { it.events.isReactiveTouchInteractionEnabled = value } + } + + override fun onTimeTick() { + timespecHandler.updateTime() + if ( + config.tickRate == ClockTickRate.PER_MINUTE || + view.contentDescription != timespecHandler.getContentDescription() + ) { + view.contentDescription = timespecHandler.getContentDescription() + } + layers.forEach { it.faceEvents.onTimeTick() } + } + + override fun onTimeZoneChanged(timeZone: TimeZone) { + timespecHandler.timeZone = timeZone + layers.forEach { it.events.onTimeZoneChanged(timeZone) } + } + + override fun onTimeFormatChanged(is24Hr: Boolean) { + timespecHandler.is24Hr = is24Hr + layers.forEach { it.events.onTimeFormatChanged(is24Hr) } + } + + override fun onLocaleChanged(locale: Locale) { + timespecHandler.updateLocale(locale) + layers.forEach { it.events.onLocaleChanged(locale) } + } + + override fun onFontSettingChanged(fontSizePx: Float) { + layers.forEach { it.faceEvents.onFontSettingChanged(fontSizePx) } + } + + override fun onColorPaletteChanged(resources: Resources) { + layers.forEach { + it.events.onColorPaletteChanged(resources) + it.updateColors() + } + } + + override fun onSeedColorChanged(seedColor: Int?) { + layers.forEach { + it.events.onSeedColorChanged(seedColor) + it.updateColors() + } + } + + override fun onRegionDarknessChanged(isRegionDark: Boolean) { + layers.forEach { it.faceEvents.onRegionDarknessChanged(isRegionDark) } + } + + override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) {} + + /** + * targetRegion passed to all customized clock applies counter translationY of + * KeyguardStatusView and keyguard_large_clock_top_margin from default clock + */ + override fun onTargetRegionChanged(targetRegion: Rect?) { + // When a clock needs to be aligned with screen, like weather clock + // it needs to offset back the translation of keyguard_large_clock_top_margin + if (view is DigitalClockFaceView && view.isAlignedWithScreen()) { + val topMargin = getKeyguardLargeClockTopMargin(assets) + targetRegion?.let { + val (_, yDiff) = computeLayoutDiff(view, it, isLargeClock) + // In LS, we use yDiff to counter translate + // the translation of KeyguardLargeClockTopMargin + // With the targetRegion passed from picker, + // we will have yDiff = 0, no translation is needed for weather clock + if (yDiff.toInt() != 0) view.translationY = yDiff - topMargin / 2 + } + return + } + + var maxWidth = 0f + var maxHeight = 0f + + for (layer in layers) { + layer.faceEvents.onTargetRegionChanged(targetRegion) + maxWidth = max(maxWidth, layer.view.layoutParams.width.toFloat()) + maxHeight = max(maxHeight, layer.view.layoutParams.height.toFloat()) + } + + val lp = + if (maxHeight <= 0 || maxWidth <= 0 || targetRegion == null) { + // No specified width/height. Just match parent size. + FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) + } else { + // Scale to fit in targetRegion based on largest child elements. + val ratio = maxWidth / maxHeight + val targetRatio = targetRegion.width() / targetRegion.height().toFloat() + val scale = + if (ratio > targetRatio) targetRegion.width() / maxWidth + else targetRegion.height() / maxHeight + + FrameLayout.LayoutParams( + (maxWidth * scale).toInt(), + (maxHeight * scale).toInt(), + ) + } + + lp.gravity = Gravity.CENTER + view.layoutParams = lp + targetRegion?.let { + val (xDiff, yDiff) = computeLayoutDiff(view, it, isLargeClock) + view.translationX = xDiff + view.translationY = yDiff + } + } + + override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) {} + + override fun onWeatherDataChanged(data: WeatherData) { + layers.forEach { it.events.onWeatherDataChanged(data) } + } + + override fun onAlarmDataChanged(data: AlarmData) { + layers.forEach { it.events.onAlarmDataChanged(data) } + } + + override fun onZenDataChanged(data: ZenData) { + layers.forEach { it.events.onZenDataChanged(data) } + } + } + + override val animations = + object : ClockAnimations { + override fun enter() { + layers.forEach { it.animations.enter() } + } + + override fun doze(fraction: Float) { + layers.forEach { it.animations.doze(fraction) } + } + + override fun fold(fraction: Float) { + layers.forEach { it.animations.fold(fraction) } + } + + override fun charge() { + layers.forEach { it.animations.charge() } + } + + override fun onPickerCarouselSwiping(swipingFraction: Float) { + face.pickerScale?.let { + view.scaleX = swipingFraction * (1 - it.scaleX) + it.scaleX + view.scaleY = swipingFraction * (1 - it.scaleY) + it.scaleY + } + if (!(view is DigitalClockFaceView && view.isAlignedWithScreen())) { + val topMargin = getKeyguardLargeClockTopMargin(assets) + view.translationY = topMargin / 2F * swipingFraction + } + layers.forEach { it.animations.onPickerCarouselSwiping(swipingFraction) } + view.invalidate() + } + + override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) { + layers.forEach { it.animations.onPositionUpdated(fromLeft, direction, fraction) } + } + + override fun onPositionUpdated(distance: Float, fraction: Float) { + layers.forEach { it.animations.onPositionUpdated(distance, fraction) } + } + } + + private fun getTickRate(): ClockTickRate { + var tickRate = ClockTickRate.PER_MINUTE + for (layer in layers) { + if (layer.config.tickRate.value < tickRate.value) { + tickRate = layer.config.tickRate + } + } + return tickRate + } + + private fun getKeyguardLargeClockTopMargin(assets: AssetLoader): Int { + val topMarginRes = + assets.resolveResourceId(null, "dimen", "keyguard_large_clock_top_margin") + if (topMarginRes != null) { + val (res, id) = topMarginRes + return res.getDimensionPixelSize(id) + } + return 0 + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt new file mode 100644 index 000000000000..f71543efa650 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.clocks + +import android.content.Context +import android.view.View +import androidx.annotation.VisibleForTesting +import com.android.systemui.log.core.MessageBuffer +import com.android.systemui.plugins.clocks.ClockAnimations +import com.android.systemui.plugins.clocks.ClockEvents +import com.android.systemui.plugins.clocks.ClockFaceConfig +import com.android.systemui.plugins.clocks.ClockFaceEvents +import com.android.systemui.shared.clocks.view.SimpleDigitalClockTextView +import kotlin.reflect.KClass + +typealias LayerControllerConstructor = + ( + ctx: Context, + assets: AssetLoader, + layer: ClockLayer, + isLargeClock: Boolean, + messageBuffer: MessageBuffer, + ) -> SimpleClockLayerController + +interface SimpleClockLayerController { + val view: View + val events: ClockEvents + val animations: ClockAnimations + val faceEvents: ClockFaceEvents + val config: ClockFaceConfig + + @VisibleForTesting var fakeTimeMills: Long? + + // Called immediately after either onColorPaletteChanged or onSeedColorChanged is called. + // Provided for convience to not duplicate color update logic after state updated. + fun updateColors() {} + + companion object Factory { + val constructorMap = mutableMapOf<Pair<KClass<*>, KClass<*>?>, LayerControllerConstructor>() + + internal inline fun <reified TLayer> registerConstructor( + noinline constructor: LayerControllerConstructor, + ) where TLayer : ClockLayer { + constructorMap[Pair(TLayer::class, null)] = constructor + } + + inline fun <reified TLayer, reified TStyle> registerTextConstructor( + noinline constructor: LayerControllerConstructor, + ) where TLayer : ClockLayer, TStyle : TextStyle { + constructorMap[Pair(TLayer::class, TStyle::class)] = constructor + } + + init { + registerConstructor<ComposedDigitalHandLayer>(::ComposedDigitalLayerController) + registerTextConstructor<DigitalHandLayer, FontTextStyle>(::createSimpleDigitalLayer) + } + + private fun createSimpleDigitalLayer( + ctx: Context, + assets: AssetLoader, + layer: ClockLayer, + isLargeClock: Boolean, + messageBuffer: MessageBuffer + ): SimpleClockLayerController { + val view = SimpleDigitalClockTextView(ctx, messageBuffer) + return SimpleDigitalHandLayerController( + ctx, + assets, + layer as DigitalHandLayer, + view, + messageBuffer + ) + } + + fun create( + ctx: Context, + assets: AssetLoader, + layer: ClockLayer, + isLargeClock: Boolean, + messageBuffer: MessageBuffer + ): SimpleClockLayerController { + val styleClass = if (layer is DigitalHandLayer) layer.style::class else null + val key = Pair(layer::class, styleClass) + return constructorMap[key]?.invoke(ctx, assets, layer, isLargeClock, messageBuffer) + ?: throw IllegalArgumentException("Unrecognized ClockLayer type: $key") + } + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockRelativeLayout.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockRelativeLayout.kt new file mode 100644 index 000000000000..6e1b9aabf86d --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockRelativeLayout.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.clocks + +import android.content.Context +import android.view.View.MeasureSpec.EXACTLY +import android.widget.RelativeLayout +import androidx.core.view.children +import com.android.systemui.shared.clocks.view.SimpleDigitalClockView + +class SimpleClockRelativeLayout(context: Context, val faceLayout: DigitalFaceLayout?) : + RelativeLayout(context) { + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + // For migrate_clocks_to_blueprint, mode is EXACTLY + // when the flag is turned off, we won't execute this codes + if (MeasureSpec.getMode(heightMeasureSpec) == EXACTLY) { + if ( + faceLayout == DigitalFaceLayout.TWO_PAIRS_VERTICAL || + faceLayout == DigitalFaceLayout.FOUR_DIGITS_ALIGN_CENTER + ) { + val constrainedHeight = MeasureSpec.getSize(heightMeasureSpec) / 2F + children.forEach { + // The assumption here is the height of text view is linear to font size + (it as SimpleDigitalClockView).applyTextSize( + constrainedHeight, + constrainedByHeight = true, + ) + } + } + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt new file mode 100644 index 000000000000..a3240f81e499 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt @@ -0,0 +1,328 @@ +/* + * 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.shared.clocks + +import android.content.Context +import android.content.res.Resources +import android.graphics.Rect +import android.view.View +import android.view.ViewGroup +import android.widget.RelativeLayout +import androidx.annotation.VisibleForTesting +import com.android.systemui.customization.R +import com.android.systemui.log.core.Logger +import com.android.systemui.log.core.MessageBuffer +import com.android.systemui.plugins.clocks.AlarmData +import com.android.systemui.plugins.clocks.ClockAnimations +import com.android.systemui.plugins.clocks.ClockEvents +import com.android.systemui.plugins.clocks.ClockFaceConfig +import com.android.systemui.plugins.clocks.ClockFaceEvents +import com.android.systemui.plugins.clocks.ClockReactiveSetting +import com.android.systemui.plugins.clocks.WeatherData +import com.android.systemui.plugins.clocks.ZenData +import com.android.systemui.shared.clocks.view.SimpleDigitalClockView +import java.util.Locale +import java.util.TimeZone + +private val TAG = SimpleDigitalHandLayerController::class.simpleName!! + +open class SimpleDigitalHandLayerController<T>( + private val ctx: Context, + private val assets: AssetLoader, + private val layer: DigitalHandLayer, + override val view: T, + messageBuffer: MessageBuffer, +) : SimpleClockLayerController where T : View, T : SimpleDigitalClockView { + private val logger = Logger(messageBuffer, TAG) + val timespec = DigitalTimespecHandler(layer.timespec, layer.dateTimeFormat) + + @VisibleForTesting + fun hasLeadingZero() = layer.dateTimeFormat.startsWith("hh") || timespec.is24Hr + + @VisibleForTesting + override var fakeTimeMills: Long? + get() = timespec.fakeTimeMills + set(value) { + timespec.fakeTimeMills = value + } + + override val config = ClockFaceConfig() + var dozeState: DefaultClockController.AnimationState? = null + var isRegionDark: Boolean = true + + init { + view.layoutParams = + RelativeLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + if (layer.alignment != null) { + layer.alignment.verticalAlignment?.let { view.verticalAlignment = it } + layer.alignment.horizontalAlignment?.let { view.horizontalAlignment = it } + } + view.applyStyles(assets, layer.style, layer.aodStyle) + view.id = + ctx.resources.getIdentifier( + generateDigitalLayerIdString(layer), + "id", + ctx.getPackageName(), + ) + } + + fun applyLayout(layout: DigitalFaceLayout?) { + when (layout) { + DigitalFaceLayout.FOUR_DIGITS_ALIGN_CENTER, + DigitalFaceLayout.FOUR_DIGITS_HORIZONTAL -> applyFourDigitsLayout(layout) + DigitalFaceLayout.TWO_PAIRS_HORIZONTAL, + DigitalFaceLayout.TWO_PAIRS_VERTICAL -> applyTwoPairsLayout(layout) + else -> { + // one view always use FrameLayout + // no need to change here + } + } + applyMargin() + } + + private fun applyMargin() { + if (view.layoutParams is RelativeLayout.LayoutParams) { + val lp = view.layoutParams as RelativeLayout.LayoutParams + layer.marginRatio?.let { + lp.setMargins( + /* left = */ (it.left * view.measuredWidth).toInt(), + /* top = */ (it.top * view.measuredHeight).toInt(), + /* right = */ (it.right * view.measuredWidth).toInt(), + /* bottom = */ (it.bottom * view.measuredHeight).toInt(), + ) + } + view.layoutParams = lp + } + } + + private fun applyTwoPairsLayout(twoPairsLayout: DigitalFaceLayout) { + val lp = view.layoutParams as RelativeLayout.LayoutParams + lp.addRule(RelativeLayout.TEXT_ALIGNMENT_CENTER) + if (twoPairsLayout == DigitalFaceLayout.TWO_PAIRS_HORIZONTAL) { + when (view.id) { + R.id.HOUR_DIGIT_PAIR -> { + lp.addRule(RelativeLayout.CENTER_VERTICAL) + lp.addRule(RelativeLayout.ALIGN_PARENT_START) + } + R.id.MINUTE_DIGIT_PAIR -> { + lp.addRule(RelativeLayout.CENTER_VERTICAL) + lp.addRule(RelativeLayout.END_OF, R.id.HOUR_DIGIT_PAIR) + } + else -> { + throw Exception("cannot apply two pairs layout to view ${view.id}") + } + } + } else { + when (view.id) { + R.id.HOUR_DIGIT_PAIR -> { + lp.addRule(RelativeLayout.CENTER_HORIZONTAL) + lp.addRule(RelativeLayout.ALIGN_PARENT_TOP) + } + R.id.MINUTE_DIGIT_PAIR -> { + lp.addRule(RelativeLayout.CENTER_HORIZONTAL) + lp.addRule(RelativeLayout.BELOW, R.id.HOUR_DIGIT_PAIR) + } + else -> { + throw Exception("cannot apply two pairs layout to view ${view.id}") + } + } + } + view.layoutParams = lp + } + + private fun applyFourDigitsLayout(fourDigitsfaceLayout: DigitalFaceLayout) { + val lp = view.layoutParams as RelativeLayout.LayoutParams + when (fourDigitsfaceLayout) { + DigitalFaceLayout.FOUR_DIGITS_ALIGN_CENTER -> { + when (view.id) { + R.id.HOUR_FIRST_DIGIT -> { + lp.addRule(RelativeLayout.ALIGN_PARENT_START) + lp.addRule(RelativeLayout.ALIGN_PARENT_TOP) + } + R.id.HOUR_SECOND_DIGIT -> { + lp.addRule(RelativeLayout.END_OF, R.id.HOUR_FIRST_DIGIT) + lp.addRule(RelativeLayout.ALIGN_TOP, R.id.HOUR_FIRST_DIGIT) + } + R.id.MINUTE_FIRST_DIGIT -> { + lp.addRule(RelativeLayout.ALIGN_START, R.id.HOUR_FIRST_DIGIT) + lp.addRule(RelativeLayout.BELOW, R.id.HOUR_FIRST_DIGIT) + } + R.id.MINUTE_SECOND_DIGIT -> { + lp.addRule(RelativeLayout.ALIGN_START, R.id.HOUR_SECOND_DIGIT) + lp.addRule(RelativeLayout.BELOW, R.id.HOUR_SECOND_DIGIT) + } + else -> { + throw Exception("cannot apply four digits layout to view ${view.id}") + } + } + } + DigitalFaceLayout.FOUR_DIGITS_HORIZONTAL -> { + when (view.id) { + R.id.HOUR_FIRST_DIGIT -> { + lp.addRule(RelativeLayout.CENTER_VERTICAL) + lp.addRule(RelativeLayout.ALIGN_PARENT_START) + } + R.id.HOUR_SECOND_DIGIT -> { + lp.addRule(RelativeLayout.CENTER_VERTICAL) + lp.addRule(RelativeLayout.END_OF, R.id.HOUR_FIRST_DIGIT) + } + R.id.MINUTE_FIRST_DIGIT -> { + lp.addRule(RelativeLayout.CENTER_VERTICAL) + lp.addRule(RelativeLayout.END_OF, R.id.HOUR_SECOND_DIGIT) + } + R.id.MINUTE_SECOND_DIGIT -> { + lp.addRule(RelativeLayout.CENTER_VERTICAL) + lp.addRule(RelativeLayout.END_OF, R.id.MINUTE_FIRST_DIGIT) + } + else -> { + throw Exception("cannot apply FOUR_DIGITS_HORIZONTAL to view ${view.id}") + } + } + } + else -> { + throw IllegalArgumentException( + "applyFourDigitsLayout function should not " + + "have parameters as ${layer.faceLayout}" + ) + } + } + if (lp == view.layoutParams) { + return + } + view.layoutParams = lp + } + + fun refreshTime() { + timespec.updateTime() + val text = timespec.getDigitString() + if (view.text != text) { + view.text = text + view.refreshTime() + logger.d({ "refreshTime: new text=$str1" }) { str1 = text } + } + } + + override val events = + object : ClockEvents { + override var isReactiveTouchInteractionEnabled = false + + override fun onLocaleChanged(locale: Locale) { + timespec.updateLocale(locale) + refreshTime() + } + + /** Call whenever the text time format changes (12hr vs 24hr) */ + override fun onTimeFormatChanged(is24Hr: Boolean) { + timespec.is24Hr = is24Hr + refreshTime() + } + + override fun onTimeZoneChanged(timeZone: TimeZone) { + timespec.timeZone = timeZone + refreshTime() + } + + override fun onColorPaletteChanged(resources: Resources) {} + + override fun onSeedColorChanged(seedColor: Int?) {} + + override fun onWeatherDataChanged(data: WeatherData) {} + + override fun onAlarmDataChanged(data: AlarmData) {} + + override fun onZenDataChanged(data: ZenData) {} + + override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) {} + } + + override fun updateColors() { + view.updateColors(assets, isRegionDark) + refreshTime() + } + + override val animations = + object : ClockAnimations { + override fun enter() { + applyLayout(layer.faceLayout) + refreshTime() + } + + override fun doze(fraction: Float) { + if (dozeState == null) { + dozeState = DefaultClockController.AnimationState(fraction) + view.animateDoze(dozeState!!.isActive, false) + } else { + val (hasChanged, hasJumped) = dozeState!!.update(fraction) + if (hasChanged) view.animateDoze(dozeState!!.isActive, !hasJumped) + } + view.dozeFraction = fraction + } + + override fun fold(fraction: Float) { + applyLayout(layer.faceLayout) + refreshTime() + } + + override fun charge() { + view.animateCharge() + } + + override fun onPickerCarouselSwiping(swipingFraction: Float) {} + + override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {} + + override fun onPositionUpdated(distance: Float, fraction: Float) {} + } + + override val faceEvents = + object : ClockFaceEvents { + override fun onTimeTick() { + refreshTime() + if ( + layer.timespec == DigitalTimespec.TIME_FULL_FORMAT || + layer.timespec == DigitalTimespec.DATE_FORMAT + ) { + view.contentDescription = timespec.getContentDescription() + } + } + + override fun onFontSettingChanged(fontSizePx: Float) { + view.applyTextSize(fontSizePx) + applyMargin() + } + + override fun onRegionDarknessChanged(isRegionDark: Boolean) { + this@SimpleDigitalHandLayerController.isRegionDark = isRegionDark + updateColors() + } + + override fun onTargetRegionChanged(targetRegion: Rect?) {} + + override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) {} + } + + companion object { + private val DEFAULT_LIGHT_COLOR = "@android:color/system_accent1_100+0" + private val DEFAULT_DARK_COLOR = "@android:color/system_accent2_600+0" + + fun getDefaultColor(assets: AssetLoader, isRegionDark: Boolean) = + assets.readColor(if (isRegionDark) DEFAULT_LIGHT_COLOR else DEFAULT_DARK_COLOR) + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt new file mode 100644 index 000000000000..ed6a403a7c7e --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.clocks + +import android.icu.text.DateFormat +import android.icu.text.SimpleDateFormat +import android.icu.util.TimeZone as IcuTimeZone +import android.icu.util.ULocale +import androidx.annotation.VisibleForTesting +import java.util.Calendar +import java.util.Locale +import java.util.TimeZone + +open class TimespecHandler( + val cal: Calendar, +) { + var timeZone: TimeZone + get() = cal.timeZone + set(value) { + cal.timeZone = value + onTimeZoneChanged() + } + + @VisibleForTesting var fakeTimeMills: Long? = null + + fun updateTime() { + var timeMs = fakeTimeMills ?: System.currentTimeMillis() + cal.timeInMillis = (timeMs * TIME_TRAVEL_SCALE).toLong() + } + + protected open fun onTimeZoneChanged() {} + + companion object { + // Modifying this will cause the clock to run faster or slower. This is a useful way of + // manually checking that clocks are correctly animating through time. + private const val TIME_TRAVEL_SCALE = 1.0 + } +} + +class DigitalTimespecHandler( + val timespec: DigitalTimespec, + private val timeFormat: String, + cal: Calendar = Calendar.getInstance(), +) : TimespecHandler(cal) { + var is24Hr = false + set(value) { + field = value + applyPattern() + } + + private var dateFormat = updateSimpleDateFormat(Locale.getDefault()) + private var contentDescriptionFormat = getContentDescriptionFormat(Locale.getDefault()) + + init { + applyPattern() + } + + override fun onTimeZoneChanged() { + dateFormat.timeZone = IcuTimeZone.getTimeZone(cal.timeZone.id) + contentDescriptionFormat?.timeZone = IcuTimeZone.getTimeZone(cal.timeZone.id) + applyPattern() + } + + fun updateLocale(locale: Locale) { + dateFormat = updateSimpleDateFormat(locale) + contentDescriptionFormat = getContentDescriptionFormat(locale) + onTimeZoneChanged() + } + + private fun updateSimpleDateFormat(locale: Locale): DateFormat { + if ( + locale.language.equals(Locale.ENGLISH.language) || + timespec != DigitalTimespec.DATE_FORMAT + ) { + // force date format in English, and time format to use format defined in json + return SimpleDateFormat(timeFormat, timeFormat, ULocale.forLocale(locale)) + } else { + return SimpleDateFormat.getInstanceForSkeleton(timeFormat, locale) + } + } + + private fun getContentDescriptionFormat(locale: Locale): DateFormat? { + return when (timespec) { + DigitalTimespec.TIME_FULL_FORMAT -> + SimpleDateFormat.getInstanceForSkeleton("hh:mm", locale) + DigitalTimespec.DATE_FORMAT -> + SimpleDateFormat.getInstanceForSkeleton("EEEE MMMM d", locale) + else -> { + null + } + } + } + + private fun applyPattern() { + val timeFormat24Hour = timeFormat.replace("hh", "h").replace("h", "HH") + val format = if (is24Hr) timeFormat24Hour else timeFormat + if (timespec != DigitalTimespec.DATE_FORMAT) { + (dateFormat as SimpleDateFormat).applyPattern(format) + (contentDescriptionFormat as? SimpleDateFormat)?.applyPattern( + if (is24Hr) CONTENT_DESCRIPTION_TIME_FORMAT_24_HOUR + else CONTENT_DESCRIPTION_TIME_FORMAT_12_HOUR + ) + } + } + + private fun getSingleDigit(): String { + val isFirstDigit = timespec == DigitalTimespec.FIRST_DIGIT + val text = dateFormat.format(cal.time).toString() + return text.substring( + if (isFirstDigit) 0 else text.length - 1, + if (isFirstDigit) text.length - 1 else text.length + ) + } + + fun getDigitString(): String { + return when (timespec) { + DigitalTimespec.FIRST_DIGIT, + DigitalTimespec.SECOND_DIGIT -> getSingleDigit() + DigitalTimespec.DIGIT_PAIR -> { + dateFormat.format(cal.time).toString() + } + DigitalTimespec.TIME_FULL_FORMAT -> { + dateFormat.format(cal.time).toString() + } + DigitalTimespec.DATE_FORMAT -> { + dateFormat.format(cal.time).toString().uppercase() + } + } + } + + fun getContentDescription(): String? { + return when (timespec) { + DigitalTimespec.TIME_FULL_FORMAT, + DigitalTimespec.DATE_FORMAT -> { + contentDescriptionFormat?.format(cal.time).toString() + } + else -> { + return null + } + } + } + + companion object { + const val CONTENT_DESCRIPTION_TIME_FORMAT_12_HOUR = "hh:mm" + const val CONTENT_DESCRIPTION_TIME_FORMAT_24_HOUR = "HH:mm" + } +} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt index d63e728cf443..d63e728cf443 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt index 2c1dacdfae73..2c1dacdfae73 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt index 5fe5cb3fe43c..5fe5cb3fe43c 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java index 7bb6ef1c8895..7bb6ef1c8895 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt index 9cd52153eff6..9cd52153eff6 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt index 3c229975eef5..3c229975eef5 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java index 8b5372a1f035..8b5372a1f035 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlaySuppressionControllerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ClipboardOverlaySuppressionControllerImplTest.kt index 86788d35a670..86788d35a670 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlaySuppressionControllerImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ClipboardOverlaySuppressionControllerImplTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt index 179ba2256442..cecc11e5ffd4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt @@ -19,7 +19,6 @@ package com.android.systemui.communal.view.viewmodel import android.appwidget.AppWidgetProviderInfo import android.content.ActivityNotFoundException import android.content.ComponentName -import android.content.Intent import android.content.pm.PackageManager import android.content.pm.UserInfo import android.provider.Settings @@ -27,7 +26,6 @@ import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityManager import android.view.accessibility.accessibilityManager import android.widget.RemoteViews -import androidx.activity.result.ActivityResultLauncher import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger @@ -88,7 +86,6 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { @Mock private lateinit var mediaHost: MediaHost @Mock private lateinit var uiEventLogger: UiEventLogger @Mock private lateinit var packageManager: PackageManager - @Mock private lateinit var activityResultLauncher: ActivityResultLauncher<Intent> @Mock private lateinit var metricsLogger: CommunalMetricsLogger private val kosmos = testKosmos() @@ -117,10 +114,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { communalSceneInteractor = kosmos.communalSceneInteractor communalInteractor = spy(kosmos.communalInteractor) kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO)) - kosmos.fakeUserTracker.set( - userInfos = listOf(MAIN_USER_INFO), - selectedUserIndex = 0, - ) + kosmos.fakeUserTracker.set(userInfos = listOf(MAIN_USER_INFO), selectedUserIndex = 0) kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true) accessibilityManager = kosmos.accessibilityManager @@ -257,10 +251,13 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { @Test fun onOpenWidgetPicker_launchesWidgetPickerActivity() { testScope.runTest { + var activityStarted = false val success = - underTest.onOpenWidgetPicker(testableResources.resources, activityResultLauncher) + underTest.onOpenWidgetPicker(testableResources.resources) { _ -> + run { activityStarted = true } + } - verify(activityResultLauncher).launch(any()) + assertTrue(activityStarted) assertTrue(success) } } @@ -268,14 +265,10 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { @Test fun onOpenWidgetPicker_activityLaunchThrowsException_failure() { testScope.runTest { - whenever(activityResultLauncher.launch(any())) - .thenThrow(ActivityNotFoundException::class.java) - val success = - underTest.onOpenWidgetPicker( - testableResources.resources, - activityResultLauncher, - ) + underTest.onOpenWidgetPicker(testableResources.resources) { _ -> + run { throw ActivityNotFoundException() } + } assertFalse(success) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/HydratorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/HydratorTest.kt index b0e93fbecbb9..b0e93fbecbb9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/HydratorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/HydratorTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarButtonTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarButtonTest.java index 00e79f5a3ac2..00e79f5a3ac2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarButtonTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarButtonTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt index c7da03dbbf30..c7da03dbbf30 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/data/model/ScreenRecordModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/data/model/ScreenRecordModelTest.kt index 9331c8df46d4..0bbf47c2275c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/data/model/ScreenRecordModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/data/model/ScreenRecordModelTest.kt @@ -16,13 +16,16 @@ package com.android.systemui.screenrecord.data.model +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.screenrecord.data.model.ScreenRecordModel.Starting.Companion.toCountdownSeconds import com.google.common.truth.Truth.assertThat +import org.junit.runner.RunWith import kotlin.test.Test @SmallTest +@RunWith(AndroidJUnit4::class) class ScreenRecordModelTest : SysuiTestCase() { @Test fun countdownSeconds_millis0_is0() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt index 4ede90ec4466..4ede90ec4466 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index 0e9ef06aa1b9..0454317b5f04 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -22,6 +22,10 @@ import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer; import static com.google.common.truth.Truth.assertThat; +import static kotlinx.coroutines.flow.FlowKt.emptyFlow; +import static kotlinx.coroutines.flow.SharedFlowKt.MutableSharedFlow; +import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyFloat; @@ -36,10 +40,6 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static kotlinx.coroutines.flow.FlowKt.emptyFlow; -import static kotlinx.coroutines.flow.SharedFlowKt.MutableSharedFlow; -import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow; - import android.animation.Animator; import android.annotation.IdRes; import android.content.ContentResolver; @@ -201,6 +201,12 @@ import com.android.systemui.util.time.FakeSystemClock; import com.android.systemui.util.time.SystemClock; import com.android.wm.shell.animation.FlingAnimationUtils; +import dagger.Lazy; + +import kotlinx.coroutines.CoroutineDispatcher; +import kotlinx.coroutines.channels.BufferOverflow; +import kotlinx.coroutines.test.TestScope; + import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -215,11 +221,6 @@ import java.util.HashSet; import java.util.List; import java.util.Optional; -import dagger.Lazy; -import kotlinx.coroutines.CoroutineDispatcher; -import kotlinx.coroutines.channels.BufferOverflow; -import kotlinx.coroutines.test.TestScope; - public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { protected static final int SPLIT_SHADE_FULL_TRANSITION_DISTANCE = 400; @@ -461,7 +462,8 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { () -> mKosmos.getSceneInteractor(), () -> mKosmos.getSceneContainerOcclusionInteractor(), () -> mKosmos.getKeyguardClockInteractor(), - () -> mKosmos.getSceneBackInteractor()); + () -> mKosmos.getSceneBackInteractor(), + () -> mKosmos.getAlternateBouncerInteractor()); KeyguardStatusView keyguardStatusView = new KeyguardStatusView(mContext); keyguardStatusView.setId(R.id.keyguard_status_view); @@ -622,7 +624,8 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { () -> mKosmos.getSceneInteractor(), () -> mKosmos.getSceneContainerOcclusionInteractor(), () -> mKosmos.getKeyguardClockInteractor(), - () -> mKosmos.getSceneBackInteractor()), + () -> mKosmos.getSceneBackInteractor(), + () -> mKosmos.getAlternateBouncerInteractor()), mKeyguardBypassController, mDozeParameters, mScreenOffAnimationController, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt index d6b3b919913f..d6b3b919913f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt index db274cc311dd..f8720b4fe5f8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt @@ -28,6 +28,8 @@ import com.android.internal.logging.testing.UiEventLoggerFake 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.bouncer.domain.interactor.alternateBouncerInteractor +import com.android.systemui.bouncer.domain.interactor.givenCanShowAlternateBouncer import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor import com.android.systemui.flags.DisableSceneContainer @@ -83,8 +85,9 @@ class StatusBarStateControllerImplTest(flags: FlagsParameterization) : SysuiTest private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val sceneInteractor = kosmos.sceneInteractor - private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository + private val sceneInteractor by lazy { kosmos.sceneInteractor } + private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository } + private val alternateBouncerInteractor by lazy { kosmos.alternateBouncerInteractor } private val mockDarkAnimator = mock<ObjectAnimator>() private lateinit var underTest: StatusBarStateControllerImpl @@ -121,6 +124,7 @@ class StatusBarStateControllerImplTest(flags: FlagsParameterization) : SysuiTest { kosmos.sceneContainerOcclusionInteractor }, { kosmos.keyguardClockInteractor }, { kosmos.sceneBackInteractor }, + { kosmos.alternateBouncerInteractor }, ) { override fun createDarkAnimator(): ObjectAnimator { return mockDarkAnimator @@ -299,6 +303,52 @@ class StatusBarStateControllerImplTest(flags: FlagsParameterization) : SysuiTest @Test @EnableSceneContainer + @DisableFlags(DualShade.FLAG_NAME) + fun start_hydratesStatusBarState_withAlternateBouncer() = + testScope.runTest { + var statusBarState = underTest.state + val listener = + object : StatusBarStateController.StateListener { + override fun onStateChanged(newState: Int) { + statusBarState = newState + } + } + underTest.addCallback(listener) + + val currentScene by collectLastValue(sceneInteractor.currentScene) + val deviceUnlockStatus by + collectLastValue(kosmos.deviceUnlockedInteractor.deviceUnlockStatus) + val alternateBouncerIsVisible by collectLastValue(alternateBouncerInteractor.isVisible) + + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Password + ) + kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) + runCurrent() + assertThat(deviceUnlockStatus!!.isUnlocked).isTrue() + + sceneInteractor.changeScene(toScene = Scenes.Lockscreen, loggingReason = "reason") + runCurrent() + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + + kosmos.givenCanShowAlternateBouncer() + alternateBouncerInteractor.forceShow() + runCurrent() + assertThat(alternateBouncerIsVisible).isTrue() + + // Call start to begin hydrating based on the scene framework: + underTest.start() + + sceneInteractor.changeScene(toScene = Scenes.Gone, loggingReason = "reason") + runCurrent() + assertThat(currentScene).isEqualTo(Scenes.Gone) + assertThat(statusBarState).isEqualTo(StatusBarState.KEYGUARD) + } + + @Test + @EnableSceneContainer @EnableFlags(DualShade.FLAG_NAME) fun start_hydratesStatusBarState_dualShade_whileLocked() = testScope.runTest { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt index 8f41caf54ec8..8f41caf54ec8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt index 7bc6d4ae2816..7bc6d4ae2816 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt index ecb1a6d44b22..ecb1a6d44b22 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt index 5005d1609113..5005d1609113 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt index e6101f500ad1..e6101f500ad1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt index 77992dbaecc2..77992dbaecc2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt index d0c5e7a102e0..d0c5e7a102e0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt index c62e40414121..c62e40414121 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt index 118dea6376bb..118dea6376bb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt index 6bfb40fa17c5..6bfb40fa17c5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt index bfb63ac66d3d..bfb63ac66d3d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt index 16101bfe387c..16101bfe387c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt index 325a42bca7d1..325a42bca7d1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt index 791a21d0fb63..791a21d0fb63 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt index 4977c548fb92..4977c548fb92 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt index 6e4d8863fee2..6e4d8863fee2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt index 26ce7b956fde..26ce7b956fde 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithRonsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithRonsViewModelTest.kt index 631120b39805..631120b39805 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithRonsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithRonsViewModelTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/AboveShelfObserverTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/AboveShelfObserverTest.java index 01a0fd020bda..01a0fd020bda 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/AboveShelfObserverTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/AboveShelfObserverTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorControllerTest.kt index cb92b7745961..cb92b7745961 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorControllerTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt index d717fe4c1e04..d717fe4c1e04 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java index 1c5f37cc60c3..1c5f37cc60c3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java index 027e899e20df..027e899e20df 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt index 48608ebd6de0..48608ebd6de0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationSnoozeTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationSnoozeTest.java index 22f1e4604bbd..22f1e4604bbd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationSnoozeTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationSnoozeTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java index 2340d0289db4..2340d0289db4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java index ec280a1d6d01..ec280a1d6d01 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt index 9d990b1d7edf..9d990b1d7edf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCustomViewWrapperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCustomViewWrapperTest.java index f9a9704334a0..f9a9704334a0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCustomViewWrapperTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCustomViewWrapperTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt index fc829d53a6b4..fc829d53a6b4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapperTest.java index d17c8dbcf38d..d17c8dbcf38d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapperTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapperTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java index 14bbd38ece2c..14bbd38ece2c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt index 660eb308fdf3..660eb308fdf3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java index dd03ab393ce9..dd03ab393ce9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java index 68e17c1b2d73..68e17c1b2d73 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewTest.java index 0932a0c9307c..0932a0c9307c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarTransitionsControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LightBarTransitionsControllerTest.java index 7dfdb9228936..7dfdb9228936 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarTransitionsControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LightBarTransitionsControllerTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt index 83600422bda4..83600422bda4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index 1d74331e429b..1d74331e429b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt index b560c591af1e..b560c591af1e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt index 624c070e95e0..624c070e95e0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt index bd857807851c..bd857807851c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt index 16061df1fa89..16061df1fa89 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt index 06b3b57bd133..06b3b57bd133 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt index ba9fa926947c..cd18925eb44f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt @@ -25,6 +25,7 @@ import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.InProgress import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NotStarted import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE import com.google.common.truth.Truth.assertThat +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -34,10 +35,12 @@ class BackGestureMonitorTest : SysuiTestCase() { private var gestureState: GestureState = NotStarted private val gestureMonitor = - BackGestureMonitor( - gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(), - gestureStateChangedCallback = { gestureState = it }, - ) + BackGestureMonitor(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt()) + + @Before + fun before() { + gestureMonitor.addGestureStateCallback { gestureState = it } + } @Test fun triggersGestureFinishedForThreeFingerGestureRight() { @@ -82,7 +85,7 @@ class BackGestureMonitorTest : SysuiTestCase() { } private fun assertStateAfterEvents(events: List<MotionEvent>, expectedState: GestureState) { - events.forEach { gestureMonitor.processTouchpadEvent(it) } + events.forEach { gestureMonitor.accept(it) } assertThat(gestureState).isEqualTo(expectedState) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt index a83ed5649889..3f1633a8972f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt @@ -36,10 +36,7 @@ class EasterEggGestureTest : SysuiTestCase() { private var triggered = false private val handler = TouchpadGestureHandler( - BackGestureMonitor( - gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(), - gestureStateChangedCallback = {}, - ), + BackGestureMonitor(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt()), EasterEggGestureMonitor(callback = { triggered = true }), ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitorTest.kt index 59cc026e82ee..edf0e5698bf0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitorTest.kt @@ -25,6 +25,7 @@ import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.InProgress import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NotStarted import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE import com.google.common.truth.Truth.assertThat +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -34,10 +35,12 @@ class HomeGestureMonitorTest : SysuiTestCase() { private var gestureState: GestureState = GestureState.NotStarted private val gestureMonitor = - HomeGestureMonitor( - gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(), - gestureStateChangedCallback = { gestureState = it }, - ) + HomeGestureMonitor(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt()) + + @Before + fun before() { + gestureMonitor.addGestureStateCallback { gestureState = it } + } @Test fun triggersGestureFinishedForThreeFingerGestureUp() { @@ -78,7 +81,7 @@ class HomeGestureMonitorTest : SysuiTestCase() { } private fun assertStateAfterEvents(events: List<MotionEvent>, expectedState: GestureState) { - events.forEach { gestureMonitor.processTouchpadEvent(it) } + events.forEach { gestureMonitor.accept(it) } assertThat(gestureState).isEqualTo(expectedState) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitorTest.kt index 7eac6bb09264..f68e7732d04e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitorTest.kt @@ -26,6 +26,7 @@ import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.InProgress import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NotStarted import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE import com.google.common.truth.Truth.assertThat +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.doReturn @@ -44,7 +45,7 @@ class RecentAppsGestureMonitorTest : SysuiTestCase() { } private var gestureState: GestureState = GestureState.NotStarted - private val velocityTracker = + private val velocityTracker1D = mock<VelocityTracker1D> { // by default return correct speed for the gesture - as if pointer is slowing down on { calculateVelocity() } doReturn SLOW @@ -52,11 +53,15 @@ class RecentAppsGestureMonitorTest : SysuiTestCase() { private val gestureMonitor = RecentAppsGestureMonitor( gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(), - gestureStateChangedCallback = { gestureState = it }, velocityThresholdPxPerMs = THRESHOLD_VELOCITY_PX_PER_MS, - velocityTracker = velocityTracker, + velocityTracker = VerticalVelocityTracker(velocityTracker1D), ) + @Before + fun before() { + gestureMonitor.addGestureStateCallback { gestureState = it } + } + @Test fun triggersGestureFinishedForThreeFingerGestureUp() { assertStateAfterEvents(events = ThreeFingerGesture.swipeUp(), expectedState = Finished) @@ -64,7 +69,7 @@ class RecentAppsGestureMonitorTest : SysuiTestCase() { @Test fun doesntTriggerGestureFinished_onGestureSpeedTooHigh() { - whenever(velocityTracker.calculateVelocity()).thenReturn(FAST) + whenever(velocityTracker1D.calculateVelocity()).thenReturn(FAST) assertStateAfterEvents(events = ThreeFingerGesture.swipeUp(), expectedState = NotStarted) } @@ -102,7 +107,7 @@ class RecentAppsGestureMonitorTest : SysuiTestCase() { } private fun assertStateAfterEvents(events: List<MotionEvent>, expectedState: GestureState) { - events.forEach { gestureMonitor.processTouchpadEvent(it) } + events.forEach { gestureMonitor.accept(it) } assertThat(gestureState).isEqualTo(expectedState) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt index 4d26366614f6..9f7ea679b822 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt @@ -27,6 +27,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE import com.google.common.truth.Truth.assertThat +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -35,14 +36,14 @@ import org.junit.runner.RunWith class TouchpadGestureHandlerTest : SysuiTestCase() { private var gestureState: GestureState = GestureState.NotStarted - private val handler = - TouchpadGestureHandler( - BackGestureMonitor( - gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(), - gestureStateChangedCallback = { gestureState = it }, - ), - EasterEggGestureMonitor {}, - ) + private val gestureMonitor = + BackGestureMonitor(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt()) + private val handler = TouchpadGestureHandler(gestureMonitor, EasterEggGestureMonitor {}) + + @Before + fun before() { + gestureMonitor.addGestureStateCallback { gestureState = it } + } @Test fun handlesEventsFromTouchpad() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt index b8dd334dcad9..b8dd334dcad9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java index b1736b16875d..c09509d8690a 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java @@ -14,7 +14,6 @@ package com.android.systemui.plugins; -import android.annotation.IntegerRes; import android.content.ComponentName; import android.media.AudioManager; import android.media.AudioSystem; @@ -22,6 +21,8 @@ import android.os.Handler; import android.os.VibrationEffect; import android.util.SparseArray; +import androidx.annotation.StringRes; + import com.android.systemui.plugins.VolumeDialogController.Callbacks; import com.android.systemui.plugins.VolumeDialogController.State; import com.android.systemui.plugins.VolumeDialogController.StreamState; @@ -90,7 +91,7 @@ public interface VolumeDialogController { public int levelMax; public boolean muted; public boolean muteSupported; - public @IntegerRes int name; + public @StringRes int name; public String remoteLabel; public boolean routedToBluetooth; diff --git a/packages/SystemUI/res/drawable/volume_dialog_background.xml b/packages/SystemUI/res/drawable/volume_dialog_background.xml new file mode 100644 index 000000000000..7d7498feeba6 --- /dev/null +++ b/packages/SystemUI/res/drawable/volume_dialog_background.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<shape xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:shape="rectangle"> + <corners android:radius="@dimen/volume_dialog_background_corner_radius" /> + <solid android:color="?androidprv:attr/materialColorSurface" /> +</shape> diff --git a/packages/SystemUI/res/drawable/volume_dialog_floating_slider_background.xml b/packages/SystemUI/res/drawable/volume_dialog_floating_slider_background.xml new file mode 100644 index 000000000000..2694435bcc78 --- /dev/null +++ b/packages/SystemUI/res/drawable/volume_dialog_floating_slider_background.xml @@ -0,0 +1,21 @@ +<!-- + ~ 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. + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:shape="rectangle"> + <corners android:radius="20dp" /> + <solid android:color="?androidprv:attr/colorSurface" /> +</shape> diff --git a/packages/SystemUI/res/drawable/volume_dialog_floating_sliders_spacer.xml b/packages/SystemUI/res/drawable/volume_dialog_floating_sliders_spacer.xml new file mode 100644 index 000000000000..66a205a57c61 --- /dev/null +++ b/packages/SystemUI/res/drawable/volume_dialog_floating_sliders_spacer.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <size + android:width="@dimen/volume_dialog_floating_sliders_spacing" + android:height="@dimen/volume_dialog_floating_sliders_spacing" /> + <solid android:color="@color/transparent" /> +</shape> diff --git a/packages/SystemUI/res/drawable/volume_dialog_spacer.xml b/packages/SystemUI/res/drawable/volume_dialog_spacer.xml new file mode 100644 index 000000000000..3c60784cf6b6 --- /dev/null +++ b/packages/SystemUI/res/drawable/volume_dialog_spacer.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <size + android:width="@dimen/volume_dialog_spacing" + android:height="@dimen/volume_dialog_spacing" /> + <solid android:color="@color/transparent" /> +</shape> diff --git a/packages/SystemUI/res/drawable/volume_row_seekbar_progress.xml b/packages/SystemUI/res/drawable/volume_row_seekbar_progress.xml index 21b177bead11..fa06bd6abeea 100644 --- a/packages/SystemUI/res/drawable/volume_row_seekbar_progress.xml +++ b/packages/SystemUI/res/drawable/volume_row_seekbar_progress.xml @@ -22,7 +22,7 @@ android:autoMirrored="true"> <item android:id="@+id/volume_seekbar_progress_solid"> <shape> - <size android:height="@dimen/volume_dialog_slider_width" /> + <size android:height="@dimen/volume_dialog_slider_width_legacy" /> <solid android:color="?android:attr/colorAccent" /> <corners android:radius="@dimen/volume_dialog_slider_corner_radius"/> </shape> diff --git a/packages/SystemUI/res/layout-land-television/volume_dialog.xml b/packages/SystemUI/res/layout-land-television/volume_dialog.xml index 0fbc519ca8dd..f77db956a493 100644 --- a/packages/SystemUI/res/layout-land-television/volume_dialog.xml +++ b/packages/SystemUI/res/layout-land-television/volume_dialog.xml @@ -1,92 +1,67 @@ <!-- - ~ Copyright (C) 2020 The Android Open Source Project - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License - --> -<FrameLayout - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:sysui="http://schemas.android.com/apk/res-auto" + 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. +--> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:id="@+id/volume_dialog_container" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:background="@android:color/transparent" + android:layout_gravity="right" + android:divider="@drawable/volume_dialog_floating_sliders_spacer" + android:orientation="horizontal" + android:showDividers="middle|end|beginning" android:theme="@style/volume_dialog_theme"> - <FrameLayout - android:id="@+id/volume_dialog" + <LinearLayout + android:id="@+id/volume_dialog_floating_sliders_container" android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="right" - android:background="@android:color/transparent" - android:padding="@dimen/volume_dialog_panel_transparent_padding" - android:clipToPadding="false"> - - <LinearLayout - android:id="@+id/volume_dialog_rows_container" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="right" - android:orientation="vertical" - android:translationZ="@dimen/volume_dialog_elevation" - android:clipChildren="false" - android:clipToPadding="false" - android:background="@android:color/transparent"> + android:layout_height="match_parent" + android:background="@drawable/volume_dialog_background" + android:divider="@drawable/volume_dialog_floating_sliders_spacer" + android:gravity="bottom" + android:orientation="horizontal" + android:paddingBottom="@dimen/volume_dialog_floating_sliders_bottom_padding" + android:showDividers="middle" /> - <LinearLayout - android:id="@+id/volume_dialog_rows" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:gravity="center" - android:orientation="horizontal" - android:background="@drawable/tv_volume_dialog_background"> - <!-- volume rows added and removed here! :-) --> - </LinearLayout> - - </LinearLayout> + <LinearLayout + android:layout_width="@dimen/volume_dialog_width" + android:layout_height="wrap_content" + android:background="@drawable/volume_dialog_background" + android:divider="@drawable/volume_dialog_spacer" + android:gravity="center_horizontal" + android:orientation="vertical" + android:paddingVertical="@dimen/volume_dialog_vertical_padding" + android:showDividers="middle"> <FrameLayout - android:id="@+id/odi_captions" - android:layout_width="@dimen/volume_dialog_caption_size" - android:layout_height="@dimen/volume_dialog_caption_size" - android:layout_marginRight="68dp" - android:layout_gravity="right" - android:clipToPadding="false" - android:translationZ="@dimen/volume_dialog_elevation" - android:background="@drawable/rounded_bg_full"> - - <com.android.systemui.volume.CaptionsToggleImageButton - android:id="@+id/odi_captions_icon" - android:src="@drawable/ic_volume_odi_captions_disabled" - style="@style/VolumeButtons" - android:background="@drawable/rounded_ripple" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:tint="@color/caption_tint_color_selector" - android:layout_gravity="center" - android:soundEffectsEnabled="false"/> - - </FrameLayout> - - <ViewStub - android:id="@+id/odi_captions_tooltip_stub" - android:inflatedId="@+id/odi_captions_tooltip_view" - android:layout="@layout/volume_tool_tip_view" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginRight="@dimen/volume_tool_tip_right_margin" - android:layout_marginTop="@dimen/volume_tool_tip_top_margin" - android:layout_gravity="right"/> + android:id="@+id/volume_dialog_ringer_button" + android:layout_width="@dimen/volume_dialog_button_size" + android:layout_height="@dimen/volume_dialog_button_size" /> - </FrameLayout> + <include + android:id="@+id/volume_dialog_slider" + layout="@layout/volume_dialog_slider" /> -</FrameLayout> + <Button + android:id="@+id/volume_dialog_settings" + android:layout_width="@dimen/volume_dialog_button_size" + android:layout_height="@dimen/volume_dialog_button_size" + android:background="@drawable/ripple_drawable_20dp" + android:contentDescription="@string/accessibility_volume_settings" + android:soundEffectsEnabled="false" + android:src="@drawable/horizontal_ellipsis" + android:tint="?androidprv:attr/materialColorPrimary" /> + </LinearLayout> +</LinearLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout-land-television/volume_dialog_legacy.xml b/packages/SystemUI/res/layout-land-television/volume_dialog_legacy.xml new file mode 100644 index 000000000000..0fbc519ca8dd --- /dev/null +++ b/packages/SystemUI/res/layout-land-television/volume_dialog_legacy.xml @@ -0,0 +1,92 @@ +<!-- + ~ Copyright (C) 2020 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License + --> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:sysui="http://schemas.android.com/apk/res-auto" + android:id="@+id/volume_dialog_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@android:color/transparent" + android:theme="@style/volume_dialog_theme"> + + <FrameLayout + android:id="@+id/volume_dialog" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="right" + android:background="@android:color/transparent" + android:padding="@dimen/volume_dialog_panel_transparent_padding" + android:clipToPadding="false"> + + <LinearLayout + android:id="@+id/volume_dialog_rows_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="right" + android:orientation="vertical" + android:translationZ="@dimen/volume_dialog_elevation" + android:clipChildren="false" + android:clipToPadding="false" + android:background="@android:color/transparent"> + + <LinearLayout + android:id="@+id/volume_dialog_rows" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center" + android:orientation="horizontal" + android:background="@drawable/tv_volume_dialog_background"> + <!-- volume rows added and removed here! :-) --> + </LinearLayout> + + </LinearLayout> + + <FrameLayout + android:id="@+id/odi_captions" + android:layout_width="@dimen/volume_dialog_caption_size" + android:layout_height="@dimen/volume_dialog_caption_size" + android:layout_marginRight="68dp" + android:layout_gravity="right" + android:clipToPadding="false" + android:translationZ="@dimen/volume_dialog_elevation" + android:background="@drawable/rounded_bg_full"> + + <com.android.systemui.volume.CaptionsToggleImageButton + android:id="@+id/odi_captions_icon" + android:src="@drawable/ic_volume_odi_captions_disabled" + style="@style/VolumeButtons" + android:background="@drawable/rounded_ripple" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:tint="@color/caption_tint_color_selector" + android:layout_gravity="center" + android:soundEffectsEnabled="false"/> + + </FrameLayout> + + <ViewStub + android:id="@+id/odi_captions_tooltip_stub" + android:inflatedId="@+id/odi_captions_tooltip_view" + android:layout="@layout/volume_tool_tip_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginRight="@dimen/volume_tool_tip_right_margin" + android:layout_marginTop="@dimen/volume_tool_tip_top_margin" + android:layout_gravity="right"/> + + </FrameLayout> + +</FrameLayout> diff --git a/packages/SystemUI/res/layout-land-television/volume_dialog_row.xml b/packages/SystemUI/res/layout-land-television/volume_dialog_row.xml index cf301c96a9f8..eb89489bdd83 100644 --- a/packages/SystemUI/res/layout-land-television/volume_dialog_row.xml +++ b/packages/SystemUI/res/layout-land-television/volume_dialog_row.xml @@ -63,8 +63,8 @@ android:layout_height="match_parent" android:layout_gravity="center" android:layoutDirection="ltr" - android:maxHeight="@dimen/volume_dialog_slider_width" - android:minHeight="@dimen/volume_dialog_slider_width" + android:maxHeight="@dimen/volume_dialog_slider_width_legacy" + android:minHeight="@dimen/volume_dialog_slider_width_legacy" android:progressDrawable="@drawable/volume_row_seekbar" android:thumb="@drawable/tv_volume_row_seek_thumb" android:splitTrack="false" diff --git a/packages/SystemUI/res/layout-land/volume_dialog.xml b/packages/SystemUI/res/layout-land/volume_dialog.xml index 08edf59000b8..f77db956a493 100644 --- a/packages/SystemUI/res/layout-land/volume_dialog.xml +++ b/packages/SystemUI/res/layout-land/volume_dialog.xml @@ -1,146 +1,67 @@ <!-- - ~ Copyright (C) 2019 The Android Open Source Project - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License - --> -<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + 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. +--> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:id="@+id/volume_dialog_container" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:gravity="right" android:layout_gravity="right" - android:background="@android:color/transparent" + android:divider="@drawable/volume_dialog_floating_sliders_spacer" + android:orientation="horizontal" + android:showDividers="middle|end|beginning" android:theme="@style/volume_dialog_theme"> - <!-- right-aligned to be physically near volume button --> <LinearLayout - android:id="@+id/volume_dialog" + android:id="@+id/volume_dialog_floating_sliders_container" android:layout_width="wrap_content" + android:layout_height="match_parent" + android:background="@drawable/volume_dialog_background" + android:divider="@drawable/volume_dialog_floating_sliders_spacer" + android:gravity="bottom" + android:orientation="horizontal" + android:paddingBottom="@dimen/volume_dialog_floating_sliders_bottom_padding" + android:showDividers="middle" /> + + <LinearLayout + android:layout_width="@dimen/volume_dialog_width" android:layout_height="wrap_content" - android:gravity="right" - android:layout_gravity="right" - android:layout_marginRight="@dimen/volume_dialog_panel_transparent_padding_right" + android:background="@drawable/volume_dialog_background" + android:divider="@drawable/volume_dialog_spacer" + android:gravity="center_horizontal" android:orientation="vertical" - android:clipToPadding="false" - android:clipChildren="false"> - - - <LinearLayout - android:id="@+id/volume_dialog_top_container" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="vertical" - android:clipChildren="false" - android:gravity="right"> - - <include layout="@layout/volume_ringer_drawer" /> - - <FrameLayout - android:visibility="gone" - android:id="@+id/ringer" - android:layout_width="@dimen/volume_dialog_ringer_size" - android:layout_height="@dimen/volume_dialog_ringer_size" - android:layout_marginBottom="@dimen/volume_dialog_spacer" - android:gravity="right" - android:layout_gravity="right" - android:translationZ="@dimen/volume_dialog_elevation" - android:clipToPadding="false" - android:background="@drawable/rounded_bg_full"> - <com.android.keyguard.AlphaOptimizedImageButton - android:id="@+id/ringer_icon" - style="@style/VolumeButtons" - android:background="@drawable/rounded_ripple" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:scaleType="fitCenter" - android:padding="@dimen/volume_dialog_ringer_icon_padding" - android:tint="?android:attr/textColorPrimary" - android:layout_gravity="center" - android:soundEffectsEnabled="false" /> - </FrameLayout> - - <LinearLayout - android:id="@+id/volume_dialog_rows_container" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:gravity="right" - android:layout_gravity="right" - android:orientation="vertical" - android:clipChildren="false" - android:clipToPadding="false" > - <LinearLayout - android:id="@+id/volume_dialog_rows" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:gravity="center" - android:orientation="horizontal"> - <!-- volume rows added and removed here! :-) --> - </LinearLayout> - <FrameLayout - android:id="@+id/settings_container" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:background="@drawable/volume_background_bottom" - android:paddingLeft="@dimen/volume_dialog_ringer_rows_padding" - android:paddingBottom="@dimen/volume_dialog_ringer_rows_padding" - android:paddingRight="@dimen/volume_dialog_ringer_rows_padding"> - - <com.android.keyguard.AlphaOptimizedImageButton - android:id="@+id/settings" - android:layout_width="@dimen/volume_dialog_tap_target_size" - android:layout_height="@dimen/volume_dialog_tap_target_size" - android:layout_gravity="center" - android:background="@drawable/ripple_drawable_20dp" - android:contentDescription="@string/accessibility_volume_settings" - android:scaleType="centerInside" - android:soundEffectsEnabled="false" - android:src="@drawable/horizontal_ellipsis" - android:tint="?androidprv:attr/colorAccent" /> - </FrameLayout> - </LinearLayout> - - </LinearLayout> + android:paddingVertical="@dimen/volume_dialog_vertical_padding" + android:showDividers="middle"> <FrameLayout - android:id="@+id/odi_captions" - android:layout_width="@dimen/volume_dialog_caption_size" - android:layout_height="@dimen/volume_dialog_caption_size" - android:layout_marginTop="@dimen/volume_dialog_row_margin_bottom" - android:gravity="right" - android:layout_gravity="right" - android:clipToPadding="false" - android:clipToOutline="true" - android:background="@drawable/volume_row_rounded_background"> - <com.android.systemui.volume.CaptionsToggleImageButton - android:id="@+id/odi_captions_icon" - android:src="@drawable/ic_volume_odi_captions_disabled" - style="@style/VolumeButtons" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:tint="?android:attr/colorAccent" - android:layout_gravity="center" - android:soundEffectsEnabled="false" /> - </FrameLayout> - </LinearLayout> + android:id="@+id/volume_dialog_ringer_button" + android:layout_width="@dimen/volume_dialog_button_size" + android:layout_height="@dimen/volume_dialog_button_size" /> - <ViewStub - android:id="@+id/odi_captions_tooltip_stub" - android:inflatedId="@+id/odi_captions_tooltip_view" - android:layout="@layout/volume_tool_tip_view" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="bottom | right" - android:layout_marginRight="@dimen/volume_tool_tip_right_margin"/> + <include + android:id="@+id/volume_dialog_slider" + layout="@layout/volume_dialog_slider" /> -</FrameLayout>
\ No newline at end of file + <Button + android:id="@+id/volume_dialog_settings" + android:layout_width="@dimen/volume_dialog_button_size" + android:layout_height="@dimen/volume_dialog_button_size" + android:background="@drawable/ripple_drawable_20dp" + android:contentDescription="@string/accessibility_volume_settings" + android:soundEffectsEnabled="false" + android:src="@drawable/horizontal_ellipsis" + android:tint="?androidprv:attr/materialColorPrimary" /> + </LinearLayout> +</LinearLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout-land/volume_dialog_legacy.xml b/packages/SystemUI/res/layout-land/volume_dialog_legacy.xml new file mode 100644 index 000000000000..08edf59000b8 --- /dev/null +++ b/packages/SystemUI/res/layout-land/volume_dialog_legacy.xml @@ -0,0 +1,146 @@ +<!-- + ~ Copyright (C) 2019 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License + --> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:id="@+id/volume_dialog_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="right" + android:layout_gravity="right" + android:background="@android:color/transparent" + android:theme="@style/volume_dialog_theme"> + + <!-- right-aligned to be physically near volume button --> + <LinearLayout + android:id="@+id/volume_dialog" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="right" + android:layout_gravity="right" + android:layout_marginRight="@dimen/volume_dialog_panel_transparent_padding_right" + android:orientation="vertical" + android:clipToPadding="false" + android:clipChildren="false"> + + + <LinearLayout + android:id="@+id/volume_dialog_top_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + android:clipChildren="false" + android:gravity="right"> + + <include layout="@layout/volume_ringer_drawer" /> + + <FrameLayout + android:visibility="gone" + android:id="@+id/ringer" + android:layout_width="@dimen/volume_dialog_ringer_size" + android:layout_height="@dimen/volume_dialog_ringer_size" + android:layout_marginBottom="@dimen/volume_dialog_spacer" + android:gravity="right" + android:layout_gravity="right" + android:translationZ="@dimen/volume_dialog_elevation" + android:clipToPadding="false" + android:background="@drawable/rounded_bg_full"> + <com.android.keyguard.AlphaOptimizedImageButton + android:id="@+id/ringer_icon" + style="@style/VolumeButtons" + android:background="@drawable/rounded_ripple" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scaleType="fitCenter" + android:padding="@dimen/volume_dialog_ringer_icon_padding" + android:tint="?android:attr/textColorPrimary" + android:layout_gravity="center" + android:soundEffectsEnabled="false" /> + </FrameLayout> + + <LinearLayout + android:id="@+id/volume_dialog_rows_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="right" + android:layout_gravity="right" + android:orientation="vertical" + android:clipChildren="false" + android:clipToPadding="false" > + <LinearLayout + android:id="@+id/volume_dialog_rows" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center" + android:orientation="horizontal"> + <!-- volume rows added and removed here! :-) --> + </LinearLayout> + <FrameLayout + android:id="@+id/settings_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@drawable/volume_background_bottom" + android:paddingLeft="@dimen/volume_dialog_ringer_rows_padding" + android:paddingBottom="@dimen/volume_dialog_ringer_rows_padding" + android:paddingRight="@dimen/volume_dialog_ringer_rows_padding"> + + <com.android.keyguard.AlphaOptimizedImageButton + android:id="@+id/settings" + android:layout_width="@dimen/volume_dialog_tap_target_size" + android:layout_height="@dimen/volume_dialog_tap_target_size" + android:layout_gravity="center" + android:background="@drawable/ripple_drawable_20dp" + android:contentDescription="@string/accessibility_volume_settings" + android:scaleType="centerInside" + android:soundEffectsEnabled="false" + android:src="@drawable/horizontal_ellipsis" + android:tint="?androidprv:attr/colorAccent" /> + </FrameLayout> + </LinearLayout> + + </LinearLayout> + + <FrameLayout + android:id="@+id/odi_captions" + android:layout_width="@dimen/volume_dialog_caption_size" + android:layout_height="@dimen/volume_dialog_caption_size" + android:layout_marginTop="@dimen/volume_dialog_row_margin_bottom" + android:gravity="right" + android:layout_gravity="right" + android:clipToPadding="false" + android:clipToOutline="true" + android:background="@drawable/volume_row_rounded_background"> + <com.android.systemui.volume.CaptionsToggleImageButton + android:id="@+id/odi_captions_icon" + android:src="@drawable/ic_volume_odi_captions_disabled" + style="@style/VolumeButtons" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:tint="?android:attr/colorAccent" + android:layout_gravity="center" + android:soundEffectsEnabled="false" /> + </FrameLayout> + </LinearLayout> + + <ViewStub + android:id="@+id/odi_captions_tooltip_stub" + android:inflatedId="@+id/odi_captions_tooltip_view" + android:layout="@layout/volume_tool_tip_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="bottom | right" + android:layout_marginRight="@dimen/volume_tool_tip_right_margin"/> + +</FrameLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/audio_sharing_dialog.xml b/packages/SystemUI/res/layout/audio_sharing_dialog.xml new file mode 100644 index 000000000000..7534e159beb0 --- /dev/null +++ b/packages/SystemUI/res/layout/audio_sharing_dialog.xml @@ -0,0 +1,115 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<androidx.constraintlayout.widget.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/root" + style="@style/Widget.SliceView.Panel" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <ImageView android:id="@+id/icon" + android:layout_width="28dp" + android:layout_height="28dp" + android:src="@drawable/ic_bt_le_audio_sharing" + android:layout_marginTop="5dp" + android:layout_marginBottom="20dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintBottom_toTopOf="@id/title" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="20dp" + android:gravity="center_vertical|center_horizontal" + android:maxLines="1" + android:ellipsize="end" + android:text="@string/quick_settings_bluetooth_audio_sharing_dialog_title" + android:textAppearance="@style/TextAppearance.Dialog.Title" + android:textSize="24sp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintBottom_toTopOf="@id/subtitle" + app:layout_constraintTop_toBottomOf="@id/icon" /> + + <TextView + android:id="@+id/subtitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="20dp" + android:gravity="center_vertical|center_horizontal" + android:ellipsize="end" + android:maxLines="2" + android:textAppearance="@style/TextAppearance.Dialog.Body.Message" + android:textFontWeight="500" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintBottom_toTopOf="@id/message" + app:layout_constraintTop_toBottomOf="@id/title" /> + + <TextView + android:id="@+id/message" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="20dp" + android:gravity="center_vertical|center_horizontal" + android:ellipsize="end" + android:maxLines="2" + android:text="@string/quick_settings_bluetooth_audio_sharing_dialog_message" + android:textAppearance="@style/TextAppearance.Dialog.Body.Message" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintBottom_toTopOf="@id/share_audio_button" + app:layout_constraintTop_toBottomOf="@id/subtitle" /> + + <Button + android:id="@+id/share_audio_button" + style="@style/SettingsLibActionButton" + android:textColor="?androidprv:attr/textColorOnAccent" + android:background="@drawable/audio_sharing_rounded_bg_ripple" + android:layout_marginBottom="4dp" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:minHeight="64dp" + android:contentDescription="@string/accessibility_bluetooth_device_settings_see_all" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@+id/message" + app:layout_constraintBottom_toTopOf="@+id/switch_active_button" + android:text="@string/quick_settings_bluetooth_audio_sharing_button" + android:maxLines="2" /> + + <Button + android:id="@+id/switch_active_button" + style="@style/SettingsLibActionButton" + android:textColor="?androidprv:attr/textColorOnAccent" + android:background="@drawable/audio_sharing_rounded_bg_ripple" + android:layout_marginBottom="20dp" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:minHeight="64dp" + android:contentDescription="@string/accessibility_bluetooth_device_settings_pair_new_device" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@+id/share_audio_button" + app:layout_constraintBottom_toBottomOf="parent" + android:maxLines="2" /> +</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml index 39a1f1f9b85d..f77db956a493 100644 --- a/packages/SystemUI/res/layout/volume_dialog.xml +++ b/packages/SystemUI/res/layout/volume_dialog.xml @@ -1,5 +1,5 @@ <!-- - Copyright (C) 2015 The Android Open Source Project + 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. @@ -13,133 +13,55 @@ See the License for the specific language governing permissions and limitations under the License. --> -<FrameLayout - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:sysui="http://schemas.android.com/apk/res-auto" +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:id="@+id/volume_dialog_container" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:gravity="right" android:layout_gravity="right" - android:clipToPadding="false" + android:divider="@drawable/volume_dialog_floating_sliders_spacer" + android:orientation="horizontal" + android:showDividers="middle|end|beginning" android:theme="@style/volume_dialog_theme"> - <!-- right-aligned to be physically near volume button --> <LinearLayout - android:id="@+id/volume_dialog" + android:id="@+id/volume_dialog_floating_sliders_container" android:layout_width="wrap_content" + android:layout_height="match_parent" + android:background="@drawable/volume_dialog_background" + android:divider="@drawable/volume_dialog_floating_sliders_spacer" + android:gravity="bottom" + android:orientation="horizontal" + android:paddingBottom="@dimen/volume_dialog_floating_sliders_bottom_padding" + android:showDividers="middle" /> + + <LinearLayout + android:layout_width="@dimen/volume_dialog_width" android:layout_height="wrap_content" - android:gravity="right" - android:layout_gravity="right" - android:layout_marginRight="@dimen/volume_dialog_panel_transparent_padding_right" + android:background="@drawable/volume_dialog_background" + android:divider="@drawable/volume_dialog_spacer" + android:gravity="center_horizontal" android:orientation="vertical" - android:clipToPadding="false" - android:clipChildren="false"> - - <LinearLayout - android:id="@+id/volume_dialog_top_container" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:clipChildren="false" - android:orientation="vertical" - android:gravity="right"> - - <include layout="@layout/volume_ringer_drawer" /> - - <FrameLayout - android:visibility="gone" - android:id="@+id/ringer" - android:layout_width="@dimen/volume_dialog_ringer_size" - android:layout_height="@dimen/volume_dialog_ringer_size" - android:layout_marginBottom="@dimen/volume_dialog_spacer" - android:gravity="right" - android:layout_gravity="right" - android:translationZ="@dimen/volume_dialog_elevation" - android:clipToPadding="false" - android:background="@drawable/rounded_bg_full"> - <com.android.keyguard.AlphaOptimizedImageButton - android:id="@+id/ringer_icon" - style="@style/VolumeButtons" - android:background="@drawable/rounded_ripple" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:scaleType="fitCenter" - android:padding="@dimen/volume_dialog_ringer_icon_padding" - android:tint="?android:attr/textColorPrimary" - android:layout_gravity="center" - android:soundEffectsEnabled="false" /> - </FrameLayout> - - <LinearLayout - android:id="@+id/volume_dialog_rows_container" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:gravity="right" - android:layout_gravity="right" - android:orientation="vertical" - android:clipChildren="false" - android:clipToPadding="false" > - <LinearLayout - android:id="@+id/volume_dialog_rows" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:gravity="center" - android:orientation="horizontal"> - <!-- volume rows added and removed here! :-) --> - </LinearLayout> - <FrameLayout - android:id="@+id/settings_container" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:background="@drawable/volume_background_bottom" - android:paddingLeft="@dimen/volume_dialog_ringer_rows_padding" - android:paddingBottom="@dimen/volume_dialog_ringer_rows_padding" - android:paddingRight="@dimen/volume_dialog_ringer_rows_padding"> - <com.android.keyguard.AlphaOptimizedImageButton - android:id="@+id/settings" - android:src="@drawable/horizontal_ellipsis" - android:layout_width="@dimen/volume_dialog_tap_target_size" - android:layout_height="@dimen/volume_dialog_tap_target_size" - android:layout_gravity="center" - android:contentDescription="@string/accessibility_volume_settings" - android:background="@drawable/ripple_drawable_20dp" - android:tint="?androidprv:attr/colorAccent" - android:soundEffectsEnabled="false" /> - </FrameLayout> - </LinearLayout> - - </LinearLayout> + android:paddingVertical="@dimen/volume_dialog_vertical_padding" + android:showDividers="middle"> <FrameLayout - android:id="@+id/odi_captions" - android:layout_width="@dimen/volume_dialog_caption_size" - android:layout_height="@dimen/volume_dialog_caption_size" - android:layout_marginTop="@dimen/volume_dialog_row_margin_bottom" - android:gravity="right" - android:layout_gravity="right" - android:clipToPadding="false" - android:clipToOutline="true" - android:background="@drawable/volume_row_rounded_background"> - <com.android.systemui.volume.CaptionsToggleImageButton - android:id="@+id/odi_captions_icon" - android:src="@drawable/ic_volume_odi_captions_disabled" - style="@style/VolumeButtons" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:tint="?android:attr/colorAccent" - android:layout_gravity="center" - android:soundEffectsEnabled="false"/> - </FrameLayout> - </LinearLayout> + android:id="@+id/volume_dialog_ringer_button" + android:layout_width="@dimen/volume_dialog_button_size" + android:layout_height="@dimen/volume_dialog_button_size" /> - <ViewStub - android:id="@+id/odi_captions_tooltip_stub" - android:inflatedId="@+id/odi_captions_tooltip_view" - android:layout="@layout/volume_tool_tip_view" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="bottom | right" - android:layout_marginRight="@dimen/volume_tool_tip_right_margin"/> + <include + android:id="@+id/volume_dialog_slider" + layout="@layout/volume_dialog_slider" /> -</FrameLayout>
\ No newline at end of file + <Button + android:id="@+id/volume_dialog_settings" + android:layout_width="@dimen/volume_dialog_button_size" + android:layout_height="@dimen/volume_dialog_button_size" + android:background="@drawable/ripple_drawable_20dp" + android:contentDescription="@string/accessibility_volume_settings" + android:soundEffectsEnabled="false" + android:src="@drawable/horizontal_ellipsis" + android:tint="?androidprv:attr/materialColorPrimary" /> + </LinearLayout> +</LinearLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/volume_dialog_legacy.xml b/packages/SystemUI/res/layout/volume_dialog_legacy.xml new file mode 100644 index 000000000000..39a1f1f9b85d --- /dev/null +++ b/packages/SystemUI/res/layout/volume_dialog_legacy.xml @@ -0,0 +1,145 @@ +<!-- + Copyright (C) 2015 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:sysui="http://schemas.android.com/apk/res-auto" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:id="@+id/volume_dialog_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="right" + android:layout_gravity="right" + android:clipToPadding="false" + android:theme="@style/volume_dialog_theme"> + + <!-- right-aligned to be physically near volume button --> + <LinearLayout + android:id="@+id/volume_dialog" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="right" + android:layout_gravity="right" + android:layout_marginRight="@dimen/volume_dialog_panel_transparent_padding_right" + android:orientation="vertical" + android:clipToPadding="false" + android:clipChildren="false"> + + <LinearLayout + android:id="@+id/volume_dialog_top_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:clipChildren="false" + android:orientation="vertical" + android:gravity="right"> + + <include layout="@layout/volume_ringer_drawer" /> + + <FrameLayout + android:visibility="gone" + android:id="@+id/ringer" + android:layout_width="@dimen/volume_dialog_ringer_size" + android:layout_height="@dimen/volume_dialog_ringer_size" + android:layout_marginBottom="@dimen/volume_dialog_spacer" + android:gravity="right" + android:layout_gravity="right" + android:translationZ="@dimen/volume_dialog_elevation" + android:clipToPadding="false" + android:background="@drawable/rounded_bg_full"> + <com.android.keyguard.AlphaOptimizedImageButton + android:id="@+id/ringer_icon" + style="@style/VolumeButtons" + android:background="@drawable/rounded_ripple" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scaleType="fitCenter" + android:padding="@dimen/volume_dialog_ringer_icon_padding" + android:tint="?android:attr/textColorPrimary" + android:layout_gravity="center" + android:soundEffectsEnabled="false" /> + </FrameLayout> + + <LinearLayout + android:id="@+id/volume_dialog_rows_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="right" + android:layout_gravity="right" + android:orientation="vertical" + android:clipChildren="false" + android:clipToPadding="false" > + <LinearLayout + android:id="@+id/volume_dialog_rows" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center" + android:orientation="horizontal"> + <!-- volume rows added and removed here! :-) --> + </LinearLayout> + <FrameLayout + android:id="@+id/settings_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@drawable/volume_background_bottom" + android:paddingLeft="@dimen/volume_dialog_ringer_rows_padding" + android:paddingBottom="@dimen/volume_dialog_ringer_rows_padding" + android:paddingRight="@dimen/volume_dialog_ringer_rows_padding"> + <com.android.keyguard.AlphaOptimizedImageButton + android:id="@+id/settings" + android:src="@drawable/horizontal_ellipsis" + android:layout_width="@dimen/volume_dialog_tap_target_size" + android:layout_height="@dimen/volume_dialog_tap_target_size" + android:layout_gravity="center" + android:contentDescription="@string/accessibility_volume_settings" + android:background="@drawable/ripple_drawable_20dp" + android:tint="?androidprv:attr/colorAccent" + android:soundEffectsEnabled="false" /> + </FrameLayout> + </LinearLayout> + + </LinearLayout> + + <FrameLayout + android:id="@+id/odi_captions" + android:layout_width="@dimen/volume_dialog_caption_size" + android:layout_height="@dimen/volume_dialog_caption_size" + android:layout_marginTop="@dimen/volume_dialog_row_margin_bottom" + android:gravity="right" + android:layout_gravity="right" + android:clipToPadding="false" + android:clipToOutline="true" + android:background="@drawable/volume_row_rounded_background"> + <com.android.systemui.volume.CaptionsToggleImageButton + android:id="@+id/odi_captions_icon" + android:src="@drawable/ic_volume_odi_captions_disabled" + style="@style/VolumeButtons" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:tint="?android:attr/colorAccent" + android:layout_gravity="center" + android:soundEffectsEnabled="false"/> + </FrameLayout> + </LinearLayout> + + <ViewStub + android:id="@+id/odi_captions_tooltip_stub" + android:inflatedId="@+id/odi_captions_tooltip_view" + android:layout="@layout/volume_tool_tip_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="bottom | right" + android:layout_marginRight="@dimen/volume_tool_tip_right_margin"/> + +</FrameLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/volume_dialog_slider.xml b/packages/SystemUI/res/layout/volume_dialog_slider.xml new file mode 100644 index 000000000000..8acdd39faaa2 --- /dev/null +++ b/packages/SystemUI/res/layout/volume_dialog_slider.xml @@ -0,0 +1,27 @@ +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="@dimen/volume_dialog_slider_width" + android:layout_height="@dimen/volume_dialog_slider_height"> + + <com.google.android.material.slider.Slider + android:id="@+id/volume_dialog_slider" + android:layout_width="@dimen/volume_dialog_slider_height" + android:layout_height="match_parent" + android:layout_gravity="center" + android:rotation="270" + android:theme="@style/Theme.MaterialComponents.DayNight" /> +</FrameLayout> diff --git a/packages/SystemUI/res/layout/volume_dialog_slider_floating.xml b/packages/SystemUI/res/layout/volume_dialog_slider_floating.xml new file mode 100644 index 000000000000..db800aa4a873 --- /dev/null +++ b/packages/SystemUI/res/layout/volume_dialog_slider_floating.xml @@ -0,0 +1,24 @@ +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@drawable/volume_dialog_floating_slider_background" + android:paddingHorizontal="@dimen/volume_dialog_floating_sliders_horizontal_padding" + android:paddingVertical="@dimen/volume_dialog_floating_sliders_vertical_padding"> + + <include layout="@layout/volume_dialog_slider" /> +</FrameLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 1727a5fec0a2..6c8a7403953e 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -591,7 +591,7 @@ <dimen name="volume_dialog_panel_width_half">28dp</dimen> - <dimen name="volume_dialog_slider_width">42dp</dimen> + <dimen name="volume_dialog_slider_width_legacy">42dp</dimen> <dimen name="volume_dialog_slider_corner_radius">21dp</dimen> @@ -622,10 +622,6 @@ <dimen name="volume_tool_tip_arrow_corner_radius">2dp</dimen> - <!-- Volume panel slices dimensions --> - <dimen name="volume_panel_slice_vertical_padding">8dp</dimen> - <dimen name="volume_panel_slice_horizontal_padding">24dp</dimen> - <dimen name="bottom_sheet_corner_radius">28dp</dimen> <!-- Size of each item in the ringer selector drawer. --> @@ -2050,4 +2046,22 @@ <dimen name="contextual_edu_dialog_bottom_margin">80dp</dimen> <dimen name="contextual_edu_dialog_elevation">2dp</dimen> + + <!-- Volume start --> + <dimen name="volume_dialog_background_corner_radius">30dp</dimen> + <dimen name="volume_dialog_width">60dp</dimen> + <dimen name="volume_dialog_vertical_padding">6dp</dimen> + <dimen name="volume_dialog_components_spacing">8dp</dimen> + <dimen name="volume_dialog_floating_sliders_spacing">8dp</dimen> + <dimen name="volume_dialog_floating_sliders_vertical_padding">10dp</dimen> + <dimen name="volume_dialog_floating_sliders_horizontal_padding">4dp</dimen> + <dimen name="volume_dialog_spacing">4dp</dimen> + <dimen name="volume_dialog_button_size">48dp</dimen> + <dimen name="volume_dialog_floating_sliders_bottom_padding">48dp</dimen> + <dimen name="volume_dialog_slider_width">52dp</dimen> + <dimen name="volume_dialog_slider_height">254dp</dimen> + + <dimen name="volume_panel_slice_vertical_padding">8dp</dimen> + <dimen name="volume_panel_slice_horizontal_padding">24dp</dimen> + <!-- Volume end --> </resources> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 72250610d768..2c5fb5667db7 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -765,6 +765,14 @@ <string name="quick_settings_bluetooth_audio_sharing_button_sharing">Sharing audio</string> <!-- QuickSettings: Bluetooth dialog audio sharing button text accessibility label. Used as part of the string "Double tap to enter audio sharing settings". [CHAR LIMIT=50]--> <string name="quick_settings_bluetooth_audio_sharing_button_accessibility">enter audio sharing settings</string> + <!-- QuickSettings: Bluetooth audio sharing dialog message. [CHAR LIMIT=NONE]--> + <string name="quick_settings_bluetooth_audio_sharing_dialog_message">This device\'s music and videos will play on both pairs of headphones</string> + <!-- QuickSettings: Bluetooth audio sharing dialog title. [CHAR LIMIT=NONE]--> + <string name="quick_settings_bluetooth_audio_sharing_dialog_title">Share your audio</string> + <!-- QuickSettings: Bluetooth audio sharing dialog subtitle. [CHAR LIMIT=NONE]--> + <string name="quick_settings_bluetooth_audio_sharing_dialog_subtitle"><xliff:g id="available_device_name" example="device 1">%1$s</xliff:g> and <xliff:g id="active_device_name" example="device 2">%2$s</xliff:g></string> + <!-- QuickSettings: Bluetooth audio sharing dialog button text. [CHAR LIMIT=NONE]--> + <string name="quick_settings_bluetooth_audio_sharing_dialog_switch_to_button">Switch to <xliff:g id="available_device_name" example="device 1">%1$s</xliff:g></string> <!-- QuickSettings: Bluetooth secondary label for the battery level of a connected device [CHAR LIMIT=20]--> <string name="quick_settings_bluetooth_secondary_label_battery_level"><xliff:g id="battery_level_as_percentage">%s</xliff:g> battery</string> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 431f04882c8d..83ab5245bc31 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -1875,7 +1875,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab if (posture == DEVICE_POSTURE_OPENED) { mLogger.d("Posture changed to open - attempting to request active" + " unlock and run face auth"); - getFaceAuthInteractor().onDeviceUnfolded(); + if (getFaceAuthInteractor() != null) { + getFaceAuthInteractor().onDeviceUnfolded(); + } requestActiveUnlockFromWakeReason(PowerManager.WAKE_REASON_UNFOLD_DEVICE, false); } diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java index 831543da3237..ef172a1b24f6 100644 --- a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java +++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java @@ -69,7 +69,9 @@ public abstract class ClockRegistryModule { layoutInflater, resources, featureFlags.isEnabled(Flags.STEP_CLOCK_ANIMATION), - MigrateClocksToBlueprint.isEnabled()), + MigrateClocksToBlueprint.isEnabled(), + com.android.systemui.Flags.clockReactiveVariants() + ), context.getString(R.string.lockscreen_clock_id_fallback), clockBuffers, /* keepAllLoaded = */ false, diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModel.kt new file mode 100644 index 000000000000..a6fb1502ed19 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModel.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.bluetooth.qsdialog + +import androidx.annotation.StringRes +import com.android.settingslib.bluetooth.BluetoothUtils +import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.res.R +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine + +sealed class AudioSharingButtonState { + object Gone : AudioSharingButtonState() + + data class Visible(@StringRes val resId: Int, val isActive: Boolean) : + AudioSharingButtonState() +} + +class AudioSharingButtonViewModel +@AssistedInject +constructor( + private val localBluetoothManager: LocalBluetoothManager?, + private val audioSharingInteractor: AudioSharingInteractor, + private val bluetoothStateInteractor: BluetoothStateInteractor, + private val deviceItemInteractor: DeviceItemInteractor, +) : ExclusiveActivatable() { + + private val mutableButtonState = + MutableStateFlow<AudioSharingButtonState>(AudioSharingButtonState.Gone) + /** Flow representing the update of AudioSharingButtonState. */ + val audioSharingButtonStateUpdate: StateFlow<AudioSharingButtonState> = + mutableButtonState.asStateFlow() + + override suspend fun onActivated(): Nothing { + combine( + bluetoothStateInteractor.bluetoothStateUpdate, + deviceItemInteractor.deviceItemUpdate, + audioSharingInteractor.isAudioSharingOn + ) { bluetoothState, deviceItem, audioSharingOn -> + getButtonState(bluetoothState, deviceItem, audioSharingOn) + } + .collect { mutableButtonState.value = it } + awaitCancellation() + } + + private fun getButtonState( + bluetoothState: Boolean, + deviceItem: List<DeviceItem>, + audioSharingOn: Boolean + ): AudioSharingButtonState { + return when { + // Don't show button when bluetooth is off + !bluetoothState -> AudioSharingButtonState.Gone + // Show sharing audio when broadcasting + audioSharingOn -> + AudioSharingButtonState.Visible( + R.string.quick_settings_bluetooth_audio_sharing_button_sharing, + isActive = true + ) + // When not broadcasting, don't show button if there's connected source in any device + deviceItem.any { + BluetoothUtils.hasConnectedBroadcastSource( + it.cachedBluetoothDevice, + localBluetoothManager + ) + } -> AudioSharingButtonState.Gone + // Show audio sharing when there's a connected LE audio device + deviceItem.any { BluetoothUtils.isActiveLeAudioDevice(it.cachedBluetoothDevice) } -> + AudioSharingButtonState.Visible( + R.string.quick_settings_bluetooth_audio_sharing_button, + isActive = false + ) + else -> AudioSharingButtonState.Gone + } + } + + @AssistedFactory + interface Factory { + fun create(): AudioSharingButtonViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt new file mode 100644 index 000000000000..692a78be075f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt @@ -0,0 +1,152 @@ +/* + * 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.bluetooth.qsdialog + +import android.bluetooth.BluetoothDevice +import android.content.Intent +import android.os.Bundle +import android.provider.Settings +import com.android.internal.logging.UiEventLogger +import com.android.settingslib.bluetooth.A2dpProfile +import com.android.settingslib.bluetooth.BluetoothUtils +import com.android.settingslib.bluetooth.HeadsetProfile +import com.android.settingslib.bluetooth.HearingAidProfile +import com.android.settingslib.bluetooth.LeAudioProfile +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast +import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.settingslib.flags.Flags.audioSharingQsDialogImprovement +import com.android.systemui.animation.DialogTransitionAnimator +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.statusbar.phone.SystemUIDialog +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext + +@SysUISingleton +class AudioSharingDeviceItemActionInteractorImpl +@Inject +constructor( + private val activityStarter: ActivityStarter, + private val audioSharingInteractor: AudioSharingInteractor, + private val dialogTransitionAnimator: DialogTransitionAnimator, + private val localBluetoothManager: LocalBluetoothManager?, + @Main private val mainDispatcher: CoroutineDispatcher, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val logger: BluetoothTileDialogLogger, + private val uiEventLogger: UiEventLogger, + private val delegateFactory: AudioSharingDialogDelegate.Factory, + private val deviceItemActionInteractorImpl: DeviceItemActionInteractorImpl, +) : DeviceItemActionInteractor { + + override suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) { + withContext(backgroundDispatcher) { + if (!audioSharingInteractor.audioSharingAvailable()) { + return@withContext deviceItemActionInteractorImpl.onClick(deviceItem, dialog) + } + val inAudioSharing = BluetoothUtils.isBroadcasting(localBluetoothManager) + logger.logDeviceClickInAudioSharingWhenEnabled(inAudioSharing) + + when { + deviceItem.type == + DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> { + if (audioSharingQsDialogImprovement()) { + withContext(mainDispatcher) { + delegateFactory + .create(deviceItem.cachedBluetoothDevice) + .createDialog() + .let { dialogTransitionAnimator.showFromDialog(it, dialog) } + } + } else { + launchSettings(deviceItem.cachedBluetoothDevice.device, dialog) + logger.logLaunchSettingsCriteriaMatched( + "AvailableAudioSharingDeviceClicked", + deviceItem, + ) + } + uiEventLogger.log( + BluetoothTileDialogUiEvent.AVAILABLE_AUDIO_SHARING_DEVICE_CLICKED + ) + } + inSharingAndDeviceNoSource(inAudioSharing, deviceItem) -> { + launchSettings(deviceItem.cachedBluetoothDevice.device, dialog) + logger.logLaunchSettingsCriteriaMatched("InSharingClickedNoSource", deviceItem) + uiEventLogger.log( + if (deviceItem.isLeAudioSupported) + BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_IN_SHARING_LE_DEVICE_CLICKED + else + BluetoothTileDialogUiEvent + .LAUNCH_SETTINGS_IN_SHARING_NON_LE_DEVICE_CLICKED + ) + } + else -> { + deviceItemActionInteractorImpl.onClick(deviceItem, dialog) + } + } + } + } + + private fun inSharingAndDeviceNoSource( + inAudioSharing: Boolean, + deviceItem: DeviceItem, + ): Boolean { + return inAudioSharing && + deviceItem.isMediaDevice && + !BluetoothUtils.hasConnectedBroadcastSource( + deviceItem.cachedBluetoothDevice, + localBluetoothManager, + ) + } + + private fun launchSettings(device: BluetoothDevice, dialog: SystemUIDialog) { + val intent = + Intent(Settings.ACTION_BLUETOOTH_SETTINGS).apply { + putExtra( + EXTRA_SHOW_FRAGMENT_ARGUMENTS, + Bundle().apply { + putParcelable(LocalBluetoothLeBroadcast.EXTRA_BLUETOOTH_DEVICE, device) + }, + ) + } + intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK + activityStarter.postStartActivityDismissingKeyguard( + intent, + 0, + dialogTransitionAnimator.createActivityTransitionController(dialog), + ) + } + + private companion object { + const val EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args" + + val DeviceItem.isLeAudioSupported: Boolean + get() = + cachedBluetoothDevice.profiles.any { profile -> + profile is LeAudioProfile && profile.isEnabled(cachedBluetoothDevice.device) + } + + val DeviceItem.isMediaDevice: Boolean + get() = + cachedBluetoothDevice.uiAccessibleProfiles.any { + it is A2dpProfile || + it is HearingAidProfile || + it is LeAudioProfile || + it is HeadsetProfile + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegate.kt new file mode 100644 index 000000000000..3ac942b769f4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegate.kt @@ -0,0 +1,88 @@ +/* + * 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.bluetooth.qsdialog + +import android.os.Bundle +import android.widget.Button +import android.widget.TextView +import com.android.internal.logging.UiEventLogger +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.res.R +import com.android.systemui.statusbar.phone.SystemUIDialog +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +class AudioSharingDialogDelegate +@AssistedInject +constructor( + @Assisted private val cachedBluetoothDevice: CachedBluetoothDevice, + @Application private val coroutineScope: CoroutineScope, + private val viewModelFactory: AudioSharingDialogViewModel.Factory, + private val sysuiDialogFactory: SystemUIDialog.Factory, + private val uiEventLogger: UiEventLogger, +) : SystemUIDialog.Delegate { + + override fun createDialog(): SystemUIDialog = sysuiDialogFactory.create(this) + + override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { + with(dialog.layoutInflater.inflate(R.layout.audio_sharing_dialog, null)) { + dialog.setView(this) + val subtitleTextView = requireViewById<TextView>(R.id.subtitle) + val shareAudioButton = requireViewById<TextView>(R.id.share_audio_button) + val switchActiveButton = requireViewById<Button>(R.id.switch_active_button) + val job = + coroutineScope.launch { + val viewModel = viewModelFactory.create(cachedBluetoothDevice, this) + viewModel.dialogState.collect { + when (it) { + is AudioSharingDialogState.Hide -> dialog.dismiss() + is AudioSharingDialogState.Show -> { + subtitleTextView.text = it.subtitle + switchActiveButton.text = it.switchButtonText + switchActiveButton.setOnClickListener { + viewModel.switchActiveClicked() + uiEventLogger.log( + BluetoothTileDialogUiEvent + .AUDIO_SHARING_DIALOG_SWITCH_ACTIVE_CLICKED + ) + dialog.dismiss() + } + shareAudioButton.setOnClickListener { + viewModel.shareAudioClicked() + uiEventLogger.log( + BluetoothTileDialogUiEvent + .AUDIO_SHARING_DIALOG_SHARE_AUDIO_CLICKED + ) + dialog.dismiss() + } + } + } + } + } + SystemUIDialog.registerDismissListener(dialog) { job.cancel() } + } + } + + @AssistedFactory + interface Factory { + fun create(cachedBluetoothDevice: CachedBluetoothDevice): AudioSharingDialogDelegate + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModel.kt new file mode 100644 index 000000000000..dc970aea7c41 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModel.kt @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.bluetooth.qsdialog + +import android.content.Context +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.res.R +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch + +sealed class AudioSharingDialogState { + data object Hide : AudioSharingDialogState() + + data class Show(val subtitle: String, val switchButtonText: String) : AudioSharingDialogState() +} + +class AudioSharingDialogViewModel +@AssistedInject +constructor( + deviceItemInteractor: DeviceItemInteractor, + private val audioSharingInteractor: AudioSharingInteractor, + private val context: Context, + private val localBluetoothManager: LocalBluetoothManager?, + @Assisted private val cachedBluetoothDevice: CachedBluetoothDevice, + @Assisted private val coroutineScope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, +) { + val dialogState: Flow<AudioSharingDialogState> = + deviceItemInteractor.deviceItemUpdateRequest + .map { + if ( + audioSharingInteractor.isAvailableAudioSharingMediaBluetoothDevice( + cachedBluetoothDevice + ) + ) { + createShowState(cachedBluetoothDevice) + } else { + AudioSharingDialogState.Hide + } + } + .onStart { emit(createShowState(cachedBluetoothDevice)) } + .flowOn(backgroundDispatcher) + .distinctUntilChanged() + + fun switchActiveClicked() { + coroutineScope.launch { audioSharingInteractor.switchActive(cachedBluetoothDevice) } + } + + fun shareAudioClicked() { + coroutineScope.launch { audioSharingInteractor.startAudioSharing() } + } + + private fun createShowState( + cachedBluetoothDevice: CachedBluetoothDevice + ): AudioSharingDialogState { + val activeDeviceName = + localBluetoothManager + ?.profileManager + ?.leAudioProfile + ?.activeDevices + ?.firstOrNull() + ?.let { localBluetoothManager.cachedDeviceManager?.findDevice(it)?.name } ?: "" + val availableDeviceName = cachedBluetoothDevice.name + return AudioSharingDialogState.Show( + context.getString( + R.string.quick_settings_bluetooth_audio_sharing_dialog_subtitle, + availableDeviceName, + activeDeviceName + ), + context.getString( + R.string.quick_settings_bluetooth_audio_sharing_dialog_switch_to_button, + availableDeviceName + ) + ) + } + + @AssistedFactory + interface Factory { + fun create( + cachedBluetoothDevice: CachedBluetoothDevice, + coroutineScope: CoroutineScope + ): AudioSharingDialogViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt index 817f2d7f6f70..65f110533573 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt @@ -16,82 +16,148 @@ package com.android.systemui.bluetooth.qsdialog -import androidx.annotation.StringRes import com.android.settingslib.bluetooth.BluetoothUtils +import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.settingslib.bluetooth.onPlaybackStarted import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.res.R import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.withContext -internal sealed class AudioSharingButtonState { - object Gone : AudioSharingButtonState() +/** Holds business logic for the audio sharing state. */ +interface AudioSharingInteractor { + val isAudioSharingOn: Flow<Boolean> + + val audioSourceStateUpdate: Flow<Unit> + + suspend fun handleAudioSourceWhenReady() + + suspend fun isAvailableAudioSharingMediaBluetoothDevice( + cachedBluetoothDevice: CachedBluetoothDevice + ): Boolean + + suspend fun switchActive(cachedBluetoothDevice: CachedBluetoothDevice) - data class Visible(@StringRes val resId: Int, val isActive: Boolean) : - AudioSharingButtonState() + suspend fun startAudioSharing() + + suspend fun audioSharingAvailable(): Boolean } -/** Holds business logic for the audio sharing state. */ @SysUISingleton -internal class AudioSharingInteractor +@OptIn(ExperimentalCoroutinesApi::class) +class AudioSharingInteractorImpl @Inject constructor( private val localBluetoothManager: LocalBluetoothManager?, - bluetoothStateInteractor: BluetoothStateInteractor, - deviceItemInteractor: DeviceItemInteractor, - @Application private val coroutineScope: CoroutineScope, + private val audioSharingRepository: AudioSharingRepository, @Background private val backgroundDispatcher: CoroutineDispatcher, -) { - /** Flow representing the update of AudioSharingButtonState. */ - internal val audioSharingButtonStateUpdate: Flow<AudioSharingButtonState> = - combine( - bluetoothStateInteractor.bluetoothStateUpdate, - deviceItemInteractor.deviceItemUpdate - ) { bluetoothState, deviceItem -> - getButtonState(bluetoothState, deviceItem) +) : AudioSharingInteractor { + + override val isAudioSharingOn: Flow<Boolean> = + flow { emit(audioSharingAvailable()) } + .flatMapLatest { isEnabled -> + if (isEnabled) { + audioSharingRepository.inAudioSharing + } else { + flowOf(false) + } } .flowOn(backgroundDispatcher) - .stateIn( - coroutineScope, - SharingStarted.WhileSubscribed(replayExpirationMillis = 0), - initialValue = AudioSharingButtonState.Gone - ) - - private fun getButtonState( - bluetoothState: Boolean, - deviceItem: List<DeviceItem> - ): AudioSharingButtonState { - return when { - // Don't show button when bluetooth is off - !bluetoothState -> AudioSharingButtonState.Gone - // Show sharing audio when broadcasting - BluetoothUtils.isBroadcasting(localBluetoothManager) -> - AudioSharingButtonState.Visible( - R.string.quick_settings_bluetooth_audio_sharing_button_sharing, - isActive = true - ) - // When not broadcasting, don't show button if there's connected source in any device - deviceItem.any { - BluetoothUtils.hasConnectedBroadcastSource( - it.cachedBluetoothDevice, - localBluetoothManager - ) - } -> AudioSharingButtonState.Gone - // Show audio sharing when there's a connected LE audio device - deviceItem.any { BluetoothUtils.isActiveLeAudioDevice(it.cachedBluetoothDevice) } -> - AudioSharingButtonState.Visible( - R.string.quick_settings_bluetooth_audio_sharing_button, - isActive = false + + override val audioSourceStateUpdate = + isAudioSharingOn + .flatMapLatest { + if (it) { + audioSharingRepository.audioSourceStateUpdate + } else { + emptyFlow() + } + } + .flowOn(backgroundDispatcher) + + override suspend fun handleAudioSourceWhenReady() { + withContext(backgroundDispatcher) { + if (audioSharingAvailable()) { + audioSharingRepository.leAudioBroadcastProfile?.let { profile -> + isAudioSharingOn + .mapNotNull { audioSharingOn -> + if (audioSharingOn) { + // onPlaybackStarted could emit multiple times during one + // audio sharing session, we only perform add source on the + // first time + profile.onPlaybackStarted.firstOrNull() + } else { + null + } + } + .flowOn(backgroundDispatcher) + .collect { audioSharingRepository.addSource() } + } + } + } + } + + override suspend fun isAvailableAudioSharingMediaBluetoothDevice( + cachedBluetoothDevice: CachedBluetoothDevice + ): Boolean { + return withContext(backgroundDispatcher) { + if (audioSharingAvailable()) { + BluetoothUtils.isAvailableAudioSharingMediaBluetoothDevice( + cachedBluetoothDevice, + localBluetoothManager, ) - else -> AudioSharingButtonState.Gone + } else { + false + } } } + + override suspend fun switchActive(cachedBluetoothDevice: CachedBluetoothDevice) { + if (!audioSharingAvailable()) { + return + } + audioSharingRepository.setActive(cachedBluetoothDevice) + } + + override suspend fun startAudioSharing() { + if (!audioSharingAvailable()) { + return + } + audioSharingRepository.startAudioSharing() + } + + // TODO(b/367965193): Move this after flags rollout + override suspend fun audioSharingAvailable(): Boolean { + return audioSharingRepository.audioSharingAvailable() + } +} + +@SysUISingleton +class AudioSharingInteractorEmptyImpl @Inject constructor() : AudioSharingInteractor { + override val isAudioSharingOn: Flow<Boolean> = flowOf(false) + + override val audioSourceStateUpdate: Flow<Unit> = emptyFlow() + + override suspend fun handleAudioSourceWhenReady() {} + + override suspend fun isAvailableAudioSharingMediaBluetoothDevice( + cachedBluetoothDevice: CachedBluetoothDevice + ) = false + + override suspend fun switchActive(cachedBluetoothDevice: CachedBluetoothDevice) {} + + override suspend fun startAudioSharing() {} + + override suspend fun audioSharingAvailable(): Boolean = false } diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt new file mode 100644 index 000000000000..b9b8d36d41e6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt @@ -0,0 +1,120 @@ +/* + * 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.bluetooth.qsdialog + +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant +import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.settingslib.bluetooth.onSourceConnectedOrRemoved +import com.android.settingslib.volume.data.repository.AudioSharingRepository as SettingsLibAudioSharingRepository +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.withContext + +interface AudioSharingRepository { + val leAudioBroadcastProfile: LocalBluetoothLeBroadcast? + + val audioSourceStateUpdate: Flow<Unit> + + val inAudioSharing: StateFlow<Boolean> + + suspend fun audioSharingAvailable(): Boolean + + suspend fun addSource() + + suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) + + suspend fun startAudioSharing() +} + +@SysUISingleton +class AudioSharingRepositoryImpl( + private val localBluetoothManager: LocalBluetoothManager, + private val settingsLibAudioSharingRepository: SettingsLibAudioSharingRepository, + @Background private val backgroundDispatcher: CoroutineDispatcher, +) : AudioSharingRepository { + + override val leAudioBroadcastProfile: LocalBluetoothLeBroadcast? + get() = localBluetoothManager.profileManager?.leAudioBroadcastProfile + + private val leAudioBroadcastAssistantProfile: LocalBluetoothLeBroadcastAssistant? + get() = localBluetoothManager.profileManager?.leAudioBroadcastAssistantProfile + + override val audioSourceStateUpdate: Flow<Unit> = + leAudioBroadcastAssistantProfile?.onSourceConnectedOrRemoved ?: emptyFlow() + + override val inAudioSharing: StateFlow<Boolean> = + settingsLibAudioSharingRepository.inAudioSharing + + override suspend fun audioSharingAvailable(): Boolean { + return settingsLibAudioSharingRepository.audioSharingAvailable() + } + + override suspend fun addSource() { + withContext(backgroundDispatcher) { + if (!settingsLibAudioSharingRepository.audioSharingAvailable()) { + return@withContext + } + leAudioBroadcastProfile?.latestBluetoothLeBroadcastMetadata?.let { metadata -> + leAudioBroadcastAssistantProfile?.let { + it.allConnectedDevices.forEach { sink -> it.addSource(sink, metadata, false) } + } + } + } + } + + override suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) { + withContext(backgroundDispatcher) { + if (!settingsLibAudioSharingRepository.audioSharingAvailable()) { + return@withContext + } + cachedBluetoothDevice.setActive() + } + } + + override suspend fun startAudioSharing() { + withContext(backgroundDispatcher) { + if (!settingsLibAudioSharingRepository.audioSharingAvailable()) { + return@withContext + } + leAudioBroadcastProfile?.startPrivateBroadcast() + } + } +} + +@SysUISingleton +class AudioSharingRepositoryEmptyImpl : AudioSharingRepository { + override val leAudioBroadcastProfile: LocalBluetoothLeBroadcast? = null + + override val audioSourceStateUpdate: Flow<Unit> = emptyFlow() + + override val inAudioSharing: StateFlow<Boolean> = MutableStateFlow(false) + + override suspend fun audioSharingAvailable(): Boolean = false + + override suspend fun addSource() {} + + override suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) {} + + override suspend fun startAudioSharing() {} +} diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt index 17f9e634ec62..55d4d3efbe27 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt @@ -39,7 +39,7 @@ import kotlinx.coroutines.withContext /** Holds business logic for the Bluetooth Dialog's bluetooth and device connection state */ @SysUISingleton -internal class BluetoothStateInteractor +class BluetoothStateInteractor @Inject constructor( private val localBluetoothManager: LocalBluetoothManager?, diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt index 7deea7335223..a9c5c69ca26e 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt @@ -300,7 +300,7 @@ internal constructor( } private fun getProgressBarBackground(dialog: SystemUIDialog): View { - return dialog.requireViewById(R.id.bluetooth_tile_dialog_progress_animation) + return dialog.requireViewById(R.id.bluetooth_tile_dialog_progress_background) } private fun getScrollViewContent(dialog: SystemUIDialog): View { diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt index bdd4c161ad59..aad233fe40ca 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt @@ -42,6 +42,7 @@ enum class BluetoothTileDialogUiEvent(val metricId: Int) : UiEventLogger.UiEvent LAUNCH_SETTINGS_IN_SHARING_LE_DEVICE_CLICKED(1717), @UiEvent(doc = "Currently broadcasting and a non-LE audio supported device is clicked") LAUNCH_SETTINGS_IN_SHARING_NON_LE_DEVICE_CLICKED(1718), + @Deprecated("Use case no longer needed") @UiEvent( doc = "Not broadcasting, having one connected, another saved LE audio device is clicked" ) @@ -52,8 +53,13 @@ enum class BluetoothTileDialogUiEvent(val metricId: Int) : UiEventLogger.UiEvent ) @UiEvent(doc = "Not broadcasting, one of the two connected LE audio devices is clicked") LAUNCH_SETTINGS_NOT_SHARING_CONNECTED_LE_DEVICE_CLICKED(1720), + @Deprecated("Use case no longer needed") @UiEvent(doc = "Not broadcasting, having two connected, the active LE audio devices is clicked") - LAUNCH_SETTINGS_NOT_SHARING_ACTIVE_LE_DEVICE_CLICKED(1881); + LAUNCH_SETTINGS_NOT_SHARING_ACTIVE_LE_DEVICE_CLICKED(1881), + @UiEvent(doc = "Clicked on switch active button on audio sharing dialog") + AUDIO_SHARING_DIALOG_SWITCH_ACTIVE_CLICKED(1890), + @UiEvent(doc = "Clicked on share audio button on audio sharing dialog") + AUDIO_SHARING_DIALOG_SHARE_AUDIO_CLICKED(1891); override fun getId() = metricId } diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt index a8f7fc345001..5c35c52a4327 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt @@ -28,8 +28,8 @@ import androidx.annotation.StringRes import androidx.annotation.VisibleForTesting import com.android.internal.jank.InteractionJankMonitor import com.android.internal.logging.UiEventLogger -import com.android.settingslib.bluetooth.BluetoothUtils import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast +import com.android.settingslib.flags.Flags.audioSharingQsDialogImprovement import com.android.systemui.Prefs import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogTransitionAnimator @@ -51,6 +51,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.produce import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.merge @@ -68,10 +69,12 @@ constructor( private val bluetoothStateInteractor: BluetoothStateInteractor, private val bluetoothAutoOnInteractor: BluetoothAutoOnInteractor, private val audioSharingInteractor: AudioSharingInteractor, + private val audioSharingButtonViewModelFactory: AudioSharingButtonViewModel.Factory, private val bluetoothDeviceMetadataInteractor: BluetoothDeviceMetadataInteractor, private val dialogTransitionAnimator: DialogTransitionAnimator, private val activityStarter: ActivityStarter, private val uiEventLogger: UiEventLogger, + private val logger: BluetoothTileDialogLogger, @Application private val coroutineScope: CoroutineScope, @Main private val mainDispatcher: CoroutineDispatcher, @Background private val backgroundDispatcher: CoroutineDispatcher, @@ -102,7 +105,7 @@ constructor( expandable?.dialogTransitionController( DialogCuj( InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, - INTERACTION_JANK_TAG + INTERACTION_JANK_TAG, ) ) controller?.let { @@ -117,7 +120,7 @@ constructor( // stop the progress bar. combine( deviceItemInteractor.deviceItemUpdate, - deviceItemInteractor.showSeeAllUpdate + deviceItemInteractor.showSeeAllUpdate, ) { deviceItem, showSeeAll -> updateDialogUiJob?.cancel() updateDialogUiJob = launch { @@ -127,7 +130,7 @@ constructor( deviceItem, showSeeAll, showPairNewDevice = - bluetoothStateInteractor.isBluetoothEnabled() + bluetoothStateInteractor.isBluetoothEnabled(), ) animateProgressBar(dialog, false) } @@ -139,7 +142,15 @@ constructor( // the device item list and animate the progress bar. merge( deviceItemInteractor.deviceItemUpdateRequest, - bluetoothDeviceMetadataInteractor.metadataUpdate + bluetoothDeviceMetadataInteractor.metadataUpdate, + if ( + audioSharingInteractor.audioSharingAvailable() && + audioSharingQsDialogImprovement() + ) { + audioSharingInteractor.audioSourceStateUpdate + } else { + emptyFlow() + }, ) .onEach { dialogDelegate.animateProgressBar(dialog, true) @@ -147,35 +158,42 @@ constructor( updateDeviceItemJob = launch { deviceItemInteractor.updateDeviceItems( context, - DeviceFetchTrigger.BLUETOOTH_CALLBACK_RECEIVED + DeviceFetchTrigger.BLUETOOTH_CALLBACK_RECEIVED, ) } } .launchIn(this) - if (BluetoothUtils.isAudioSharingEnabled()) { - audioSharingInteractor.audioSharingButtonStateUpdate - .onEach { - when (it) { - is AudioSharingButtonState.Visible -> { - dialogDelegate.onAudioSharingButtonUpdated( - dialog, - VISIBLE, - context.getString(it.resId), - it.isActive - ) - } - is AudioSharingButtonState.Gone -> { - dialogDelegate.onAudioSharingButtonUpdated( - dialog, - GONE, - label = null, - isActive = false - ) + if (audioSharingInteractor.audioSharingAvailable()) { + if (audioSharingQsDialogImprovement()) { + launch { audioSharingInteractor.handleAudioSourceWhenReady() } + } + + audioSharingButtonViewModelFactory.create().run { + audioSharingButtonStateUpdate + .onEach { + when (it) { + is AudioSharingButtonState.Visible -> { + dialogDelegate.onAudioSharingButtonUpdated( + dialog, + VISIBLE, + context.getString(it.resId), + it.isActive, + ) + } + is AudioSharingButtonState.Gone -> { + dialogDelegate.onAudioSharingButtonUpdated( + dialog, + GONE, + label = null, + isActive = false, + ) + } } } - } - .launchIn(this) + .launchIn(this@launch) + launch { activate() } + } } // bluetoothStateUpdate is emitted when bluetooth on/off state is changed, re-fetch @@ -185,13 +203,13 @@ constructor( dialogDelegate.onBluetoothStateUpdated( dialog, it, - UiProperties.build(it, isAutoOnToggleFeatureAvailable()) + UiProperties.build(it, isAutoOnToggleFeatureAvailable()), ) updateDeviceItemJob?.cancel() updateDeviceItemJob = launch { deviceItemInteractor.updateDeviceItems( context, - DeviceFetchTrigger.BLUETOOTH_STATE_CHANGE_RECEIVED + DeviceFetchTrigger.BLUETOOTH_STATE_CHANGE_RECEIVED, ) } } @@ -209,7 +227,10 @@ constructor( // deviceItemClick is emitted when user clicked on a device item. dialogDelegate.deviceItemClick - .onEach { deviceItemActionInteractor.onClick(it, dialog) } + .onEach { + deviceItemActionInteractor.onClick(it, dialog) + logger.logDeviceClick(it.cachedBluetoothDevice.address, it.type) + } .launchIn(this) // contentHeight is emitted when the dialog is dismissed. @@ -230,7 +251,7 @@ constructor( dialog, it, if (it) R.string.turn_on_bluetooth_auto_info_enabled - else R.string.turn_on_bluetooth_auto_info_disabled + else R.string.turn_on_bluetooth_auto_info_disabled, ) } .launchIn(this) @@ -252,18 +273,18 @@ constructor( withContext(backgroundDispatcher) { sharedPreferences.getInt( CONTENT_HEIGHT_PREF_KEY, - ViewGroup.LayoutParams.WRAP_CONTENT + ViewGroup.LayoutParams.WRAP_CONTENT, ) } return bluetoothDialogDelegateFactory.create( UiProperties.build( bluetoothStateInteractor.isBluetoothEnabled(), - isAutoOnToggleFeatureAvailable() + isAutoOnToggleFeatureAvailable(), ), cachedContentHeight, this@BluetoothTileDialogViewModel, - { cancelJob() } + { cancelJob() }, ) } @@ -275,7 +296,7 @@ constructor( EXTRA_SHOW_FRAGMENT_ARGUMENTS, Bundle().apply { putString("device_address", deviceItem.cachedBluetoothDevice.address) - } + }, ) } startSettingsActivity(intent, view) @@ -299,7 +320,7 @@ constructor( EXTRA_SHOW_FRAGMENT_ARGUMENTS, Bundle().apply { putBoolean(LocalBluetoothLeBroadcast.EXTRA_START_LE_AUDIO_SHARING, true) - } + }, ) } startSettingsActivity(intent, view) @@ -345,7 +366,7 @@ constructor( companion object { internal fun build( isBluetoothEnabled: Boolean, - isAutoOnToggleFeatureAvailable: Boolean + isAutoOnToggleFeatureAvailable: Boolean, ) = UiProperties( subTitleResId = getSubtitleResId(isBluetoothEnabled), @@ -355,7 +376,7 @@ constructor( scrollViewMinHeightResId = if (isAutoOnToggleFeatureAvailable) R.dimen.bluetooth_dialog_scroll_view_min_height_with_auto_on - else R.dimen.bluetooth_dialog_scroll_view_min_height + else R.dimen.bluetooth_dialog_scroll_view_min_height, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt index f1894d3bb111..cf0f19f1d361 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt @@ -16,87 +16,28 @@ package com.android.systemui.bluetooth.qsdialog -import android.bluetooth.BluetoothDevice -import android.bluetooth.BluetoothProfile -import android.content.Intent -import android.os.Bundle -import android.provider.Settings import com.android.internal.logging.UiEventLogger -import com.android.settingslib.bluetooth.A2dpProfile -import com.android.settingslib.bluetooth.BluetoothUtils -import com.android.settingslib.bluetooth.HeadsetProfile -import com.android.settingslib.bluetooth.HearingAidProfile -import com.android.settingslib.bluetooth.LeAudioProfile -import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast -import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant -import com.android.settingslib.bluetooth.LocalBluetoothManager -import com.android.systemui.animation.DialogTransitionAnimator -import com.android.systemui.bluetooth.qsdialog.DeviceItemActionInteractor.LaunchSettingsCriteria.Companion.getCurrentConnectedLeByGroupId import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.phone.SystemUIDialog import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext +interface DeviceItemActionInteractor { + suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) {} +} + @SysUISingleton -class DeviceItemActionInteractor +class DeviceItemActionInteractorImpl @Inject constructor( - private val activityStarter: ActivityStarter, - private val dialogTransitionAnimator: DialogTransitionAnimator, - private val localBluetoothManager: LocalBluetoothManager?, @Background private val backgroundDispatcher: CoroutineDispatcher, - private val logger: BluetoothTileDialogLogger, private val uiEventLogger: UiEventLogger, -) { - private val leAudioProfile: LeAudioProfile? - get() = localBluetoothManager?.profileManager?.leAudioProfile - - private val assistantProfile: LocalBluetoothLeBroadcastAssistant? - get() = localBluetoothManager?.profileManager?.leAudioBroadcastAssistantProfile - - private val launchSettingsCriteriaList: List<LaunchSettingsCriteria> - get() = - listOf( - InSharingClickedNoSource(localBluetoothManager, backgroundDispatcher, logger), - NotSharingClickedNonConnect( - leAudioProfile, - assistantProfile, - backgroundDispatcher, - logger - ), - NotSharingClickedActive( - leAudioProfile, - assistantProfile, - backgroundDispatcher, - logger - ) - ) +) : DeviceItemActionInteractor { - suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) { + override suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) { withContext(backgroundDispatcher) { - logger.logDeviceClick(deviceItem.cachedBluetoothDevice.address, deviceItem.type) - if ( - BluetoothUtils.isAudioSharingEnabled() && - localBluetoothManager != null && - leAudioProfile != null && - assistantProfile != null - ) { - val inAudioSharing = BluetoothUtils.isBroadcasting(localBluetoothManager) - logger.logDeviceClickInAudioSharingWhenEnabled(inAudioSharing) - - val criteriaMatched = - launchSettingsCriteriaList.firstOrNull { - it.matched(inAudioSharing, deviceItem) - } - if (criteriaMatched != null) { - uiEventLogger.log(criteriaMatched.getClickUiEvent(deviceItem)) - launchSettings(deviceItem.cachedBluetoothDevice.device, dialog) - return@withContext - } - } deviceItem.cachedBluetoothDevice.apply { when (deviceItem.type) { DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE -> { @@ -106,12 +47,6 @@ constructor( DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> { uiEventLogger.log(BluetoothTileDialogUiEvent.AUDIO_SHARING_DEVICE_CLICKED) } - DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> { - // TODO(b/360759048): pop up dialog - uiEventLogger.log( - BluetoothTileDialogUiEvent.AVAILABLE_AUDIO_SHARING_DEVICE_CLICKED - ) - } DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE -> { setActive() uiEventLogger.log(BluetoothTileDialogUiEvent.CONNECTED_DEVICE_SET_ACTIVE) @@ -126,186 +61,12 @@ constructor( connect() uiEventLogger.log(BluetoothTileDialogUiEvent.SAVED_DEVICE_CONNECT) } - } - } - } - } - - private fun launchSettings(device: BluetoothDevice, dialog: SystemUIDialog) { - val intent = - Intent(Settings.ACTION_BLUETOOTH_SETTINGS).apply { - putExtra( - EXTRA_SHOW_FRAGMENT_ARGUMENTS, - Bundle().apply { - putParcelable(LocalBluetoothLeBroadcast.EXTRA_BLUETOOTH_DEVICE, device) + DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> { + // Do nothing. Should already be handled in + // AudioSharingDeviceItemActionInteractor. } - ) - } - intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK - activityStarter.postStartActivityDismissingKeyguard( - intent, - 0, - dialogTransitionAnimator.createActivityTransitionController(dialog) - ) - } - - private interface LaunchSettingsCriteria { - suspend fun matched(inAudioSharing: Boolean, deviceItem: DeviceItem): Boolean - - suspend fun getClickUiEvent(deviceItem: DeviceItem): BluetoothTileDialogUiEvent - - companion object { - suspend fun getCurrentConnectedLeByGroupId( - leAudioProfile: LeAudioProfile, - assistantProfile: LocalBluetoothLeBroadcastAssistant, - @Background backgroundDispatcher: CoroutineDispatcher, - logger: BluetoothTileDialogLogger, - ): Map<Int, List<BluetoothDevice>> { - return withContext(backgroundDispatcher) { - assistantProfile - .getDevicesMatchingConnectionStates( - intArrayOf(BluetoothProfile.STATE_CONNECTED) - ) - ?.filterNotNull() - ?.groupBy { leAudioProfile.getGroupId(it) } - ?.also { logger.logConnectedLeByGroupId(it) } ?: emptyMap() } } } } - - private class InSharingClickedNoSource( - private val localBluetoothManager: LocalBluetoothManager?, - @Background private val backgroundDispatcher: CoroutineDispatcher, - private val logger: BluetoothTileDialogLogger, - ) : LaunchSettingsCriteria { - // If currently broadcasting and the clicked device is not connected to the source - override suspend fun matched(inAudioSharing: Boolean, deviceItem: DeviceItem): Boolean { - return withContext(backgroundDispatcher) { - val matched = - inAudioSharing && - deviceItem.isMediaDevice && - !BluetoothUtils.hasConnectedBroadcastSource( - deviceItem.cachedBluetoothDevice, - localBluetoothManager - ) - - if (matched) { - logger.logLaunchSettingsCriteriaMatched("InSharingClickedNoSource", deviceItem) - } - - matched - } - } - - override suspend fun getClickUiEvent(deviceItem: DeviceItem) = - if (deviceItem.isLeAudioSupported) - BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_IN_SHARING_LE_DEVICE_CLICKED - else BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_IN_SHARING_NON_LE_DEVICE_CLICKED - } - - private class NotSharingClickedNonConnect( - private val leAudioProfile: LeAudioProfile?, - private val assistantProfile: LocalBluetoothLeBroadcastAssistant?, - @Background private val backgroundDispatcher: CoroutineDispatcher, - private val logger: BluetoothTileDialogLogger, - ) : LaunchSettingsCriteria { - // If not broadcasting, having one device connected, and clicked on a not yet connected LE - // audio device - override suspend fun matched(inAudioSharing: Boolean, deviceItem: DeviceItem): Boolean { - return withContext(backgroundDispatcher) { - val matched = - leAudioProfile?.let { leAudio -> - assistantProfile?.let { assistant -> - !inAudioSharing && - getCurrentConnectedLeByGroupId( - leAudio, - assistant, - backgroundDispatcher, - logger - ) - .size == 1 && - deviceItem.isNotConnectedLeAudioSupported - } - } ?: false - - if (matched) { - logger.logLaunchSettingsCriteriaMatched( - "NotSharingClickedNonConnect", - deviceItem - ) - } - - matched - } - } - - override suspend fun getClickUiEvent(deviceItem: DeviceItem) = - BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_NOT_SHARING_SAVED_LE_DEVICE_CLICKED - } - - private class NotSharingClickedActive( - private val leAudioProfile: LeAudioProfile?, - private val assistantProfile: LocalBluetoothLeBroadcastAssistant?, - @Background private val backgroundDispatcher: CoroutineDispatcher, - private val logger: BluetoothTileDialogLogger, - ) : LaunchSettingsCriteria { - // If not broadcasting, having two device connected, clicked on the active LE audio - // device - override suspend fun matched(inAudioSharing: Boolean, deviceItem: DeviceItem): Boolean { - return withContext(backgroundDispatcher) { - val matched = - leAudioProfile?.let { leAudio -> - assistantProfile?.let { assistant -> - !inAudioSharing && - getCurrentConnectedLeByGroupId( - leAudio, - assistant, - backgroundDispatcher, - logger - ) - .size == 2 && - deviceItem.isActiveLeAudioSupported - } - } ?: false - - if (matched) { - logger.logLaunchSettingsCriteriaMatched( - "NotSharingClickedConnected", - deviceItem - ) - } - - matched - } - } - - override suspend fun getClickUiEvent(deviceItem: DeviceItem) = - BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_NOT_SHARING_ACTIVE_LE_DEVICE_CLICKED - } - - private companion object { - const val EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args" - - val DeviceItem.isLeAudioSupported: Boolean - get() = - cachedBluetoothDevice.profiles.any { profile -> - profile is LeAudioProfile && profile.isEnabled(cachedBluetoothDevice.device) - } - - val DeviceItem.isNotConnectedLeAudioSupported: Boolean - get() = type == DeviceItemType.SAVED_BLUETOOTH_DEVICE && isLeAudioSupported - - val DeviceItem.isActiveLeAudioSupported: Boolean - get() = type == DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE && isLeAudioSupported - - val DeviceItem.isMediaDevice: Boolean - get() = - cachedBluetoothDevice.uiAccessibleProfiles.any { - it is A2dpProfile || - it is HearingAidProfile || - it is LeAudioProfile || - it is HeadsetProfile - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt index 7280489e0835..7ed56296e865 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt @@ -23,7 +23,6 @@ import com.android.settingslib.bluetooth.BluetoothUtils import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.flags.Flags -import com.android.settingslib.flags.Flags.enableLeAudioSharing import com.android.systemui.res.R private val backgroundOn = R.drawable.settingslib_switch_bar_bg_on @@ -56,7 +55,7 @@ abstract class DeviceItemFactory { connectionSummary: String, background: Int, actionAccessibilityLabel: String, - isActive: Boolean + isActive: Boolean, ): DeviceItem { return DeviceItem( type = type, @@ -70,7 +69,7 @@ abstract class DeviceItemFactory { background = background, isEnabled = !cachedDevice.isBusy, actionAccessibilityLabel = actionAccessibilityLabel, - isActive = isActive + isActive = isActive, ) } } @@ -80,7 +79,7 @@ internal open class ActiveMediaDeviceItemFactory : DeviceItemFactory() { override fun isFilterMatched( context: Context, cachedDevice: CachedBluetoothDevice, - audioManager: AudioManager + audioManager: AudioManager, ): Boolean { return BluetoothUtils.isActiveMediaDevice(cachedDevice) && BluetoothUtils.isAvailableMediaBluetoothDevice(cachedDevice, audioManager) @@ -94,20 +93,20 @@ internal open class ActiveMediaDeviceItemFactory : DeviceItemFactory() { cachedDevice.connectionSummary ?: "", backgroundOn, context.getString(actionAccessibilityLabelDisconnect), - isActive = true + isActive = true, ) } } internal class AudioSharingMediaDeviceItemFactory( - private val localBluetoothManager: LocalBluetoothManager? + private val localBluetoothManager: LocalBluetoothManager ) : DeviceItemFactory() { override fun isFilterMatched( context: Context, cachedDevice: CachedBluetoothDevice, - audioManager: AudioManager + audioManager: AudioManager, ): Boolean { - return enableLeAudioSharing() && + return BluetoothUtils.isAudioSharingEnabled() && BluetoothUtils.hasConnectedBroadcastSource(cachedDevice, localBluetoothManager) } @@ -120,24 +119,24 @@ internal class AudioSharingMediaDeviceItemFactory( ?: context.getString(audioSharing), if (cachedDevice.isBusy) backgroundOffBusy else backgroundOn, "", - isActive = !cachedDevice.isBusy + isActive = !cachedDevice.isBusy, ) } } internal class AvailableAudioSharingMediaDeviceItemFactory( - private val localBluetoothManager: LocalBluetoothManager? + private val localBluetoothManager: LocalBluetoothManager ) : AvailableMediaDeviceItemFactory() { override fun isFilterMatched( context: Context, cachedDevice: CachedBluetoothDevice, - audioManager: AudioManager + audioManager: AudioManager, ): Boolean { return BluetoothUtils.isAudioSharingEnabled() && super.isFilterMatched(context, cachedDevice, audioManager) && BluetoothUtils.isAvailableAudioSharingMediaBluetoothDevice( cachedDevice, - localBluetoothManager + localBluetoothManager, ) } @@ -151,7 +150,7 @@ internal class AvailableAudioSharingMediaDeviceItemFactory( ), if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff, "", - isActive = false + isActive = false, ) } } @@ -160,7 +159,7 @@ internal class ActiveHearingDeviceItemFactory : ActiveMediaDeviceItemFactory() { override fun isFilterMatched( context: Context, cachedDevice: CachedBluetoothDevice, - audioManager: AudioManager + audioManager: AudioManager, ): Boolean { return BluetoothUtils.isActiveMediaDevice(cachedDevice) && BluetoothUtils.isAvailableHearingDevice(cachedDevice) @@ -171,7 +170,7 @@ open class AvailableMediaDeviceItemFactory : DeviceItemFactory() { override fun isFilterMatched( context: Context, cachedDevice: CachedBluetoothDevice, - audioManager: AudioManager + audioManager: AudioManager, ): Boolean { return !BluetoothUtils.isActiveMediaDevice(cachedDevice) && BluetoothUtils.isAvailableMediaBluetoothDevice(cachedDevice, audioManager) @@ -186,7 +185,7 @@ open class AvailableMediaDeviceItemFactory : DeviceItemFactory() { ?: context.getString(connected), if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff, context.getString(actionAccessibilityLabelActivate), - isActive = false + isActive = false, ) } } @@ -195,7 +194,7 @@ internal class AvailableHearingDeviceItemFactory : AvailableMediaDeviceItemFacto override fun isFilterMatched( context: Context, cachedDevice: CachedBluetoothDevice, - audioManager: AudioManager + audioManager: AudioManager, ): Boolean { return !BluetoothUtils.isActiveMediaDevice(cachedDevice) && BluetoothUtils.isAvailableHearingDevice(cachedDevice) @@ -206,7 +205,7 @@ internal class ConnectedDeviceItemFactory : DeviceItemFactory() { override fun isFilterMatched( context: Context, cachedDevice: CachedBluetoothDevice, - audioManager: AudioManager + audioManager: AudioManager, ): Boolean { return if (Flags.enableHideExclusivelyManagedBluetoothDevice()) { !BluetoothUtils.isExclusivelyManagedBluetoothDevice(context, cachedDevice.device) && @@ -225,7 +224,7 @@ internal class ConnectedDeviceItemFactory : DeviceItemFactory() { ?: context.getString(connected), if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff, context.getString(actionAccessibilityLabelDisconnect), - isActive = false + isActive = false, ) } } @@ -234,7 +233,7 @@ internal open class SavedDeviceItemFactory : DeviceItemFactory() { override fun isFilterMatched( context: Context, cachedDevice: CachedBluetoothDevice, - audioManager: AudioManager + audioManager: AudioManager, ): Boolean { return if (Flags.enableHideExclusivelyManagedBluetoothDevice()) { !BluetoothUtils.isExclusivelyManagedBluetoothDevice(context, cachedDevice.device) && @@ -254,7 +253,7 @@ internal open class SavedDeviceItemFactory : DeviceItemFactory() { ?: context.getString(saved), if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff, context.getString(actionAccessibilityLabelActivate), - isActive = false + isActive = false, ) } } @@ -263,12 +262,12 @@ internal class SavedHearingDeviceItemFactory : SavedDeviceItemFactory() { override fun isFilterMatched( context: Context, cachedDevice: CachedBluetoothDevice, - audioManager: AudioManager + audioManager: AudioManager, ): Boolean { return if (Flags.enableHideExclusivelyManagedBluetoothDevice()) { !BluetoothUtils.isExclusivelyManagedBluetoothDevice( context, - cachedDevice.getDevice() + cachedDevice.getDevice(), ) && cachedDevice.isHearingAidDevice && cachedDevice.bondState == BluetoothDevice.BOND_BONDED && diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt index 9114ecac7ac7..0118e56a773c 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt @@ -54,6 +54,8 @@ constructor( private val localBluetoothManager: LocalBluetoothManager?, private val systemClock: SystemClock, private val logger: BluetoothTileDialogLogger, + private val deviceItemFactoryList: List<@JvmSuppressWildcards DeviceItemFactory>, + private val deviceItemDisplayPriority: List<@JvmSuppressWildcards DeviceItemType>, @Application private val coroutineScope: CoroutineScope, @Background private val backgroundDispatcher: CoroutineDispatcher, ) { @@ -67,7 +69,7 @@ constructor( internal val showSeeAllUpdate get() = mutableShowSeeAllUpdate.asStateFlow() - internal val deviceItemUpdateRequest: SharedFlow<Unit> = + val deviceItemUpdateRequest: SharedFlow<Unit> = conflatedCallbackFlow { val listener = object : BluetoothCallback { @@ -114,26 +116,6 @@ constructor( } .shareIn(coroutineScope, SharingStarted.WhileSubscribed(replayExpirationMillis = 0)) - private var deviceItemFactoryList: List<DeviceItemFactory> = - listOf( - ActiveMediaDeviceItemFactory(), - AudioSharingMediaDeviceItemFactory(localBluetoothManager), - AvailableAudioSharingMediaDeviceItemFactory(localBluetoothManager), - AvailableMediaDeviceItemFactory(), - ConnectedDeviceItemFactory(), - SavedDeviceItemFactory() - ) - - private var displayPriority: List<DeviceItemType> = - listOf( - DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE, - DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE, - DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE, - DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE, - DeviceItemType.CONNECTED_BLUETOOTH_DEVICE, - DeviceItemType.SAVED_BLUETOOTH_DEVICE, - ) - internal suspend fun updateDeviceItems(context: Context, trigger: DeviceFetchTrigger) { withContext(backgroundDispatcher) { val start = systemClock.elapsedRealtime() @@ -144,7 +126,7 @@ constructor( .firstOrNull { it.isFilterMatched(context, cachedDevice, audioManager) } ?.create(context, cachedDevice) } - .sort(displayPriority, bluetoothAdapter?.mostRecentlyConnectedDevices) + .sort(deviceItemDisplayPriority, bluetoothAdapter?.mostRecentlyConnectedDevices) // Only emit when the job is not cancelled if (isActive) { mutableDeviceItemUpdate.tryEmit(deviceItems.take(MAX_DEVICE_ITEM_ENTRY)) @@ -176,14 +158,6 @@ constructor( ) } - internal fun setDeviceItemFactoryListForTesting(list: List<DeviceItemFactory>) { - deviceItemFactoryList = list - } - - internal fun setDisplayPriorityForTesting(list: List<DeviceItemType>) { - displayPriority = list - } - companion object { private const val TAG = "DeviceItemInteractor" private const val MAX_DEVICE_ITEM_ENTRY = 3 diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/dagger/AudioSharingModule.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/dagger/AudioSharingModule.kt new file mode 100644 index 000000000000..50970a5d006f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/dagger/AudioSharingModule.kt @@ -0,0 +1,128 @@ +/* + * 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.bluetooth.qsdialog.dagger + +import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.settingslib.flags.Flags +import com.android.settingslib.volume.data.repository.AudioSharingRepository as SettingsLibAudioSharingRepository +import com.android.systemui.bluetooth.qsdialog.ActiveMediaDeviceItemFactory +import com.android.systemui.bluetooth.qsdialog.AudioSharingDeviceItemActionInteractorImpl +import com.android.systemui.bluetooth.qsdialog.AudioSharingInteractor +import com.android.systemui.bluetooth.qsdialog.AudioSharingInteractorEmptyImpl +import com.android.systemui.bluetooth.qsdialog.AudioSharingInteractorImpl +import com.android.systemui.bluetooth.qsdialog.AudioSharingMediaDeviceItemFactory +import com.android.systemui.bluetooth.qsdialog.AudioSharingRepository +import com.android.systemui.bluetooth.qsdialog.AudioSharingRepositoryEmptyImpl +import com.android.systemui.bluetooth.qsdialog.AudioSharingRepositoryImpl +import com.android.systemui.bluetooth.qsdialog.AvailableAudioSharingMediaDeviceItemFactory +import com.android.systemui.bluetooth.qsdialog.AvailableMediaDeviceItemFactory +import com.android.systemui.bluetooth.qsdialog.ConnectedDeviceItemFactory +import com.android.systemui.bluetooth.qsdialog.DeviceItemActionInteractor +import com.android.systemui.bluetooth.qsdialog.DeviceItemActionInteractorImpl +import com.android.systemui.bluetooth.qsdialog.DeviceItemFactory +import com.android.systemui.bluetooth.qsdialog.DeviceItemType +import com.android.systemui.bluetooth.qsdialog.SavedDeviceItemFactory +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import dagger.Lazy +import dagger.Module +import dagger.Provides +import kotlinx.coroutines.CoroutineDispatcher + +/** Dagger module for audio sharing code for BT QS dialog */ +@Module +interface AudioSharingModule { + + companion object { + @Provides + @SysUISingleton + fun provideAudioSharingRepository( + localBluetoothManager: LocalBluetoothManager?, + settingsLibAudioSharingRepository: SettingsLibAudioSharingRepository, + @Background backgroundDispatcher: CoroutineDispatcher, + ): AudioSharingRepository = + if ( + Flags.enableLeAudioSharing() && + Flags.audioSharingQsDialogImprovement() && + localBluetoothManager != null + ) { + AudioSharingRepositoryImpl( + localBluetoothManager, + settingsLibAudioSharingRepository, + backgroundDispatcher, + ) + } else { + AudioSharingRepositoryEmptyImpl() + } + + @Provides + @SysUISingleton + fun provideAudioSharingInteractor( + localBluetoothManager: LocalBluetoothManager?, + impl: Lazy<AudioSharingInteractorImpl>, + emptyImpl: Lazy<AudioSharingInteractorEmptyImpl>, + ): AudioSharingInteractor = + if (Flags.enableLeAudioSharing() && localBluetoothManager != null) { + impl.get() + } else { + emptyImpl.get() + } + + @Provides + @SysUISingleton + fun provideDeviceItemActionInteractor( + localBluetoothManager: LocalBluetoothManager?, + audioSharingImpl: Lazy<AudioSharingDeviceItemActionInteractorImpl>, + impl: Lazy<DeviceItemActionInteractorImpl>, + ): DeviceItemActionInteractor = + if (Flags.enableLeAudioSharing() && localBluetoothManager != null) { + audioSharingImpl.get() + } else { + impl.get() + } + + @Provides + @SysUISingleton + fun provideDeviceItemFactoryList( + localBluetoothManager: LocalBluetoothManager? + ): List<DeviceItemFactory> = buildList { + add(ActiveMediaDeviceItemFactory()) + if (Flags.enableLeAudioSharing() && localBluetoothManager != null) { + add(AudioSharingMediaDeviceItemFactory(localBluetoothManager)) + add(AvailableAudioSharingMediaDeviceItemFactory(localBluetoothManager)) + } + add(AvailableMediaDeviceItemFactory()) + add(ConnectedDeviceItemFactory()) + add(SavedDeviceItemFactory()) + } + + @Provides + @SysUISingleton + fun provideDeviceItemDisplayPriority( + localBluetoothManager: LocalBluetoothManager? + ): List<DeviceItemType> = buildList { + add(DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE) + if (Flags.enableLeAudioSharing() && localBluetoothManager != null) { + add(DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE) + add(DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE) + } + add(DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE) + add(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE) + add(DeviceItemType.SAVED_BLUETOOTH_DEVICE) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt index 3ae9250b2938..6508e4b574a3 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt @@ -27,7 +27,6 @@ import android.os.UserHandle import android.util.Log import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityManager -import androidx.activity.result.ActivityResultLauncher import com.android.internal.logging.UiEventLogger import com.android.systemui.communal.dagger.CommunalModule.Companion.LAUNCHER_PACKAGE import com.android.systemui.communal.data.model.CommunalWidgetCategories @@ -184,10 +183,10 @@ constructor( val isIdleOnCommunal: StateFlow<Boolean> = communalInteractor.isIdleOnCommunal - /** Launch the widget picker activity using the given {@link ActivityResultLauncher}. */ + /** Launch the widget picker activity using the given startActivity method. */ suspend fun onOpenWidgetPicker( resources: Resources, - activityLauncher: ActivityResultLauncher<Intent>, + startActivity: (intent: Intent) -> Unit, ): Boolean = withContext(backgroundDispatcher) { val widgets = communalInteractor.widgetContent.first() @@ -199,7 +198,7 @@ constructor( } getWidgetPickerActivityIntent(resources, excludeList)?.let { try { - activityLauncher.launch(it) + startActivity(it) return@withContext true } catch (e: Exception) { Log.e(TAG, "Failed to launch widget picker activity", e) diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt index 6228ac5f84ae..8c14d63c0e84 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt @@ -27,8 +27,6 @@ import android.view.IWindowManager import android.view.WindowInsets import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -51,6 +49,7 @@ import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.settings.UserTracker import javax.inject.Inject import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @@ -64,12 +63,15 @@ constructor( private val uiEventLogger: UiEventLogger, private val widgetConfiguratorFactory: WidgetConfigurationController.Factory, private val widgetSection: CommunalAppWidgetSection, + private val userTracker: UserTracker, @CommunalLog logBuffer: LogBuffer, ) : ComponentActivity() { companion object { private const val TAG = "EditWidgetsActivity" private const val EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag" const val EXTRA_OPEN_WIDGET_PICKER_ON_START = "open_widget_picker_on_start" + + private const val REQUEST_CODE_WIDGET_PICKER = 200 } /** @@ -110,7 +112,7 @@ constructor( object : ActivityLifecycleCallbacks { override fun onActivityCreated( activity: Activity, - savedInstanceState: Bundle? + savedInstanceState: Bundle?, ) { waitingForResult = savedInstanceState?.getBoolean(STATE_EXTRA_IS_WAITING_FOR_RESULT) @@ -172,41 +174,6 @@ constructor( if (communalEditWidgetsActivityFinishFix()) ActivityControllerImpl(this) else NopActivityController() - private val addWidgetActivityLauncher: ActivityResultLauncher<Intent> = - registerForActivityResult(StartActivityForResult()) { result -> - when (result.resultCode) { - RESULT_OK -> { - uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_WIDGET_PICKER_SHOWN) - - result.data?.let { intent -> - val isPendingWidgetDrag = - intent.getBooleanExtra(EXTRA_IS_PENDING_WIDGET_DRAG, false) - // Nothing to do when a widget is being dragged & dropped. The drop - // target in the communal grid will receive the widget to be added (if - // the user drops it over). - if (!isPendingWidgetDrag) { - val (componentName, user) = getWidgetExtraFromIntent(intent) - if (componentName != null && user != null) { - // Add widget at the end. - communalViewModel.onAddWidget( - componentName, - user, - configurator = widgetConfigurator, - ) - } else { - run { Log.w(TAG, "No AppWidgetProviderInfo found in result.") } - } - } - } ?: run { Log.w(TAG, "No data in result.") } - } - else -> - Log.w( - TAG, - "Failed to receive result from widget picker, code=${result.resultCode}" - ) - } - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -226,8 +193,7 @@ constructor( PlatformTheme { Box( modifier = - Modifier.fillMaxSize() - .background(MaterialTheme.colorScheme.surfaceDim), + Modifier.fillMaxSize().background(MaterialTheme.colorScheme.surfaceDim) ) { CommunalHub( viewModel = communalViewModel, @@ -274,7 +240,13 @@ constructor( private fun onOpenWidgetPicker() { lifecycleScope.launch { - communalViewModel.onOpenWidgetPicker(resources, addWidgetActivityLauncher) + communalViewModel.onOpenWidgetPicker(resources) { intent: Intent -> + startActivityForResultAsUser( + intent, + REQUEST_CODE_WIDGET_PICKER, + userTracker.userHandle, + ) + } } } @@ -285,7 +257,7 @@ constructor( communalViewModel.changeScene( scene = CommunalScenes.Communal, loggingReason = "edit mode closing", - transitionKey = CommunalTransitionKeys.FromEditMode + transitionKey = CommunalTransitionKeys.FromEditMode, ) // Wait for the current scene to be idle on communal. @@ -309,7 +281,7 @@ constructor( flagsMask: Int, flagsValues: Int, extraFlags: Int, - options: Bundle? + options: Bundle?, ) { activityController.onWaitingForResult(true) super.startIntentSenderForResult( @@ -319,15 +291,46 @@ constructor( flagsMask, flagsValues, extraFlags, - options + options, ) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { activityController.onWaitingForResult(false) super.onActivityResult(requestCode, resultCode, data) - if (requestCode == WidgetConfigurationController.REQUEST_CODE) { - widgetConfigurator.setConfigurationResult(resultCode) + + when (requestCode) { + WidgetConfigurationController.REQUEST_CODE -> + widgetConfigurator.setConfigurationResult(resultCode) + REQUEST_CODE_WIDGET_PICKER -> { + if (resultCode != RESULT_OK) { + Log.w(TAG, "Failed to receive result from widget picker, code=$resultCode") + return + } + + uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_WIDGET_PICKER_SHOWN) + + data?.let { intent -> + val isPendingWidgetDrag = + intent.getBooleanExtra(EXTRA_IS_PENDING_WIDGET_DRAG, false) + // Nothing to do when a widget is being dragged & dropped. The drop + // target in the communal grid will receive the widget to be added (if + // the user drops it over). + if (!isPendingWidgetDrag) { + val (componentName, user) = getWidgetExtraFromIntent(intent) + if (componentName != null && user != null) { + // Add widget at the end. + communalViewModel.onAddWidget( + componentName, + user, + configurator = widgetConfigurator, + ) + } else { + run { Log.w(TAG, "No AppWidgetProviderInfo found in result.") } + } + } + } ?: run { Log.w(TAG, "No data in result.") } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt b/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt index b56ed8c4c090..589dbf92de38 100644 --- a/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt +++ b/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt @@ -24,6 +24,8 @@ import com.android.systemui.display.data.repository.DisplayRepository import com.android.systemui.display.data.repository.DisplayRepositoryImpl import com.android.systemui.display.data.repository.DisplayScopeRepository import com.android.systemui.display.data.repository.DisplayScopeRepositoryImpl +import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepository +import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepositoryImpl import com.android.systemui.display.data.repository.FocusedDisplayRepository import com.android.systemui.display.data.repository.FocusedDisplayRepositoryImpl import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor @@ -58,6 +60,11 @@ interface DisplayModule { @Binds fun displayScopeRepository(impl: DisplayScopeRepositoryImpl): DisplayScopeRepository + @Binds + fun displayWindowPropertiesRepository( + impl: DisplayWindowPropertiesRepositoryImpl + ): DisplayWindowPropertiesRepository + companion object { @Provides @SysUISingleton @@ -72,5 +79,19 @@ interface DisplayModule { CoreStartable.NOP } } + + @Provides + @SysUISingleton + @IntoMap + @ClassKey(DisplayWindowPropertiesRepository::class) + fun displayWindowPropertiesRepoAsCoreStartable( + repoLazy: Lazy<DisplayWindowPropertiesRepositoryImpl> + ): CoreStartable { + return if (StatusBarConnectedDisplays.isEnabled) { + return repoLazy.get() + } else { + CoreStartable.NOP + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepository.kt new file mode 100644 index 000000000000..88d3a28669df --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepository.kt @@ -0,0 +1,115 @@ +/* + * 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.display.data.repository + +import android.annotation.SuppressLint +import android.content.Context +import android.view.Display +import android.view.WindowManager +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.display.shared.model.DisplayWindowProperties +import com.android.systemui.res.R +import com.android.systemui.statusbar.core.StatusBarConnectedDisplays +import com.google.common.collect.HashBasedTable +import com.google.common.collect.Table +import java.io.PrintWriter +import javax.inject.Inject +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** Provides per display instances of [DisplayWindowProperties]. */ +interface DisplayWindowPropertiesRepository { + + /** + * Returns a [DisplayWindowProperties] instance for a given display id and window type. + * + * @throws IllegalArgumentException if no display with the given display id exists. + */ + fun get( + displayId: Int, + @WindowManager.LayoutParams.WindowType windowType: Int, + ): DisplayWindowProperties +} + +@SysUISingleton +class DisplayWindowPropertiesRepositoryImpl +@Inject +constructor( + @Background private val backgroundApplicationScope: CoroutineScope, + private val globalContext: Context, + private val globalWindowManager: WindowManager, + private val displayRepository: DisplayRepository, +) : DisplayWindowPropertiesRepository, CoreStartable { + + init { + StatusBarConnectedDisplays.assertInNewMode() + } + + private val properties: Table<Int, Int, DisplayWindowProperties> = HashBasedTable.create() + + override fun get( + displayId: Int, + @WindowManager.LayoutParams.WindowType windowType: Int, + ): DisplayWindowProperties { + val display = + displayRepository.getDisplay(displayId) + ?: throw IllegalArgumentException("Display with id $displayId doesn't exist") + return properties.get(displayId, windowType) + ?: create(display, windowType).also { properties.put(displayId, windowType, it) } + } + + override fun start() { + backgroundApplicationScope.launch( + CoroutineName("DisplayWindowPropertiesRepositoryImpl#start") + ) { + displayRepository.displayRemovalEvent.collect { removedDisplayId -> + properties.row(removedDisplayId).clear() + } + } + } + + private fun create(display: Display, windowType: Int): DisplayWindowProperties { + val displayId = display.displayId + return if (displayId == Display.DEFAULT_DISPLAY) { + // For the default display, we can just reuse the global/application properties. + // Creating a window context is expensive, therefore we avoid it. + DisplayWindowProperties( + displayId = displayId, + windowType = windowType, + context = globalContext, + windowManager = globalWindowManager, + ) + } else { + val context = createWindowContext(display, windowType) + @SuppressLint("NonInjectedService") // Need to manually get the service + val windowManager = context.getSystemService(WindowManager::class.java) as WindowManager + DisplayWindowProperties(displayId, windowType, context, windowManager) + } + } + + private fun createWindowContext(display: Display, windowType: Int): Context = + globalContext.createWindowContext(display, windowType, /* options= */ null).also { + it.setTheme(R.style.Theme_SystemUI) + } + + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.write("perDisplayContexts: $properties") + } +} diff --git a/packages/SystemUI/src/com/android/systemui/display/shared/model/DisplayWindowProperties.kt b/packages/SystemUI/src/com/android/systemui/display/shared/model/DisplayWindowProperties.kt new file mode 100644 index 000000000000..6acc296367a9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/display/shared/model/DisplayWindowProperties.kt @@ -0,0 +1,43 @@ +/* + * 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.display.shared.model + +import android.content.Context +import android.view.WindowManager + +/** Represents a display specific group of window related properties. */ +data class DisplayWindowProperties( + /** The id of the display associated with this instance. */ + val displayId: Int, + /** + * The window type that was used to create the [Context] in this instance, using + * [Context.createWindowContext]. This is the window type that can be used when adding views to + * the [WindowManager] associated with this instance. + */ + @WindowManager.LayoutParams.WindowType val windowType: Int, + /** + * The display specific [Context] created using [Context.createWindowContext] with window type + * associated with this instance. + */ + val context: Context, + + /** + * The display specific [WindowManager] instance to be used when adding windows of the type + * associated with this instance. + */ + val windowManager: WindowManager, +) diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt index 65c29b829429..9c5231d716da 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt @@ -92,6 +92,7 @@ import com.android.systemui.plugins.qs.QSContainerController import com.android.systemui.qs.composefragment.SceneKeys.QuickQuickSettings import com.android.systemui.qs.composefragment.SceneKeys.QuickSettings import com.android.systemui.qs.composefragment.SceneKeys.toIdleSceneKey +import com.android.systemui.qs.composefragment.ui.NotificationScrimClipParams import com.android.systemui.qs.composefragment.ui.notificationScrimClip import com.android.systemui.qs.composefragment.ui.quickQuickSettingsToQuickSettings import com.android.systemui.qs.composefragment.viewmodel.QSFragmentComposeViewModel @@ -149,20 +150,12 @@ constructor( private val notificationScrimClippingParams = object { var isEnabled by mutableStateOf(false) - var leftInset by mutableStateOf(0) - var rightInset by mutableStateOf(0) - var top by mutableStateOf(0) - var bottom by mutableStateOf(0) - var radius by mutableStateOf(0) + var params by mutableStateOf(NotificationScrimClipParams()) fun dump(pw: IndentingPrintWriter) { pw.printSection("NotificationScrimClippingParams") { pw.println("isEnabled", isEnabled) - pw.println("leftInset", "${leftInset}px") - pw.println("rightInset", "${rightInset}px") - pw.println("top", "${top}px") - pw.println("bottom", "${bottom}px") - pw.println("radius", "${radius}px") + pw.println("params", params) } } } @@ -216,7 +209,7 @@ constructor( FrameLayoutTouchPassthrough( context, { notificationScrimClippingParams.isEnabled }, - { notificationScrimClippingParams.top }, + { notificationScrimClippingParams.params.top }, ) frame.addView( composeView, @@ -237,13 +230,7 @@ constructor( Modifier.windowInsetsPadding(WindowInsets.navigationBars).thenIf( notificationScrimClippingParams.isEnabled ) { - Modifier.notificationScrimClip( - notificationScrimClippingParams.leftInset, - notificationScrimClippingParams.top, - notificationScrimClippingParams.rightInset, - notificationScrimClippingParams.bottom, - notificationScrimClippingParams.radius, - ) + Modifier.notificationScrimClip { notificationScrimClippingParams.params } }, ) { val isEditing by @@ -445,13 +432,14 @@ constructor( fullWidth: Boolean, ) { notificationScrimClippingParams.isEnabled = visible - notificationScrimClippingParams.top = top - notificationScrimClippingParams.bottom = bottom - // Full width means that QS will show in the entire width allocated to it (for example - // phone) vs. showing in a narrower column (for example, tablet portrait). - notificationScrimClippingParams.leftInset = if (fullWidth) 0 else leftInset - notificationScrimClippingParams.rightInset = if (fullWidth) 0 else rightInset - notificationScrimClippingParams.radius = cornerRadius + notificationScrimClippingParams.params = + NotificationScrimClipParams( + top, + bottom, + if (fullWidth) 0 else leftInset, + if (fullWidth) 0 else rightInset, + cornerRadius, + ) } override fun isFullyCollapsed(): Boolean { diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt index 93c6445b78ef..c912bd59c19f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt @@ -31,87 +31,73 @@ import androidx.compose.ui.platform.InspectorInfo * ([ClipOp.Difference]) a `RoundRect(-leftInset, top, width + rightInset, bottom, radius, radius)` * from the QS container. */ -fun Modifier.notificationScrimClip( - leftInset: Int, - top: Int, - rightInset: Int, - bottom: Int, - radius: Int -): Modifier { - return this then NotificationScrimClipElement(leftInset, top, rightInset, bottom, radius) +fun Modifier.notificationScrimClip(clipParams: () -> NotificationScrimClipParams): Modifier { + return this then NotificationScrimClipElement(clipParams) } -private class NotificationScrimClipNode( - var leftInset: Float, - var top: Float, - var rightInset: Float, - var bottom: Float, - var radius: Float, -) : DrawModifierNode, Modifier.Node() { +private class NotificationScrimClipNode(var clipParams: () -> NotificationScrimClipParams) : + DrawModifierNode, Modifier.Node() { private val path = Path() - var invalidated = true + private var lastClipParams = NotificationScrimClipParams() override fun ContentDrawScope.draw() { - if (invalidated) { + val newClipParams = clipParams() + if (newClipParams != lastClipParams) { + lastClipParams = newClipParams + applyClipParams(path, lastClipParams) + } + clipPath(path, ClipOp.Difference) { this@draw.drawContent() } + } + + private fun ContentDrawScope.applyClipParams( + path: Path, + clipParams: NotificationScrimClipParams, + ) { + with(clipParams) { path.rewind() path .asAndroidPath() .addRoundRect( - -leftInset, - top, + -leftInset.toFloat(), + top.toFloat(), size.width + rightInset, - bottom, - radius, - radius, - android.graphics.Path.Direction.CW + bottom.toFloat(), + radius.toFloat(), + radius.toFloat(), + android.graphics.Path.Direction.CW, ) - invalidated = false } - clipPath(path, ClipOp.Difference) { this@draw.drawContent() } } } -private data class NotificationScrimClipElement( - val leftInset: Int, - val top: Int, - val rightInset: Int, - val bottom: Int, - val radius: Int, -) : ModifierNodeElement<NotificationScrimClipNode>() { +private data class NotificationScrimClipElement(val clipParams: () -> NotificationScrimClipParams) : + ModifierNodeElement<NotificationScrimClipNode>() { override fun create(): NotificationScrimClipNode { - return NotificationScrimClipNode( - leftInset.toFloat(), - top.toFloat(), - rightInset.toFloat(), - bottom.toFloat(), - radius.toFloat(), - ) + return NotificationScrimClipNode(clipParams) } override fun update(node: NotificationScrimClipNode) { - val changed = - node.leftInset != leftInset.toFloat() || - node.top != top.toFloat() || - node.rightInset != rightInset.toFloat() || - node.bottom != bottom.toFloat() || - node.radius != radius.toFloat() - if (changed) { - node.leftInset = leftInset.toFloat() - node.top = top.toFloat() - node.rightInset = rightInset.toFloat() - node.bottom = bottom.toFloat() - node.radius = radius.toFloat() - node.invalidated = true - } + node.clipParams = clipParams } override fun InspectorInfo.inspectableProperties() { name = "notificationScrimClip" - properties["leftInset"] = leftInset - properties["top"] = top - properties["rightInset"] = rightInset - properties["bottom"] = bottom - properties["radius"] = radius + with(clipParams()) { + properties["leftInset"] = leftInset + properties["top"] = top + properties["rightInset"] = rightInset + properties["bottom"] = bottom + properties["radius"] = radius + } } } + +/** Params for [notificationScrimClip]. */ +data class NotificationScrimClipParams( + val top: Int = 0, + val bottom: Int = 0, + val leftInset: Int = 0, + val rightInset: Int = 0, + val radius: Int = 0, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java index 73ad0e50793a..da04f6edf9e2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java @@ -46,6 +46,7 @@ import com.android.internal.jank.InteractionJankMonitor.Configuration; import com.android.internal.logging.UiEventLogger; import com.android.keyguard.KeyguardClockSwitch; import com.android.systemui.DejankUtils; +import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor; import com.android.systemui.deviceentry.shared.model.DeviceUnlockStatus; @@ -123,6 +124,7 @@ public class StatusBarStateControllerImpl implements private final Lazy<SceneContainerOcclusionInteractor> mSceneContainerOcclusionInteractorLazy; private final Lazy<KeyguardClockInteractor> mKeyguardClockInteractorLazy; private final Lazy<SceneBackInteractor> mSceneBackInteractorLazy; + private final Lazy<AlternateBouncerInteractor> mAlternateBouncerInteractorLazy; private int mState; private int mLastState; private int mUpcomingState; @@ -193,7 +195,8 @@ public class StatusBarStateControllerImpl implements Lazy<SceneInteractor> sceneInteractorLazy, Lazy<SceneContainerOcclusionInteractor> sceneContainerOcclusionInteractor, Lazy<KeyguardClockInteractor> keyguardClockInteractorLazy, - Lazy<SceneBackInteractor> sceneBackInteractorLazy) { + Lazy<SceneBackInteractor> sceneBackInteractorLazy, + Lazy<AlternateBouncerInteractor> alternateBouncerInteractorLazy) { mUiEventLogger = uiEventLogger; mInteractionJankMonitorLazy = interactionJankMonitorLazy; mJavaAdapter = javaAdapter; @@ -205,6 +208,7 @@ public class StatusBarStateControllerImpl implements mSceneContainerOcclusionInteractorLazy = sceneContainerOcclusionInteractor; mKeyguardClockInteractorLazy = keyguardClockInteractorLazy; mSceneBackInteractorLazy = sceneBackInteractorLazy; + mAlternateBouncerInteractorLazy = alternateBouncerInteractorLazy; for (int i = 0; i < HISTORY_SIZE; i++) { mHistoricalRecords[i] = new HistoricalState(); } @@ -233,6 +237,7 @@ public class StatusBarStateControllerImpl implements mSceneInteractorLazy.get().getCurrentOverlays(), mSceneBackInteractorLazy.get().getBackStack(), mSceneContainerOcclusionInteractorLazy.get().getInvisibleDueToOcclusion(), + mAlternateBouncerInteractorLazy.get().isVisible(), this::calculateStateFromSceneFramework), this::onStatusBarStateChanged); @@ -693,7 +698,8 @@ public class StatusBarStateControllerImpl implements SceneKey currentScene, Set<OverlayKey> currentOverlays, SceneStack backStack, - boolean isOccluded) { + boolean isOccluded, + boolean alternateBouncerIsVisible) { SceneContainerFlag.isUnexpectedlyInLegacyMode(); final boolean onBouncer = currentScene.equals(Scenes.Bouncer); @@ -714,7 +720,8 @@ public class StatusBarStateControllerImpl implements final String inputLogString = "currentScene=" + currentScene.getTestTag() + " currentOverlays=" + currentOverlays + " backStack=" + backStack - + " isUnlocked=" + isUnlocked + " isOccluded=" + isOccluded; + + " isUnlocked=" + isUnlocked + " isOccluded=" + isOccluded + + " alternateBouncerIsVisible=" + alternateBouncerIsVisible; int newState; @@ -722,6 +729,7 @@ public class StatusBarStateControllerImpl implements // 1. deviceUnlockStatus.isUnlocked changes from false to true. // 2. Lockscreen changes to Gone, either in currentScene or in backStack. // 3. Bouncer is removed from currentScene or backStack, if it was present. + // 4. the alternate bouncer is hidden, if it was visible. // // From this function's perspective, though, deviceUnlockStatus, currentScene, and backStack // each update separately, and the relative order of those updates is not well-defined. This @@ -733,6 +741,7 @@ public class StatusBarStateControllerImpl implements // 1. deviceUnlockStatus.isUnlocked is false. // 2. currentScene is a keyguardish scene (Lockscreen, Bouncer, or Communal). // 3. backStack contains a keyguardish scene (Lockscreen or Communal). + // 4. the alternate bouncer is visible. final boolean onKeyguardish = onLockscreen || onBouncer || onCommunal; final boolean overKeyguardish = overLockscreen || overCommunal; @@ -741,7 +750,7 @@ public class StatusBarStateControllerImpl implements // Occlusion is special; even though the device is still technically on the lockscreen, // the UI behaves as if it is unlocked. newState = StatusBarState.SHADE; - } else if (onKeyguardish || overKeyguardish) { + } else if (onKeyguardish || overKeyguardish || alternateBouncerIsVisible) { // We get here if we are on or over a keyguardish scene, even if isUnlocked is true; we // want to return SHADE_LOCKED or KEYGUARD until we are also neither on nor over a // keyguardish scene. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt index dac01028ef64..1009028345de 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.connectivity import android.os.UserManager +import com.android.systemui.bluetooth.qsdialog.dagger.AudioSharingModule import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags.SIGNAL_CALLBACK_DEPRECATION import com.android.systemui.qs.QsEventLogger @@ -56,7 +57,7 @@ import dagger.Provides import dagger.multibindings.IntoMap import dagger.multibindings.StringKey -@Module +@Module(includes = [AudioSharingModule::class]) interface ConnectivityModule { /** Inject BluetoothTile into tileMap in QSModule */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt index cf238d553225..cd1642eee4b4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt @@ -22,15 +22,20 @@ import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogBufferFactory +import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.data.StatusBarDataLayerModule import com.android.systemui.statusbar.phone.LightBarController import com.android.systemui.statusbar.phone.StatusBarSignalPolicy import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLog import com.android.systemui.statusbar.ui.SystemBarUtilsProxyImpl +import com.android.systemui.statusbar.window.MultiDisplayStatusBarWindowControllerStore +import com.android.systemui.statusbar.window.SingleDisplayStatusBarWindowControllerStore import com.android.systemui.statusbar.window.StatusBarWindowController import com.android.systemui.statusbar.window.StatusBarWindowControllerImpl +import com.android.systemui.statusbar.window.StatusBarWindowControllerStore import dagger.Binds +import dagger.Lazy import dagger.Module import dagger.Provides import dagger.multibindings.ClassKey @@ -62,13 +67,19 @@ abstract class StatusBarModule { @ClassKey(StatusBarSignalPolicy::class) abstract fun bindStatusBarSignalPolicy(impl: StatusBarSignalPolicy): CoreStartable + @Binds + @SysUISingleton + abstract fun statusBarWindowControllerFactory( + implFactory: StatusBarWindowControllerImpl.Factory + ): StatusBarWindowController.Factory + companion object { @Provides @SysUISingleton - fun statusBarWindowController( - context: Context?, - viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager?, + fun defaultStatusBarWindowController( + context: Context, + viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager, factory: StatusBarWindowControllerImpl.Factory, ): StatusBarWindowController { return factory.create(context, viewCaptureAwareWindowManager) @@ -76,6 +87,33 @@ abstract class StatusBarModule { @Provides @SysUISingleton + fun windowControllerStore( + multiDisplayImplLazy: Lazy<MultiDisplayStatusBarWindowControllerStore>, + singleDisplayImplLazy: Lazy<SingleDisplayStatusBarWindowControllerStore>, + ): StatusBarWindowControllerStore { + return if (StatusBarConnectedDisplays.isEnabled) { + multiDisplayImplLazy.get() + } else { + singleDisplayImplLazy.get() + } + } + + @Provides + @SysUISingleton + @IntoMap + @ClassKey(MultiDisplayStatusBarWindowControllerStore::class) + fun multiDisplayControllerStoreAsCoreStartable( + storeLazy: Lazy<MultiDisplayStatusBarWindowControllerStore> + ): CoreStartable { + return if (StatusBarConnectedDisplays.isEnabled) { + storeLazy.get() + } else { + CoreStartable.NOP + } + } + + @Provides + @SysUISingleton @OngoingCallLog fun provideOngoingCallLogBuffer(factory: LogBufferFactory): LogBuffer { return factory.create("OngoingCall", 75) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt index f5cfc8c5b307..e0bf00fbe431 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt @@ -26,6 +26,7 @@ import android.net.NetworkCapabilities.TRANSPORT_CELLULAR import android.net.NetworkCapabilities.TRANSPORT_ETHERNET import android.net.NetworkCapabilities.TRANSPORT_WIFI import android.net.vcn.VcnTransportInfo +import android.net.vcn.VcnUtils import android.net.wifi.WifiInfo import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import androidx.annotation.ArrayRes @@ -161,7 +162,9 @@ constructor( defaultNetworkCapabilities .map { networkCapabilities -> networkCapabilities?.run { - val subId = (transportInfo as? VcnTransportInfo)?.subId + val subId = + VcnUtils.getSubIdFromVcnCaps(connectivityManager, networkCapabilities) + // Never return an INVALID_SUBSCRIPTION_ID (-1) if (subId != INVALID_SUBSCRIPTION_ID) { subId @@ -245,9 +248,9 @@ constructor( * info. */ fun NetworkCapabilities.getMainOrUnderlyingWifiInfo( - connectivityManager: ConnectivityManager, + connectivityManager: ConnectivityManager ): WifiInfo? { - val mainWifiInfo = this.getMainWifiInfo() + val mainWifiInfo = this.getMainWifiInfo(connectivityManager) if (mainWifiInfo != null) { return mainWifiInfo } @@ -264,7 +267,9 @@ constructor( // eventually traced to a wifi or carrier merged connection. So, check those underlying // networks for possible wifi information as well. See b/225902574. return this.underlyingNetworks?.firstNotNullOfOrNull { underlyingNetwork -> - connectivityManager.getNetworkCapabilities(underlyingNetwork)?.getMainWifiInfo() + connectivityManager + .getNetworkCapabilities(underlyingNetwork) + ?.getMainWifiInfo(connectivityManager) } } @@ -272,7 +277,9 @@ constructor( * Checks the network capabilities for wifi info, but does *not* check the underlying * networks. See [getMainOrUnderlyingWifiInfo]. */ - private fun NetworkCapabilities.getMainWifiInfo(): WifiInfo? { + private fun NetworkCapabilities.getMainWifiInfo( + connectivityManager: ConnectivityManager + ): WifiInfo? { // Wifi info can either come from a WIFI Transport, or from a CELLULAR transport for // virtual networks like VCN. val canHaveWifiInfo = @@ -286,7 +293,7 @@ constructor( // [com.android.settingslib.Utils.tryGetWifiInfoForVcn]. It's copied instead of // re-used because it makes the logic here clearer, and because the method will be // removed once this pipeline is fully launched. - is VcnTransportInfo -> currentTransportInfo.wifiInfo + is VcnTransportInfo -> VcnUtils.getWifiInfoFromVcnCaps(connectivityManager, this) is WifiInfo -> currentTransportInfo else -> null } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt index 421e5c45bbfe..e8dc93465685 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt @@ -16,8 +16,10 @@ package com.android.systemui.statusbar.window +import android.content.Context import android.view.View import android.view.ViewGroup +import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.fragments.FragmentHostManager import java.util.Optional @@ -73,4 +75,11 @@ interface StatusBarWindowController { * this#setForceStatusBarVisible} together and use some sort of ranking system instead. */ fun setOngoingProcessRequiresStatusBarVisible(visible: Boolean) + + interface Factory { + fun create( + context: Context, + viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager, + ): StatusBarWindowController + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java index 1ee7cf3490f4..d709e5a0cd6c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java @@ -354,11 +354,13 @@ public class StatusBarWindowControllerImpl implements StatusBarWindowController } @AssistedFactory - public interface Factory { + public interface Factory extends StatusBarWindowController.Factory { /** Creates a new instance. */ + @NonNull + @Override StatusBarWindowControllerImpl create( - Context context, - ViewCaptureAwareWindowManager viewCaptureAwareWindowManager); + @NonNull Context context, + @NonNull ViewCaptureAwareWindowManager viewCaptureAwareWindowManager); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt new file mode 100644 index 000000000000..5f30b3719aa7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt @@ -0,0 +1,117 @@ +/* + * 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.window + +import android.view.Display +import android.view.WindowManager +import com.android.app.viewcapture.ViewCaptureAwareWindowManager +import com.android.systemui.CoreStartable +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.display.data.repository.DisplayWindowPropertiesRepository +import com.android.systemui.statusbar.core.StatusBarConnectedDisplays +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** Store that allows to retrieve per display instances of [StatusBarWindowController]. */ +interface StatusBarWindowControllerStore { + /** + * The instance for the default/main display of the device. For example, on a phone or a tablet, + * the default display is the internal/built-in display of the device. + * + * Note that the id of the default display is [Display.DEFAULT_DISPLAY]. + */ + val defaultDisplay: StatusBarWindowController + + /** + * Returns an instance for a specific display id. + * + * @throws IllegalArgumentException if [displayId] doesn't match the id of any existing + * displays. + */ + fun forDisplay(displayId: Int): StatusBarWindowController +} + +@SysUISingleton +class MultiDisplayStatusBarWindowControllerStore +@Inject +constructor( + @Background private val backgroundApplicationScope: CoroutineScope, + private val controllerFactory: StatusBarWindowController.Factory, + private val displayWindowPropertiesRepository: DisplayWindowPropertiesRepository, + private val viewCaptureAwareWindowManagerFactory: ViewCaptureAwareWindowManager.Factory, + private val displayRepository: DisplayRepository, +) : StatusBarWindowControllerStore, CoreStartable { + + init { + StatusBarConnectedDisplays.assertInNewMode() + } + + private val perDisplayControllers = ConcurrentHashMap<Int, StatusBarWindowController>() + + override fun start() { + backgroundApplicationScope.launch(CoroutineName("StatusBarWindowController#start")) { + displayRepository.displayRemovalEvent.collect { displayId -> + perDisplayControllers.remove(displayId) + } + } + } + + override val defaultDisplay: StatusBarWindowController + get() = forDisplay(Display.DEFAULT_DISPLAY) + + override fun forDisplay(displayId: Int): StatusBarWindowController { + if (displayRepository.getDisplay(displayId) == null) { + throw IllegalArgumentException("Display with id $displayId doesn't exist.") + } + return perDisplayControllers.computeIfAbsent(displayId) { + createControllerForDisplay(displayId) + } + } + + private fun createControllerForDisplay(displayId: Int): StatusBarWindowController { + val statusBarDisplayContext = + displayWindowPropertiesRepository.get( + displayId = displayId, + windowType = WindowManager.LayoutParams.TYPE_STATUS_BAR, + ) + val viewCaptureAwareWindowManager = + viewCaptureAwareWindowManagerFactory.create(statusBarDisplayContext.windowManager) + return controllerFactory.create( + statusBarDisplayContext.context, + viewCaptureAwareWindowManager, + ) + } +} + +@SysUISingleton +class SingleDisplayStatusBarWindowControllerStore +@Inject +constructor(private val controller: StatusBarWindowController) : StatusBarWindowControllerStore { + + init { + StatusBarConnectedDisplays.assertInLegacyMode() + } + + override val defaultDisplay = controller + + override fun forDisplay(displayId: Int) = controller +} diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt index bfc5429b59d4..6879a3415238 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt @@ -27,10 +27,7 @@ import com.android.systemui.res.R import com.android.systemui.touchpad.tutorial.ui.gesture.BackGestureMonitor @Composable -fun BackGestureTutorialScreen( - onDoneButtonClicked: () -> Unit, - onBack: () -> Unit, -) { +fun BackGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) { val screenConfig = TutorialScreenConfig( colors = rememberScreenColors(), @@ -39,18 +36,20 @@ fun BackGestureTutorialScreen( titleResId = R.string.touchpad_back_gesture_action_title, bodyResId = R.string.touchpad_back_gesture_guidance, titleSuccessResId = R.string.touchpad_back_gesture_success_title, - bodySuccessResId = R.string.touchpad_back_gesture_success_body + bodySuccessResId = R.string.touchpad_back_gesture_success_body, ), animations = TutorialScreenConfig.Animations( educationResId = R.raw.trackpad_back_edu, - successResId = R.raw.trackpad_back_success - ) + successResId = R.raw.trackpad_back_success, + ), ) val gestureMonitorProvider = DistanceBasedGestureMonitorProvider( monitorFactory = { distanceThresholdPx, gestureStateCallback -> - BackGestureMonitor(distanceThresholdPx, gestureStateCallback) + BackGestureMonitor(distanceThresholdPx).also { + it.addGestureStateCallback(gestureStateCallback) + } } ) GestureTutorialScreen(screenConfig, gestureMonitorProvider, onDoneButtonClicked, onBack) @@ -67,7 +66,7 @@ private fun rememberScreenColors(): TutorialScreenConfig.Colors { rememberColorFilterProperty(".tertiaryFixedDim", tertiaryFixedDim), rememberColorFilterProperty(".onTertiaryFixed", onTertiaryFixed), rememberColorFilterProperty(".onTertiary", onTertiary), - rememberColorFilterProperty(".onTertiaryFixedVariant", onTertiaryFixedVariant) + rememberColorFilterProperty(".onTertiaryFixedVariant", onTertiaryFixedVariant), ) val screenColors = remember(dynamicProperties) { diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt index f2fec5f5d9b1..a55fa442cd96 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt @@ -26,10 +26,7 @@ import com.android.systemui.res.R import com.android.systemui.touchpad.tutorial.ui.gesture.HomeGestureMonitor @Composable -fun HomeGestureTutorialScreen( - onDoneButtonClicked: () -> Unit, - onBack: () -> Unit, -) { +fun HomeGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) { val screenConfig = TutorialScreenConfig( colors = rememberScreenColors(), @@ -38,18 +35,20 @@ fun HomeGestureTutorialScreen( titleResId = R.string.touchpad_home_gesture_action_title, bodyResId = R.string.touchpad_home_gesture_guidance, titleSuccessResId = R.string.touchpad_home_gesture_success_title, - bodySuccessResId = R.string.touchpad_home_gesture_success_body + bodySuccessResId = R.string.touchpad_home_gesture_success_body, ), animations = TutorialScreenConfig.Animations( educationResId = R.raw.trackpad_home_edu, - successResId = R.raw.trackpad_home_success - ) + successResId = R.raw.trackpad_home_success, + ), ) val gestureMonitorProvider = DistanceBasedGestureMonitorProvider( monitorFactory = { distanceThresholdPx, gestureStateCallback -> - HomeGestureMonitor(distanceThresholdPx, gestureStateCallback) + HomeGestureMonitor(distanceThresholdPx).also { + it.addGestureStateCallback(gestureStateCallback) + } } ) GestureTutorialScreen(screenConfig, gestureMonitorProvider, onDoneButtonClicked, onBack) @@ -64,7 +63,7 @@ private fun rememberScreenColors(): TutorialScreenConfig.Colors { rememberLottieDynamicProperties( rememberColorFilterProperty(".primaryFixedDim", primaryFixedDim), rememberColorFilterProperty(".onPrimaryFixed", onPrimaryFixed), - rememberColorFilterProperty(".onPrimaryFixedVariant", onPrimaryFixedVariant) + rememberColorFilterProperty(".onPrimaryFixedVariant", onPrimaryFixedVariant), ) val screenColors = remember(dynamicProperties) { diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt index b2fb6cdfcee5..6ee15aa952f4 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt @@ -29,10 +29,7 @@ import com.android.systemui.touchpad.tutorial.ui.gesture.RecentAppsGestureMonito import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureMonitor @Composable -fun RecentAppsGestureTutorialScreen( - onDoneButtonClicked: () -> Unit, - onBack: () -> Unit, -) { +fun RecentAppsGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) { val screenConfig = TutorialScreenConfig( colors = rememberScreenColors(), @@ -41,20 +38,20 @@ fun RecentAppsGestureTutorialScreen( titleResId = R.string.touchpad_recent_apps_gesture_action_title, bodyResId = R.string.touchpad_recent_apps_gesture_guidance, titleSuccessResId = R.string.touchpad_recent_apps_gesture_success_title, - bodySuccessResId = R.string.touchpad_recent_apps_gesture_success_body + bodySuccessResId = R.string.touchpad_recent_apps_gesture_success_body, ), animations = TutorialScreenConfig.Animations( educationResId = R.raw.trackpad_recent_apps_edu, - successResId = R.raw.trackpad_recent_apps_success - ) + successResId = R.raw.trackpad_recent_apps_success, + ), ) val gestureMonitorProvider = object : GestureMonitorProvider { @Composable override fun rememberGestureMonitor( resources: Resources, - gestureStateChangedCallback: (GestureState) -> Unit + gestureStateChangedCallback: (GestureState) -> Unit, ): TouchpadGestureMonitor { val distanceThresholdPx = resources.getDimensionPixelSize( @@ -63,11 +60,9 @@ fun RecentAppsGestureTutorialScreen( val velocityThresholdPxPerMs = resources.getDimension(R.dimen.touchpad_recent_apps_gesture_velocity_threshold) return remember(distanceThresholdPx, velocityThresholdPxPerMs) { - RecentAppsGestureMonitor( - distanceThresholdPx, - gestureStateChangedCallback, - velocityThresholdPxPerMs - ) + RecentAppsGestureMonitor(distanceThresholdPx, velocityThresholdPxPerMs).also { + it.addGestureStateCallback(gestureStateChangedCallback) + } } } } @@ -83,7 +78,7 @@ private fun rememberScreenColors(): TutorialScreenConfig.Colors { rememberLottieDynamicProperties( rememberColorFilterProperty(".secondaryFixedDim", secondaryFixedDim), rememberColorFilterProperty(".onSecondaryFixed", onSecondaryFixed), - rememberColorFilterProperty(".onSecondaryFixedVariant", onSecondaryFixedVariant) + rememberColorFilterProperty(".onSecondaryFixedVariant", onSecondaryFixedVariant), ) val screenColors = remember(dynamicProperties) { 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 94e19deb0006..3c31efa6265a 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 @@ -84,7 +84,7 @@ private fun TutorialSelectionButtons( ) { TutorialButton( text = stringResource(R.string.touchpad_tutorial_home_gesture_button), - icon = Icons.AutoMirrored.Outlined.ArrowBack, + icon = ImageVector.vectorResource(id = R.drawable.touchpad_tutorial_home_icon), iconColor = MaterialTheme.colorScheme.onPrimary, onClick = onHomeTutorialClicked, backgroundColor = MaterialTheme.colorScheme.primary, @@ -92,7 +92,7 @@ private fun TutorialSelectionButtons( ) TutorialButton( text = stringResource(R.string.touchpad_tutorial_back_gesture_button), - icon = ImageVector.vectorResource(id = R.drawable.touchpad_tutorial_home_icon), + icon = Icons.AutoMirrored.Outlined.ArrowBack, iconColor = MaterialTheme.colorScheme.onTertiary, onClick = onBackTutorialClicked, backgroundColor = MaterialTheme.colorScheme.tertiary, diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt index ecb5574ba5df..490f04d55802 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt @@ -20,18 +20,21 @@ import android.view.MotionEvent import kotlin.math.abs /** Monitors for touchpad back gesture, that is three fingers swiping left or right */ -class BackGestureMonitor( - private val gestureDistanceThresholdPx: Int, - override val gestureStateChangedCallback: (GestureState) -> Unit, -) : TouchpadGestureMonitor { +class BackGestureMonitor(private val gestureDistanceThresholdPx: Int) : TouchpadGestureMonitor { + private val distanceTracker = DistanceTracker() + private var gestureStateChangedCallback: (GestureState) -> Unit = {} + + override fun addGestureStateCallback(callback: (GestureState) -> Unit) { + gestureStateChangedCallback = callback + } - override fun processTouchpadEvent(event: MotionEvent) { + override fun accept(event: MotionEvent) { if (!isThreeFingerTouchpadSwipe(event)) return - val distanceState = distanceTracker.processEvent(event) - updateGestureStateBasedOnDistance( + val gestureState = distanceTracker.processEvent(event) + updateGestureState( gestureStateChangedCallback, - distanceState, + gestureState, isFinished = { abs(it.deltaX) >= gestureDistanceThresholdPx }, progress = { 0f }, ) diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/DistanceTracker.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/DistanceTracker.kt index 70d93668c6b3..d48235892d69 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/DistanceTracker.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/DistanceTracker.kt @@ -38,10 +38,10 @@ class DistanceTracker(var startX: Float = 0f, var startY: Float = 0f) { } } -sealed interface DistanceGestureState +sealed class DistanceGestureState(val deltaX: Float, val deltaY: Float) -class Started(val deltaX: Float, val deltaY: Float) : DistanceGestureState +class Started(deltaX: Float, deltaY: Float) : DistanceGestureState(deltaX, deltaY) -class Moving(val deltaX: Float, val deltaY: Float) : DistanceGestureState +class Moving(deltaX: Float, deltaY: Float) : DistanceGestureState(deltaX, deltaY) -class Finished(val deltaX: Float, val deltaY: Float) : DistanceGestureState +class Finished(deltaX: Float, deltaY: Float) : DistanceGestureState(deltaX, deltaY) diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt index c1caeb3cbf9e..f19467726def 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt @@ -16,11 +16,8 @@ package com.android.systemui.touchpad.tutorial.ui.gesture -/** - * Helper function for gesture recognizers to have common state triggering logic based on distance - * only. - */ -inline fun updateGestureStateBasedOnDistance( +/** Helper function for gesture recognizers to have common state triggering logic */ +inline fun updateGestureState( gestureStateChangedCallback: (GestureState) -> Unit, gestureState: DistanceGestureState?, isFinished: (Finished) -> Boolean, diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitor.kt index fdcf9deec923..83d4f566257b 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitor.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitor.kt @@ -19,18 +19,21 @@ package com.android.systemui.touchpad.tutorial.ui.gesture import android.view.MotionEvent /** Monitors for touchpad home gesture, that is three fingers swiping up */ -class HomeGestureMonitor( - private val gestureDistanceThresholdPx: Int, - override val gestureStateChangedCallback: (GestureState) -> Unit, -) : TouchpadGestureMonitor { +class HomeGestureMonitor(private val gestureDistanceThresholdPx: Int) : TouchpadGestureMonitor { + private val distanceTracker = DistanceTracker() + private var gestureStateChangedCallback: (GestureState) -> Unit = {} + + override fun addGestureStateCallback(callback: (GestureState) -> Unit) { + gestureStateChangedCallback = callback + } - override fun processTouchpadEvent(event: MotionEvent) { + override fun accept(event: MotionEvent) { if (!isThreeFingerTouchpadSwipe(event)) return - val distanceState = distanceTracker.processEvent(event) - updateGestureStateBasedOnDistance( + val gestureState = distanceTracker.processEvent(event) + updateGestureState( gestureStateChangedCallback, - distanceState, + gestureState, isFinished = { -it.deltaY >= gestureDistanceThresholdPx }, progress = { 0f }, ) diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitor.kt index dd31ce309cce..1731bb85fba4 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitor.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitor.kt @@ -17,7 +17,6 @@ package com.android.systemui.touchpad.tutorial.ui.gesture import android.view.MotionEvent -import androidx.compose.ui.input.pointer.util.VelocityTracker1D import kotlin.math.abs /** @@ -27,45 +26,30 @@ import kotlin.math.abs */ class RecentAppsGestureMonitor( private val gestureDistanceThresholdPx: Int, - override val gestureStateChangedCallback: (GestureState) -> Unit, private val velocityThresholdPxPerMs: Float, - private val velocityTracker: VelocityTracker1D = VelocityTracker1D(isDataDifferential = false), + private val distanceTracker: DistanceTracker = DistanceTracker(), + private val velocityTracker: VerticalVelocityTracker = VerticalVelocityTracker(), ) : TouchpadGestureMonitor { - private var xStart = 0f - private var yStart = 0f + private var gestureStateChangedCallback: (GestureState) -> Unit = {} - override fun processTouchpadEvent(event: MotionEvent) { - val action = event.actionMasked - velocityTracker.addDataPoint(event.eventTime, event.y) - when (action) { - MotionEvent.ACTION_DOWN -> { - if (isThreeFingerTouchpadSwipe(event)) { - xStart = event.x - yStart = event.y - gestureStateChangedCallback(GestureState.InProgress()) - } - } - MotionEvent.ACTION_UP -> { - if (isThreeFingerTouchpadSwipe(event) && isRecentAppsGesture(event)) { - gestureStateChangedCallback(GestureState.Finished) - } else { - gestureStateChangedCallback(GestureState.NotStarted) - } - velocityTracker.resetTracking() - } - MotionEvent.ACTION_CANCEL -> { - velocityTracker.resetTracking() - } - } + override fun addGestureStateCallback(callback: (GestureState) -> Unit) { + gestureStateChangedCallback = callback } - private fun isRecentAppsGesture(event: MotionEvent): Boolean { - // below is trying to mirror behavior of TriggerSwipeUpTouchTracker#onGestureEnd. - // We're diving velocity by 1000, to have the same unit of measure: pixels/ms. - val swipeDistance = yStart - event.y - val velocity = velocityTracker.calculateVelocity() / 1000 - return swipeDistance >= gestureDistanceThresholdPx && - abs(velocity) <= velocityThresholdPxPerMs + override fun accept(event: MotionEvent) { + if (!isThreeFingerTouchpadSwipe(event)) return + val gestureState = distanceTracker.processEvent(event) + velocityTracker.accept(event) + + updateGestureState( + gestureStateChangedCallback, + gestureState, + isFinished = { state -> + -state.deltaY >= gestureDistanceThresholdPx && + abs(velocityTracker.calculateVelocity().value) <= velocityThresholdPxPerMs + }, + progress = { 0f }, + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt index 88671d41f1cd..4b82ba1c0dda 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt @@ -18,13 +18,14 @@ package com.android.systemui.touchpad.tutorial.ui.gesture import android.view.InputDevice import android.view.MotionEvent +import java.util.function.Consumer /** * Allows listening to touchpadGesture and calling onDone when gesture was triggered. Can have all * motion events passed to [onMotionEvent] and will filter touchpad events accordingly */ class TouchpadGestureHandler( - private val gestureMonitor: TouchpadGestureMonitor, + private val gestureMonitor: Consumer<MotionEvent>, private val easterEggGestureMonitor: EasterEggGestureMonitor, ) { @@ -40,7 +41,7 @@ class TouchpadGestureHandler( if (isTwoFingerSwipe(event)) { easterEggGestureMonitor.processTouchpadEvent(event) } else { - gestureMonitor.processTouchpadEvent(event) + gestureMonitor.accept(event) } true } else { diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureMonitor.kt index 4655c98b65ac..9216821272ff 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureMonitor.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureMonitor.kt @@ -17,15 +17,11 @@ package com.android.systemui.touchpad.tutorial.ui.gesture import android.view.MotionEvent +import java.util.function.Consumer -/** - * Monitor for touchpad gestures that calls [gestureStateChangedCallback] when [GestureState] - * changes. All tracked motion events should be passed to [processTouchpadEvent] - */ -interface TouchpadGestureMonitor { - val gestureStateChangedCallback: (GestureState) -> Unit - - fun processTouchpadEvent(event: MotionEvent) +/** Monitor for touchpad gestures that can notify callback when [GestureState] changes. */ +interface TouchpadGestureMonitor : Consumer<MotionEvent> { + fun addGestureStateCallback(callback: (GestureState) -> Unit) } fun isThreeFingerTouchpadSwipe(event: MotionEvent) = isNFingerTouchpadSwipe(event, fingerCount = 3) diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/VelocityTracker.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/VelocityTracker.kt new file mode 100644 index 000000000000..9b38eca89f15 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/VelocityTracker.kt @@ -0,0 +1,51 @@ +/* + * 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.touchpad.tutorial.ui.gesture + +import android.view.MotionEvent +import androidx.compose.ui.input.pointer.util.VelocityTracker1D +import java.util.function.Consumer + +/** Velocity in pixels/ms. */ +@JvmInline value class Velocity(val value: Float) + +/** + * Tracks velocity for processed MotionEvents. Useful for recognizing gestures based on velocity. + */ +interface VelocityTracker : Consumer<MotionEvent> { + + fun calculateVelocity(): Velocity +} + +class VerticalVelocityTracker( + private val velocityTracker: VelocityTracker1D = VelocityTracker1D(isDataDifferential = false) +) : VelocityTracker { + + override fun accept(event: MotionEvent) { + val action = event.actionMasked + if (action == MotionEvent.ACTION_DOWN) { + velocityTracker.resetTracking() + } + velocityTracker.addDataPoint(event.eventTime, event.y) + } + + /** + * Calculates velocity on demand - this calculation can be expensive so shouldn't be called + * after every event. + */ + override fun calculateVelocity() = Velocity(velocityTracker.calculateVelocity() / 1000) +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java index 1f92bc1df9c8..bbd8f3dc7e82 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java @@ -59,7 +59,6 @@ import android.view.accessibility.CaptioningManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; import androidx.lifecycle.Observer; import com.android.internal.annotations.GuardedBy; @@ -110,8 +109,8 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa // It is safe to use 99 as the broadcast stream now. There are only 10+ default audio // streams defined in AudioSystem for now and audio team is in the middle of restructure, // no new default stream is preferred. - @VisibleForTesting static final int DYNAMIC_STREAM_BROADCAST = 99; - private static final int DYNAMIC_STREAM_REMOTE_START_INDEX = 100; + public static final int DYNAMIC_STREAM_BROADCAST = 99; + public static final int DYNAMIC_STREAM_REMOTE_START_INDEX = 100; private static final AudioAttributes SONIFICIATION_VIBRATION_ATTRIBUTES = new AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 7166428d863f..7c5116dbf72c 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -537,7 +537,7 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, mWindow.setAttributes(lp); mWindow.setLayout(WRAP_CONTENT, WRAP_CONTENT); - mDialog.setContentView(R.layout.volume_dialog); + mDialog.setContentView(R.layout.volume_dialog_legacy); mDialogView = mDialog.findViewById(R.id.volume_dialog); mDialogView.setAlpha(0); mDialogTimeoutMillis = mSecureSettings.get().getInt( diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStateModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStateModel.kt index f1443e36d019..500cc0bf748c 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStateModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStateModel.kt @@ -23,7 +23,7 @@ import com.android.systemui.plugins.VolumeDialogController /** Models a state of the Volume Dialog. */ data class VolumeDialogStateModel( - val states: Map<Int, VolumeDialogStreamStateModel>, + val states: Map<Int, VolumeDialogStreamModel>, val ringerModeInternal: Int = 0, val ringerModeExternal: Int = 0, val zenMode: Int = 0, @@ -39,7 +39,7 @@ data class VolumeDialogStateModel( constructor( legacyState: VolumeDialogController.State ) : this( - states = legacyState.states.mapToMap { VolumeDialogStreamStateModel(it) }, + states = legacyState.states.mapToMap { VolumeDialogStreamModel(it) }, ringerModeInternal = legacyState.ringerModeInternal, ringerModeExternal = legacyState.ringerModeExternal, zenMode = legacyState.zenMode, diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStreamStateModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStreamModel.kt index a9d367da41e4..26c96eabc65f 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStreamStateModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStreamModel.kt @@ -16,18 +16,18 @@ package com.android.systemui.volume.dialog.domain.model -import android.annotation.IntegerRes +import androidx.annotation.StringRes import com.android.systemui.plugins.VolumeDialogController /** Models a state of an audio stream of the Volume Dialog. */ -data class VolumeDialogStreamStateModel( +data class VolumeDialogStreamModel( val isDynamic: Boolean = false, val level: Int = 0, val levelMin: Int = 0, val levelMax: Int = 0, val muted: Boolean = false, val muteSupported: Boolean = false, - @IntegerRes val name: Int = 0, + @StringRes val name: Int = 0, val remoteLabel: String? = null, val routedToBluetooth: Boolean = false, ) { diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinder.kt index ba08876609ae..b2f6cb332e74 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinder.kt @@ -34,7 +34,7 @@ constructor(private val viewModelFactory: VolumeDialogSettingsButtonViewModel.Fa fun bind(view: View) { with(view) { - val button = requireViewById<View>(R.id.settings) + val button = requireViewById<View>(R.id.volume_dialog_settings) repeatWhenAttached { viewModel( traceName = "VolumeDialogViewBinder", diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt new file mode 100644 index 000000000000..81507ba7dc60 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt @@ -0,0 +1,54 @@ +/* + * 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.domain.interactor + +import com.android.systemui.plugins.VolumeDialogController +import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope +import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogStateInteractor +import com.android.systemui.volume.dialog.domain.model.VolumeDialogStreamModel +import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.mapNotNull + +/** Operates a state of particular slider of the Volume Dialog. */ +class VolumeDialogSliderInteractor +@AssistedInject +constructor( + @Assisted private val sliderType: VolumeDialogSliderType, + volumeDialogStateInteractor: VolumeDialogStateInteractor, + private val volumeDialogController: VolumeDialogController, +) { + + val slider: Flow<VolumeDialogStreamModel> = + volumeDialogStateInteractor.volumeDialogState.mapNotNull { + it.states[sliderType.audioStream] + } + + fun setStreamVolume(userLevel: Int) { + volumeDialogController.setStreamVolume(sliderType.audioStream, userLevel) + } + + @VolumeDialogScope + @AssistedFactory + interface Factory { + + fun create(sliderType: VolumeDialogSliderType): VolumeDialogSliderInteractor + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSlidersInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSlidersInteractor.kt new file mode 100644 index 000000000000..325e4c9514cf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSlidersInteractor.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.dialog.sliders.domain.interactor + +import com.android.systemui.volume.VolumeDialogControllerImpl +import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope +import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogStateInteractor +import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType +import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSlidersModel +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +/** Provides a state for the Sliders section of the Volume Dialog. */ +@VolumeDialogScope +class VolumeDialogSlidersInteractor +@Inject +constructor(volumeDialogStateInteractor: VolumeDialogStateInteractor) { + + val sliders: Flow<VolumeDialogSlidersModel> = + volumeDialogStateInteractor.volumeDialogState.map { + val sliderTypes: List<VolumeDialogSliderType> = + it.states.keys.sortedWith(StreamsSorter).map { audioStream -> + when { + audioStream == VolumeDialogControllerImpl.DYNAMIC_STREAM_BROADCAST -> + VolumeDialogSliderType.AudioSharingStream(audioStream) + audioStream >= + VolumeDialogControllerImpl.DYNAMIC_STREAM_REMOTE_START_INDEX -> + VolumeDialogSliderType.RemoteMediaStream(audioStream) + else -> VolumeDialogSliderType.Stream(audioStream) + } + } + VolumeDialogSlidersModel( + slider = sliderTypes.first(), + floatingSliders = sliderTypes.drop(1), + ) + } + + private object StreamsSorter : Comparator<Int> { + + // TODO(b/369992924) order the streams + override fun compare(lhs: Int, rhs: Int): Int { + return lhs - rhs + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/model/VolumeDialogSliderType.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/model/VolumeDialogSliderType.kt new file mode 100644 index 000000000000..18a26891e904 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/model/VolumeDialogSliderType.kt @@ -0,0 +1,32 @@ +/* + * 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.domain.model + +/** Models different possible audio sliders shown in the Volume Dialog. */ +sealed interface VolumeDialogSliderType { + + // VolumeDialogController uses the same model for every slider type. We need to follow the same + // logic until we refactor and decouple data and domain layers from the VolumeDialogController + // into separated interactors. + val audioStream: Int + + class Stream(override val audioStream: Int) : VolumeDialogSliderType + + class RemoteMediaStream(override val audioStream: Int) : VolumeDialogSliderType + + class AudioSharingStream(override val audioStream: Int) : VolumeDialogSliderType +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/model/VolumeDialogSlidersModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/model/VolumeDialogSlidersModel.kt new file mode 100644 index 000000000000..91a332830b75 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/model/VolumeDialogSlidersModel.kt @@ -0,0 +1,23 @@ +/* + * 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.domain.model + +/** Models a state of the sliders section of the Volume Dialog. */ +data class VolumeDialogSlidersModel( + val slider: VolumeDialogSliderType, + val floatingSliders: List<VolumeDialogSliderType>, +) 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 new file mode 100644 index 000000000000..25a5f287c21f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.dialog.sliders.ui + +import android.view.View +import androidx.lifecycle.viewmodel.compose.viewModel +import com.android.systemui.lifecycle.WindowLifecycleState +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.lifecycle.setSnapshotBinding +import com.android.systemui.lifecycle.viewModel +import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope +import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderViewModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.awaitCancellation + +class VolumeDialogSliderViewBinder +@AssistedInject +constructor(@Assisted private val viewModelProvider: () -> VolumeDialogSliderViewModel) { + + fun bind(view: View) { + with(view) { + repeatWhenAttached { + viewModel( + traceName = "VolumeDialogSliderViewBinder", + minWindowLifecycleState = WindowLifecycleState.ATTACHED, + factory = { viewModelProvider() }, + ) { viewModel -> + setSnapshotBinding {} + + awaitCancellation() + } + } + } + } + + @AssistedFactory + @VolumeDialogScope + interface Factory { + + fun create( + viewModelProvider: () -> VolumeDialogSliderViewModel + ): VolumeDialogSliderViewBinder + } +} 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 new file mode 100644 index 000000000000..0a00f70b54f1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt @@ -0,0 +1,49 @@ +/* + * 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.lifecycle.WindowLifecycleState +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.lifecycle.setSnapshotBinding +import com.android.systemui.lifecycle.viewModel +import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope +import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSlidersViewModel +import javax.inject.Inject +import kotlinx.coroutines.awaitCancellation + +@VolumeDialogScope +class VolumeDialogSlidersViewBinder +@Inject +constructor(private val viewModelFactory: VolumeDialogSlidersViewModel.Factory) { + + fun bind(view: View) { + with(view) { + repeatWhenAttached { + viewModel( + traceName = "VolumeDialogSlidersViewBinder", + minWindowLifecycleState = WindowLifecycleState.ATTACHED, + factory = { viewModelFactory.create() }, + ) { viewModel -> + setSnapshotBinding {} + + awaitCancellation() + } + } + } + } +} 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 new file mode 100644 index 000000000000..27b8f2f5adb7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt @@ -0,0 +1,41 @@ +/* + * 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 com.android.systemui.volume.dialog.domain.model.VolumeDialogStreamModel +import com.android.systemui.volume.dialog.sliders.domain.interactor.VolumeDialogSliderInteractor +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.Flow + +class VolumeDialogSliderViewModel +@AssistedInject +constructor(@Assisted private val interactor: VolumeDialogSliderInteractor) { + + val model: Flow<VolumeDialogStreamModel> = interactor.slider + + fun setStreamVolume(volume: Int) { + interactor.setStreamVolume(volume) + } + + @AssistedFactory + interface Factory { + + fun create(interactor: VolumeDialogSliderInteractor): VolumeDialogSliderViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModel.kt new file mode 100644 index 000000000000..b5b292fa4a66 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModel.kt @@ -0,0 +1,73 @@ +/* + * 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 com.android.systemui.volume.dialog.dagger.scope.VolumeDialog +import com.android.systemui.volume.dialog.sliders.domain.interactor.VolumeDialogSliderInteractor +import com.android.systemui.volume.dialog.sliders.domain.interactor.VolumeDialogSlidersInteractor +import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType +import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderViewBinder +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +class VolumeDialogSlidersViewModel +@AssistedInject +constructor( + @VolumeDialog coroutineScope: CoroutineScope, + private val slidersInteractor: VolumeDialogSlidersInteractor, + private val sliderInteractorFactory: VolumeDialogSliderInteractor.Factory, + private val sliderViewModelFactory: VolumeDialogSliderViewModel.Factory, + private val sliderViewBinderFactory: VolumeDialogSliderViewBinder.Factory, +) { + + val sliders: Flow<VolumeDialogSliderUiModel> = + slidersInteractor.sliders + .distinctUntilChanged() + .map { slidersModel -> + VolumeDialogSliderUiModel( + sliderViewBinder = createSliderViewBinder(slidersModel.slider), + floatingSliderViewBinders = + slidersModel.floatingSliders.map(::createSliderViewBinder), + ) + } + .stateIn(coroutineScope, SharingStarted.Eagerly, null) + .filterNotNull() + + private fun createSliderViewBinder(type: VolumeDialogSliderType): VolumeDialogSliderViewBinder = + sliderViewBinderFactory.create { + sliderViewModelFactory.create(sliderInteractorFactory.create(type)) + } + + @AssistedFactory + interface Factory { + + fun create(): VolumeDialogSlidersViewModel + } +} + +/** Models slider ui */ +data class VolumeDialogSliderUiModel( + val sliderViewBinder: VolumeDialogSliderViewBinder, + val floatingSliderViewBinders: List<VolumeDialogSliderViewBinder>, +) diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt index 9452d8c0dcd4..77733fe33275 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt @@ -50,7 +50,7 @@ constructor( dialog.setContentView(R.layout.volume_dialog) dialog.setCanceledOnTouchOutside(true) - settingsButtonViewBinder.bind(dialog.requireViewById(R.id.settings_container)) + settingsButtonViewBinder.bind(dialog.requireViewById(R.id.volume_dialog_settings)) volumeDialogViewBinder.bind( dialog, dialog.requireViewById(R.id.volume_dialog_container), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt index 7889b3cd6cc3..7889b3cd6cc3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt index 9fbe09619ff1..9fbe09619ff1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelTest.kt new file mode 100644 index 000000000000..655b2cc2dece --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelTest.kt @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.bluetooth.qsdialog + +import android.testing.TestableLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession +import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.settingslib.bluetooth.BluetoothUtils +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn +import com.android.systemui.res.R +import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock + +@ExperimentalCoroutinesApi +@SmallTest +@RunWith(AndroidJUnit4::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +class AudioSharingButtonViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val bluetoothState = MutableStateFlow(false) + private val deviceItemUpdate: MutableSharedFlow<List<DeviceItem>> = MutableSharedFlow() + @Mock private lateinit var cachedBluetoothDevice: CachedBluetoothDevice + @Mock private lateinit var localBluetoothManager: LocalBluetoothManager + @Mock private lateinit var bluetoothStateInteractor: BluetoothStateInteractor + @Mock private lateinit var deviceItemInteractor: DeviceItemInteractor + @Mock private lateinit var deviceItem: DeviceItem + private lateinit var mockitoSession: StaticMockitoSession + private lateinit var audioSharingButtonViewModel: AudioSharingButtonViewModel + + @Before + fun setUp() { + mockitoSession = + mockitoSession().initMocks(this).mockStatic(BluetoothUtils::class.java).startMocking() + whenever(bluetoothStateInteractor.bluetoothStateUpdate).thenReturn(bluetoothState) + whenever(deviceItemInteractor.deviceItemUpdate).thenReturn(deviceItemUpdate) + audioSharingButtonViewModel = + AudioSharingButtonViewModel( + localBluetoothManager, + kosmos.audioSharingInteractor, + bluetoothStateInteractor, + deviceItemInteractor, + ) + audioSharingButtonViewModel.activateIn(testScope) + } + + @After + fun tearDown() { + mockitoSession.finishMocking() + } + + @Test + fun testButtonStateUpdate_bluetoothOff_returnGone() { + testScope.runTest { + val actual by + collectLastValue(audioSharingButtonViewModel.audioSharingButtonStateUpdate) + kosmos.bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) + + runCurrent() + + assertThat(actual).isEqualTo(AudioSharingButtonState.Gone) + } + } + + @Test + fun testButtonStateUpdate_noDevice_returnGone() { + testScope.runTest { + val actual by + collectLastValue(audioSharingButtonViewModel.audioSharingButtonStateUpdate) + kosmos.bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) + bluetoothState.value = true + runCurrent() + + assertThat(actual).isEqualTo(AudioSharingButtonState.Gone) + } + } + + @Test + fun testButtonStateUpdate_isBroadcasting_returnSharingAudio() { + testScope.runTest { + val actual by + collectLastValue(audioSharingButtonViewModel.audioSharingButtonStateUpdate) + kosmos.bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) + bluetoothState.value = true + runCurrent() + deviceItemUpdate.emit(listOf()) + runCurrent() + kosmos.bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true) + runCurrent() + + assertThat(actual) + .isEqualTo( + AudioSharingButtonState.Visible( + R.string.quick_settings_bluetooth_audio_sharing_button_sharing, + isActive = true, + ) + ) + } + } + + @Test + fun testButtonStateUpdate_hasSource_returnGone() { + testScope.runTest { + val actual by + collectLastValue(audioSharingButtonViewModel.audioSharingButtonStateUpdate) + kosmos.bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) + whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice) + whenever( + BluetoothUtils.hasConnectedBroadcastSource( + cachedBluetoothDevice, + localBluetoothManager, + ) + ) + .thenReturn(true) + bluetoothState.value = true + runCurrent() + deviceItemUpdate.emit(listOf(deviceItem)) + runCurrent() + + assertThat(actual).isEqualTo(AudioSharingButtonState.Gone) + } + } + + @Test + fun testButtonStateUpdate_hasActiveDevice_returnAudioSharing() { + testScope.runTest { + val actual by + collectLastValue(audioSharingButtonViewModel.audioSharingButtonStateUpdate) + kosmos.bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) + whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice) + whenever( + BluetoothUtils.hasConnectedBroadcastSource( + cachedBluetoothDevice, + localBluetoothManager, + ) + ) + .thenReturn(false) + whenever(BluetoothUtils.isActiveLeAudioDevice(cachedBluetoothDevice)).thenReturn(true) + bluetoothState.value = true + runCurrent() + deviceItemUpdate.emit(listOf(deviceItem)) + runCurrent() + + assertThat(actual) + .isEqualTo( + AudioSharingButtonState.Visible( + R.string.quick_settings_bluetooth_audio_sharing_button, + isActive = false, + ) + ) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt new file mode 100644 index 000000000000..ce37eee24e2a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt @@ -0,0 +1,193 @@ +/* + * 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.bluetooth.qsdialog + +import android.bluetooth.BluetoothDevice +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession +import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.settingslib.bluetooth.BluetoothUtils +import com.android.settingslib.bluetooth.LeAudioProfile +import com.android.settingslib.flags.Flags +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.plugins.activityStarter +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.testKosmos +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.never +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +@OptIn(ExperimentalCoroutinesApi::class) +class AudioSharingDeviceItemActionInteractorTest : SysuiTestCase() { + @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() + private val kosmos = testKosmos().apply { testDispatcher = UnconfinedTestDispatcher() } + private lateinit var actionInteractorImpl: DeviceItemActionInteractor + private lateinit var mockitoSession: StaticMockitoSession + private lateinit var connectedAudioSharingMediaDeviceItem: DeviceItem + private lateinit var connectedMediaDeviceItem: DeviceItem + @Mock private lateinit var dialog: SystemUIDialog + @Mock private lateinit var leAudioProfile: LeAudioProfile + @Mock private lateinit var bluetoothDevice: BluetoothDevice + + @Before + fun setUp() { + mockitoSession = + mockitoSession().initMocks(this).mockStatic(BluetoothUtils::class.java).startMocking() + connectedMediaDeviceItem = + DeviceItem( + type = DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE, + cachedBluetoothDevice = kosmos.cachedBluetoothDevice, + deviceName = DEVICE_NAME, + connectionSummary = DEVICE_CONNECTION_SUMMARY, + iconWithDescription = null, + background = null, + ) + connectedAudioSharingMediaDeviceItem = + DeviceItem( + type = DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE, + cachedBluetoothDevice = kosmos.cachedBluetoothDevice, + deviceName = DEVICE_NAME, + connectionSummary = DEVICE_CONNECTION_SUMMARY, + iconWithDescription = null, + background = null, + ) + actionInteractorImpl = kosmos.audioSharingDeviceItemActionInteractorImpl + } + + @After + fun tearDown() { + mockitoSession.finishMocking() + } + + @Test + @EnableFlags(Flags.FLAG_AUDIO_SHARING_QS_DIALOG_IMPROVEMENT) + fun testOnClick_connectedAudioSharingMediaDevice_flagOn_createDialog() { + with(kosmos) { + testScope.runTest { + bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) + actionInteractorImpl.onClick(connectedAudioSharingMediaDeviceItem, dialog) + verify(dialogTransitionAnimator) + .showFromDialog(any(), any(), eq(null), anyBoolean()) + } + } + } + + @Test + @DisableFlags(Flags.FLAG_AUDIO_SHARING_QS_DIALOG_IMPROVEMENT) + fun testOnClick_connectedAudioSharingMediaDevice_flagOff_shouldLaunchSettings() { + with(kosmos) { + testScope.runTest { + bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) + whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice) + actionInteractorImpl.onClick(connectedAudioSharingMediaDeviceItem, dialog) + verify(activityStarter) + .postStartActivityDismissingKeyguard( + ArgumentMatchers.any(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.any(), + ) + verify(dialogTransitionAnimator, never()) + .showFromDialog(any(), any(), eq(null), anyBoolean()) + } + } + } + + @Test + fun testOnClick_inAudioSharing_clickedDeviceHasSource_shouldNotLaunchSettings() { + with(kosmos) { + testScope.runTest { + bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) + whenever(cachedBluetoothDevice.uiAccessibleProfiles) + .thenReturn(listOf(leAudioProfile)) + whenever(BluetoothUtils.isBroadcasting(ArgumentMatchers.any())).thenReturn(true) + whenever( + BluetoothUtils.hasConnectedBroadcastSource( + ArgumentMatchers.any(), + ArgumentMatchers.any(), + ) + ) + .thenReturn(true) + + actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog) + verify(activityStarter, Mockito.never()) + .postStartActivityDismissingKeyguard( + ArgumentMatchers.any(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.any(), + ) + } + } + } + + @Test + fun testOnClick_inAudioSharing_clickedDeviceNoSource_shouldLaunchSettings() { + with(kosmos) { + testScope.runTest { + bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) + whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice) + whenever(cachedBluetoothDevice.uiAccessibleProfiles) + .thenReturn(listOf(leAudioProfile)) + + whenever(BluetoothUtils.isBroadcasting(ArgumentMatchers.any())).thenReturn(true) + whenever( + BluetoothUtils.hasConnectedBroadcastSource( + ArgumentMatchers.any(), + ArgumentMatchers.any(), + ) + ) + .thenReturn(false) + + actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog) + verify(activityStarter) + .postStartActivityDismissingKeyguard( + ArgumentMatchers.any(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.any(), + ) + } + } + } + + private companion object { + const val DEVICE_NAME = "device" + const val DEVICE_CONNECTION_SUMMARY = "active" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt new file mode 100644 index 000000000000..25b85b514435 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt @@ -0,0 +1,118 @@ +/* + * 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.bluetooth.qsdialog + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.widget.Button +import android.widget.TextView +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.testScope +import com.android.systemui.res.R +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +@OptIn(ExperimentalCoroutinesApi::class) +class AudioSharingDialogDelegateTest : SysuiTestCase() { + @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() + private val kosmos = testKosmos() + private val updateFlow = MutableSharedFlow<Unit>() + private lateinit var underTest: AudioSharingDialogDelegate + + @Before + fun setUp() { + with(kosmos) { + // TODO(b/364515243): use real object instead of mock + whenever(deviceItemInteractor.deviceItemUpdateRequest).thenReturn(updateFlow) + whenever(deviceItemInteractor.deviceItemUpdate) + .thenReturn(MutableStateFlow(emptyList())) + underTest = audioSharingDialogDelegate + } + } + + @Test + fun testCreateDialog() = + kosmos.testScope.runTest { + val dialog = underTest.createDialog() + assertThat(dialog).isInstanceOf(SystemUIDialog::class.java) + } + + @Test + fun testCreateDialog_showState() = + with(kosmos) { + testScope.runTest { + val availableDeviceName = "name" + whenever(cachedBluetoothDevice.name).thenReturn(availableDeviceName) + val dialog = spy(underTest.createDialog()) + dialog.show() + runCurrent() + val subtitleTextView = dialog.findViewById<TextView>(R.id.subtitle) + val switchActiveButton = dialog.findViewById<Button>(R.id.switch_active_button) + val shareAudioButton = dialog.findViewById<Button>(R.id.share_audio_button) + val subtitle = + context.getString( + R.string.quick_settings_bluetooth_audio_sharing_dialog_subtitle, + availableDeviceName, + "" + ) + val switchButtonText = + context.getString( + R.string.quick_settings_bluetooth_audio_sharing_dialog_switch_to_button, + availableDeviceName + ) + assertThat(subtitleTextView.text).isEqualTo(subtitle) + assertThat(switchActiveButton.text).isEqualTo(switchButtonText) + assertThat(switchActiveButton.hasOnClickListeners()).isTrue() + assertThat(shareAudioButton.hasOnClickListeners()).isTrue() + + switchActiveButton.performClick() + verify(dialog).dismiss() + } + } + + @Test + fun testCreateDialog_hideState() = + with(kosmos) { + testScope.runTest { + val dialog = spy(underTest.createDialog()) + dialog.show() + runCurrent() + updateFlow.emit(Unit) + runCurrent() + verify(dialog).dismiss() + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelTest.kt new file mode 100644 index 000000000000..beb816cae095 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelTest.kt @@ -0,0 +1,142 @@ +/* + * 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.bluetooth.qsdialog + +import android.bluetooth.BluetoothDevice +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.settingslib.bluetooth.LeAudioProfile +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager +import com.android.systemui.SysuiTestCase +import com.android.systemui.bluetooth.cachedBluetoothDeviceManager +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.res.R +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +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 org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +@OptIn(ExperimentalCoroutinesApi::class) +class AudioSharingDialogViewModelTest : SysuiTestCase() { + @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() + private val kosmos = testKosmos().apply { testDispatcher = UnconfinedTestDispatcher() } + @Mock private lateinit var profileManager: LocalBluetoothProfileManager + @Mock private lateinit var leAudioProfile: LeAudioProfile + private val updateFlow = MutableSharedFlow<Unit>() + private lateinit var underTest: AudioSharingDialogViewModel + + @Before + fun setUp() { + with(kosmos) { + // TODO(b/364515243): use real object instead of mock + whenever(deviceItemInteractor.deviceItemUpdateRequest).thenReturn(updateFlow) + whenever(deviceItemInteractor.deviceItemUpdate) + .thenReturn(MutableStateFlow(emptyList())) + underTest = audioSharingDialogViewModel + } + } + + @Test + fun testDialogState_show() = + with(kosmos) { + testScope.runTest { + val deviceName = "name" + whenever(cachedBluetoothDevice.name).thenReturn(deviceName) + val actual by collectLastValue(underTest.dialogState) + runCurrent() + assertThat(actual) + .isEqualTo( + AudioSharingDialogState.Show( + context.getString( + R.string.quick_settings_bluetooth_audio_sharing_dialog_subtitle, + deviceName, + "" + ), + context.getString( + R.string + .quick_settings_bluetooth_audio_sharing_dialog_switch_to_button, + deviceName + ) + ) + ) + } + } + + @Test + fun testDialogState_showWithActiveDeviceName() = + with(kosmos) { + testScope.runTest { + val deviceName = "name" + whenever(cachedBluetoothDevice.name).thenReturn(deviceName) + whenever(localBluetoothManager.profileManager).thenReturn(profileManager) + whenever(localBluetoothManager.cachedDeviceManager) + .thenReturn(cachedBluetoothDeviceManager) + whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile) + whenever(leAudioProfile.activeDevices).thenReturn(listOf(mock<BluetoothDevice>())) + whenever(cachedBluetoothDeviceManager.findDevice(any())) + .thenReturn(cachedBluetoothDevice) + val actual by collectLastValue(underTest.dialogState) + runCurrent() + assertThat(actual) + .isEqualTo( + AudioSharingDialogState.Show( + context.getString( + R.string.quick_settings_bluetooth_audio_sharing_dialog_subtitle, + deviceName, + deviceName + ), + context.getString( + R.string + .quick_settings_bluetooth_audio_sharing_dialog_switch_to_button, + deviceName + ) + ) + ) + } + } + + @Test + fun testDialogState_hide() = + with(kosmos) { + testScope.runTest { + val actual by collectLastValue(underTest.dialogState) + runCurrent() + updateFlow.emit(Unit) + assertThat(actual).isEqualTo(AudioSharingDialogState.Hide) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt index 2c53fd67c89d..25f956574274 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt @@ -16,158 +16,197 @@ package com.android.systemui.bluetooth.qsdialog +import android.bluetooth.BluetoothLeBroadcast import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession -import com.android.dx.mockito.inline.extended.StaticMockitoSession -import com.android.settingslib.bluetooth.BluetoothUtils -import com.android.settingslib.bluetooth.CachedBluetoothDevice -import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.res.R -import com.android.systemui.util.mockito.whenever +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.launch import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest -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.Captor import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.mockito.kotlin.any -@ExperimentalCoroutinesApi @SmallTest @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper(setAsMainLooper = true) +@OptIn(ExperimentalCoroutinesApi::class) class AudioSharingInteractorTest : SysuiTestCase() { - private val testDispatcher = UnconfinedTestDispatcher() - private val testScope = TestScope(testDispatcher) - private val bluetoothState = MutableStateFlow(false) - private val deviceItemUpdate: MutableSharedFlow<List<DeviceItem>> = MutableSharedFlow() - @Mock private lateinit var cachedBluetoothDevice: CachedBluetoothDevice - @Mock private lateinit var localBluetoothManager: LocalBluetoothManager - @Mock private lateinit var bluetoothStateInteractor: BluetoothStateInteractor - @Mock private lateinit var deviceItemInteractor: DeviceItemInteractor - @Mock private lateinit var deviceItem: DeviceItem - private lateinit var mockitoSession: StaticMockitoSession - private lateinit var audioSharingInteractor: AudioSharingInteractor + @get:Rule val mockito: MockitoRule = MockitoJUnit.rule() + private val kosmos = testKosmos() + @Mock private lateinit var localBluetoothLeBroadcast: LocalBluetoothLeBroadcast + @Captor private lateinit var callbackCaptor: ArgumentCaptor<BluetoothLeBroadcast.Callback> + private lateinit var underTest: AudioSharingInteractor @Before fun setUp() { - mockitoSession = - mockitoSession().initMocks(this).mockStatic(BluetoothUtils::class.java).startMocking() - whenever(bluetoothStateInteractor.bluetoothStateUpdate).thenReturn(bluetoothState) - whenever(deviceItemInteractor.deviceItemUpdate).thenReturn(deviceItemUpdate) - audioSharingInteractor = - AudioSharingInteractor( - localBluetoothManager, - bluetoothStateInteractor, - deviceItemInteractor, - testScope.backgroundScope, - testDispatcher, - ) + with(kosmos) { underTest = audioSharingInteractor } } - @After - fun tearDown() { - mockitoSession.finishMocking() - } + @Test + fun testIsAudioSharingOn_flagOff_false() = + with(kosmos) { + testScope.runTest { + bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(false) + val value by collectLastValue(underTest.isAudioSharingOn) + runCurrent() + + assertThat(value).isFalse() + } + } @Test - fun testButtonStateUpdate_bluetoothOff_returnGone() { - testScope.runTest { - val actual by collectLastValue(audioSharingInteractor.audioSharingButtonStateUpdate) + fun testIsAudioSharingOn_flagOn_notInAudioSharing_false() = + with(kosmos) { + testScope.runTest { + bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) + bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false) + val value by collectLastValue(underTest.isAudioSharingOn) + runCurrent() + + assertThat(value).isFalse() + } + } - assertThat(actual).isEqualTo(AudioSharingButtonState.Gone) + @Test + fun testIsAudioSharingOn_flagOn_inAudioSharing_true() = + with(kosmos) { + testScope.runTest { + bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) + bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true) + val value by collectLastValue(underTest.isAudioSharingOn) + runCurrent() + + assertThat(value).isTrue() + } } - } @Test - fun testButtonStateUpdate_noDevice_returnGone() { - testScope.runTest { - val actual by collectLastValue(audioSharingInteractor.audioSharingButtonStateUpdate) - bluetoothState.value = true - runCurrent() + fun testAudioSourceStateUpdate_notInAudioSharing_returnEmpty() = + with(kosmos) { + testScope.runTest { + bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) + bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false) + val value by collectLastValue(underTest.audioSourceStateUpdate) + runCurrent() + + assertThat(value).isNull() + } + } - assertThat(actual).isEqualTo(AudioSharingButtonState.Gone) + @Test + fun testAudioSourceStateUpdate_inAudioSharing_returnUnit() = + with(kosmos) { + testScope.runTest { + bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) + bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true) + val value by collectLastValue(underTest.audioSourceStateUpdate) + runCurrent() + bluetoothTileDialogAudioSharingRepository.emitAudioSourceStateUpdate() + runCurrent() + + assertThat(value).isNull() + } } - } @Test - fun testButtonStateUpdate_isBroadcasting_returnSharingAudio() { - testScope.runTest { - whenever(BluetoothUtils.isBroadcasting(localBluetoothManager)).thenReturn(true) - - val actual by collectLastValue(audioSharingInteractor.audioSharingButtonStateUpdate) - bluetoothState.value = true - deviceItemUpdate.emit(listOf()) - runCurrent() - - assertThat(actual) - .isEqualTo( - AudioSharingButtonState.Visible( - R.string.quick_settings_bluetooth_audio_sharing_button_sharing, - isActive = true - ) - ) + fun testHandleAudioSourceWhenReady_flagOff_sourceNotAdded() = + with(kosmos) { + testScope.runTest { + bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(false) + val job = launch { underTest.handleAudioSourceWhenReady() } + runCurrent() + + assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse() + job.cancel() + } } - } @Test - fun testButtonStateUpdate_hasSource_returnGone() { - testScope.runTest { - whenever(BluetoothUtils.isBroadcasting(localBluetoothManager)).thenReturn(false) - whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice) - whenever( - BluetoothUtils.hasConnectedBroadcastSource( - cachedBluetoothDevice, - localBluetoothManager - ) - ) - .thenReturn(true) + fun testHandleAudioSourceWhenReady_noProfile_sourceNotAdded() = + with(kosmos) { + testScope.runTest { + bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) + bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(null) + val job = launch { underTest.handleAudioSourceWhenReady() } + runCurrent() + + assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse() + job.cancel() + } + } - val actual by collectLastValue(audioSharingInteractor.audioSharingButtonStateUpdate) - bluetoothState.value = true - deviceItemUpdate.emit(listOf(deviceItem)) - runCurrent() + @Test + fun testHandleAudioSourceWhenReady_hasProfileButAudioSharingOff_sourceNotAdded() = + with(kosmos) { + testScope.runTest { + bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false) + bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) + bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile( + localBluetoothLeBroadcast + ) + val job = launch { underTest.handleAudioSourceWhenReady() } + runCurrent() - assertThat(actual).isEqualTo(AudioSharingButtonState.Gone) + assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse() + job.cancel() + } } - } @Test - fun testButtonStateUpdate_hasActiveDevice_returnAudioSharing() { - testScope.runTest { - whenever(BluetoothUtils.isBroadcasting(localBluetoothManager)).thenReturn(false) - whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice) - whenever( - BluetoothUtils.hasConnectedBroadcastSource( - cachedBluetoothDevice, - localBluetoothManager - ) + fun testHandleAudioSourceWhenReady_audioSharingOnButNoPlayback_sourceNotAdded() = + with(kosmos) { + testScope.runTest { + bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true) + bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) + bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile( + localBluetoothLeBroadcast ) - .thenReturn(false) - whenever(BluetoothUtils.isActiveLeAudioDevice(cachedBluetoothDevice)).thenReturn(true) - - val actual by collectLastValue(audioSharingInteractor.audioSharingButtonStateUpdate) - bluetoothState.value = true - deviceItemUpdate.emit(listOf(deviceItem)) - runCurrent() - - assertThat(actual) - .isEqualTo( - AudioSharingButtonState.Visible( - R.string.quick_settings_bluetooth_audio_sharing_button, - isActive = false - ) + val job = launch { underTest.handleAudioSourceWhenReady() } + runCurrent() + verify(localBluetoothLeBroadcast) + .registerServiceCallBack(any(), callbackCaptor.capture()) + runCurrent() + + assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse() + job.cancel() + } + } + + @Test + fun testHandleAudioSourceWhenReady_audioSharingOnAndPlaybackStarts_sourceAdded() = + with(kosmos) { + testScope.runTest { + bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true) + bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) + bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile( + localBluetoothLeBroadcast ) + val job = launch { underTest.handleAudioSourceWhenReady() } + runCurrent() + verify(localBluetoothLeBroadcast) + .registerServiceCallBack(any(), callbackCaptor.capture()) + runCurrent() + callbackCaptor.value.onPlaybackStarted(0, 0) + runCurrent() + + assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isTrue() + job.cancel() + } } - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt new file mode 100644 index 000000000000..c9e88136e4a0 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.bluetooth.qsdialog + +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothLeBroadcastMetadata +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.volume.data.repository.audioSharingRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.Mock +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.mockito.kotlin.any +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@ExperimentalCoroutinesApi +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +class AudioSharingRepositoryTest : SysuiTestCase() { + @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() + @Mock private lateinit var profileManager: LocalBluetoothProfileManager + @Mock private lateinit var leAudioBroadcastProfile: LocalBluetoothLeBroadcast + @Mock private lateinit var leAudioBroadcastAssistant: LocalBluetoothLeBroadcastAssistant + @Mock private lateinit var metadata: BluetoothLeBroadcastMetadata + @Mock private lateinit var bluetoothDevice: BluetoothDevice + private val kosmos = testKosmos() + private lateinit var underTest: AudioSharingRepository + + @Before + fun setUp() { + underTest = + AudioSharingRepositoryImpl( + kosmos.localBluetoothManager, + kosmos.audioSharingRepository, + kosmos.testDispatcher, + ) + } + + @Test + fun testSwitchActive() = + with(kosmos) { + testScope.runTest { + audioSharingRepository.setAudioSharingAvailable(true) + underTest.setActive(cachedBluetoothDevice) + verify(cachedBluetoothDevice).setActive() + } + } + + @Test + fun testSwitchActive_flagOff_doNothing() = + with(kosmos) { + testScope.runTest { + audioSharingRepository.setAudioSharingAvailable(false) + underTest.setActive(cachedBluetoothDevice) + verify(cachedBluetoothDevice, never()).setActive() + } + } + + @Test + fun testStartAudioSharing() = + with(kosmos) { + testScope.runTest { + whenever(localBluetoothManager.profileManager).thenReturn(profileManager) + whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile) + audioSharingRepository.setAudioSharingAvailable(true) + underTest.startAudioSharing() + verify(leAudioBroadcastProfile).startPrivateBroadcast() + } + } + + @Test + fun testStartAudioSharing_flagOff_doNothing() = + with(kosmos) { + testScope.runTest { + audioSharingRepository.setAudioSharingAvailable(false) + underTest.startAudioSharing() + verify(leAudioBroadcastProfile, never()).startPrivateBroadcast() + } + } + + @Test + fun testAddSource_flagOff_doesNothing() = + with(kosmos) { + testScope.runTest { + audioSharingRepository.setAudioSharingAvailable(false) + + underTest.addSource() + runCurrent() + + verify(leAudioBroadcastAssistant, never()).allConnectedDevices + } + } + + @Test + fun testAddSource_noMetadata_doesNothing() = + with(kosmos) { + testScope.runTest { + whenever(localBluetoothManager.profileManager).thenReturn(profileManager) + whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile) + audioSharingRepository.setAudioSharingAvailable(true) + whenever(leAudioBroadcastProfile.latestBluetoothLeBroadcastMetadata) + .thenReturn(null) + + underTest.addSource() + runCurrent() + + verify(leAudioBroadcastAssistant, never()).allConnectedDevices + } + } + + @Test + fun testAddSource_noConnectedDevice_doesNothing() = + with(kosmos) { + testScope.runTest { + whenever(localBluetoothManager.profileManager).thenReturn(profileManager) + whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile) + whenever(profileManager.leAudioBroadcastAssistantProfile) + .thenReturn(leAudioBroadcastAssistant) + audioSharingRepository.setAudioSharingAvailable(true) + whenever(leAudioBroadcastProfile.latestBluetoothLeBroadcastMetadata) + .thenReturn(metadata) + whenever(leAudioBroadcastAssistant.allConnectedDevices).thenReturn(emptyList()) + + underTest.addSource() + runCurrent() + + verify(leAudioBroadcastAssistant, never()).addSource(any(), any(), anyBoolean()) + } + } + + @Test + fun testAddSource_hasConnectedDeviceAndMetadata_addSource() = + with(kosmos) { + testScope.runTest { + whenever(localBluetoothManager.profileManager).thenReturn(profileManager) + whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile) + whenever(profileManager.leAudioBroadcastAssistantProfile) + .thenReturn(leAudioBroadcastAssistant) + audioSharingRepository.setAudioSharingAvailable(true) + whenever(leAudioBroadcastProfile.latestBluetoothLeBroadcastMetadata) + .thenReturn(metadata) + whenever(leAudioBroadcastAssistant.allConnectedDevices) + .thenReturn(listOf(bluetoothDevice)) + + underTest.addSource() + runCurrent() + + verify(leAudioBroadcastAssistant).addSource(bluetoothDevice, metadata, false) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt index d7bea6680c2d..a56c2cb25542 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt @@ -31,8 +31,11 @@ import com.android.settingslib.flags.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.animation.Expandable +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.testKosmos import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.kotlin.getMutableStateFlow @@ -42,12 +45,12 @@ import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.test.TestCoroutineScheduler import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule @@ -64,10 +67,12 @@ import org.mockito.junit.MockitoRule @SmallTest @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper(setAsMainLooper = true) +@OptIn(ExperimentalCoroutinesApi::class) @EnableFlags(Flags.FLAG_BLUETOOTH_QS_TILE_DIALOG_AUTO_ON_TOGGLE) class BluetoothTileDialogViewModelTest : SysuiTestCase() { @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() + private val kosmos = testKosmos() private val fakeSystemClock = FakeSystemClock() private val backgroundExecutor = FakeExecutor(fakeSystemClock) @@ -75,8 +80,6 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { @Mock private lateinit var bluetoothStateInteractor: BluetoothStateInteractor - @Mock private lateinit var audioSharingInteractor: AudioSharingInteractor - @Mock private lateinit var bluetoothDeviceMetadataInteractor: BluetoothDeviceMetadataInteractor @Mock private lateinit var deviceItemInteractor: DeviceItemInteractor @@ -111,15 +114,15 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { private val sharedPreferences = FakeSharedPreferences() - private lateinit var scheduler: TestCoroutineScheduler private lateinit var dispatcher: CoroutineDispatcher private lateinit var testScope: TestScope @Before fun setUp() { - scheduler = TestCoroutineScheduler() - dispatcher = UnconfinedTestDispatcher(scheduler) - testScope = TestScope(dispatcher) + dispatcher = kosmos.testDispatcher + testScope = kosmos.testScope + // TODO(b/364515243): use real object instead of mock + whenever(kosmos.deviceItemInteractor.deviceItemUpdate).thenReturn(MutableSharedFlow()) bluetoothTileDialogViewModel = BluetoothTileDialogViewModel( deviceItemInteractor, @@ -139,11 +142,13 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { dispatcher ) ), - audioSharingInteractor, + kosmos.audioSharingInteractor, + kosmos.audioSharingButtonViewModelFactory, bluetoothDeviceMetadataInteractor, mDialogTransitionAnimator, activityStarter, uiEventLogger, + bluetoothTileDialogLogger, testScope.backgroundScope, dispatcher, dispatcher, @@ -161,13 +166,10 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { whenever(sysuiDialog.context).thenReturn(mContext) whenever(bluetoothTileDialogDelegate.bluetoothStateToggle) .thenReturn(getMutableStateFlow(false)) - whenever(bluetoothTileDialogDelegate.deviceItemClick) - .thenReturn(getMutableStateFlow(deviceItem)) + whenever(bluetoothTileDialogDelegate.deviceItemClick).thenReturn(MutableSharedFlow()) whenever(bluetoothTileDialogDelegate.contentHeight).thenReturn(getMutableStateFlow(0)) whenever(bluetoothTileDialogDelegate.bluetoothAutoOnToggle) .thenReturn(getMutableStateFlow(false)) - whenever(audioSharingInteractor.audioSharingButtonStateUpdate) - .thenReturn(getMutableStateFlow(AudioSharingButtonState.Gone)) whenever(expandable.dialogTransitionController(any())).thenReturn(controller) } @@ -175,6 +177,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { fun testShowDialog_noAnimation() { testScope.runTest { bluetoothTileDialogViewModel.showDialog(null) + runCurrent() verify(mDialogTransitionAnimator, never()).show(any(), any(), any()) } @@ -184,6 +187,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { fun testShowDialog_animated() { testScope.runTest { bluetoothTileDialogViewModel.showDialog(expandable) + runCurrent() verify(mDialogTransitionAnimator).show(any(), any(), anyBoolean()) } @@ -194,6 +198,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { testScope.runTest { backgroundExecutor.execute { bluetoothTileDialogViewModel.showDialog(expandable) + runCurrent() verify(mDialogTransitionAnimator).show(any(), any(), anyBoolean()) } @@ -204,6 +209,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { fun testShowDialog_fetchDeviceItem() { testScope.runTest { bluetoothTileDialogViewModel.showDialog(null) + runCurrent() verify(deviceItemInteractor).deviceItemUpdate } @@ -214,6 +220,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { testScope.runTest { whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice) bluetoothTileDialogViewModel.showDialog(null) + runCurrent() val clickedView = View(context) bluetoothTileDialogViewModel.onPairNewDeviceClicked(clickedView) diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt index 681ea754e630..9c427c6b085e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt @@ -15,34 +15,22 @@ */ package com.android.systemui.bluetooth.qsdialog -import android.bluetooth.BluetoothDevice import android.testing.AndroidTestingRunner import android.testing.TestableLooper import androidx.test.filters.SmallTest -import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession -import com.android.dx.mockito.inline.extended.StaticMockitoSession -import com.android.settingslib.bluetooth.BluetoothUtils -import com.android.settingslib.bluetooth.CachedBluetoothDevice -import com.android.settingslib.bluetooth.LeAudioProfile -import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant -import com.android.settingslib.bluetooth.LocalBluetoothProfileManager import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope -import com.android.systemui.plugins.activityStarter import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.testKosmos import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest -import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers import org.mockito.Mock -import org.mockito.Mockito import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule @@ -56,28 +44,18 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() private val kosmos = testKosmos().apply { testDispatcher = UnconfinedTestDispatcher() } private lateinit var actionInteractorImpl: DeviceItemActionInteractor - private lateinit var mockitoSession: StaticMockitoSession private lateinit var activeMediaDeviceItem: DeviceItem private lateinit var notConnectedDeviceItem: DeviceItem - private lateinit var connectedAudioSharingMediaDeviceItem: DeviceItem private lateinit var connectedMediaDeviceItem: DeviceItem private lateinit var connectedOtherDeviceItem: DeviceItem @Mock private lateinit var dialog: SystemUIDialog - @Mock private lateinit var profileManager: LocalBluetoothProfileManager - @Mock private lateinit var leAudioProfile: LeAudioProfile - @Mock private lateinit var assistantProfile: LocalBluetoothLeBroadcastAssistant - @Mock private lateinit var bluetoothDevice: BluetoothDevice - @Mock private lateinit var bluetoothDeviceGroupId2: BluetoothDevice - @Mock private lateinit var cachedBluetoothDevice: CachedBluetoothDevice @Before fun setUp() { - mockitoSession = - mockitoSession().initMocks(this).mockStatic(BluetoothUtils::class.java).startMocking() activeMediaDeviceItem = DeviceItem( type = DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE, - cachedBluetoothDevice = cachedBluetoothDevice, + cachedBluetoothDevice = kosmos.cachedBluetoothDevice, deviceName = DEVICE_NAME, connectionSummary = DEVICE_CONNECTION_SUMMARY, iconWithDescription = null, @@ -86,7 +64,7 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { notConnectedDeviceItem = DeviceItem( type = DeviceItemType.SAVED_BLUETOOTH_DEVICE, - cachedBluetoothDevice = cachedBluetoothDevice, + cachedBluetoothDevice = kosmos.cachedBluetoothDevice, deviceName = DEVICE_NAME, connectionSummary = DEVICE_CONNECTION_SUMMARY, iconWithDescription = null, @@ -95,16 +73,7 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { connectedMediaDeviceItem = DeviceItem( type = DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE, - cachedBluetoothDevice = cachedBluetoothDevice, - deviceName = DEVICE_NAME, - connectionSummary = DEVICE_CONNECTION_SUMMARY, - iconWithDescription = null, - background = null - ) - connectedAudioSharingMediaDeviceItem = - DeviceItem( - type = DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE, - cachedBluetoothDevice = cachedBluetoothDevice, + cachedBluetoothDevice = kosmos.cachedBluetoothDevice, deviceName = DEVICE_NAME, connectionSummary = DEVICE_CONNECTION_SUMMARY, iconWithDescription = null, @@ -113,18 +82,13 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { connectedOtherDeviceItem = DeviceItem( type = DeviceItemType.CONNECTED_BLUETOOTH_DEVICE, - cachedBluetoothDevice = cachedBluetoothDevice, + cachedBluetoothDevice = kosmos.cachedBluetoothDevice, deviceName = DEVICE_NAME, connectionSummary = DEVICE_CONNECTION_SUMMARY, iconWithDescription = null, background = null ) - actionInteractorImpl = kosmos.deviceItemActionInteractor - } - - @After - fun tearDown() { - mockitoSession.finishMocking() + actionInteractorImpl = kosmos.deviceItemActionInteractorImpl } @Test @@ -132,14 +96,8 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { with(kosmos) { testScope.runTest { whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) - whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false) actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog) verify(cachedBluetoothDevice).setActive() - verify(bluetoothTileDialogLogger) - .logDeviceClick( - cachedBluetoothDevice.address, - DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE - ) } } } @@ -149,14 +107,8 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { with(kosmos) { testScope.runTest { whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) - whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false) actionInteractorImpl.onClick(activeMediaDeviceItem, dialog) verify(cachedBluetoothDevice).disconnect() - verify(bluetoothTileDialogLogger) - .logDeviceClick( - cachedBluetoothDevice.address, - DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE - ) } } } @@ -166,14 +118,8 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { with(kosmos) { testScope.runTest { whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) - whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false) actionInteractorImpl.onClick(connectedOtherDeviceItem, dialog) verify(cachedBluetoothDevice).disconnect() - verify(bluetoothTileDialogLogger) - .logDeviceClick( - cachedBluetoothDevice.address, - DeviceItemType.CONNECTED_BLUETOOTH_DEVICE - ) } } } @@ -183,293 +129,8 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { with(kosmos) { testScope.runTest { whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) - whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false) actionInteractorImpl.onClick(notConnectedDeviceItem, dialog) verify(cachedBluetoothDevice).connect() - verify(bluetoothTileDialogLogger) - .logDeviceClick( - cachedBluetoothDevice.address, - DeviceItemType.SAVED_BLUETOOTH_DEVICE - ) - } - } - } - - @Test - fun testOnClick_connectedAudioSharingMediaDevice_logClick() { - with(kosmos) { - testScope.runTest { - whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) - actionInteractorImpl.onClick(connectedAudioSharingMediaDeviceItem, dialog) - verify(bluetoothTileDialogLogger) - .logDeviceClick( - cachedBluetoothDevice.address, - DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE - ) - } - } - } - - @Test - fun testOnClick_audioSharingDisabled_shouldNotLaunchSettings() { - with(kosmos) { - testScope.runTest { - whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) - whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false) - - actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog) - verify(activityStarter, Mockito.never()) - .postStartActivityDismissingKeyguard( - ArgumentMatchers.any(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.any() - ) - } - } - } - - @Test - fun testOnClick_inAudioSharing_clickedDeviceHasSource_shouldNotLaunchSettings() { - with(kosmos) { - testScope.runTest { - whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) - whenever(cachedBluetoothDevice.uiAccessibleProfiles) - .thenReturn(listOf(leAudioProfile)) - - whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) - whenever(localBluetoothManager.profileManager).thenReturn(profileManager) - whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile) - whenever(profileManager.leAudioBroadcastAssistantProfile) - .thenReturn(assistantProfile) - - whenever(BluetoothUtils.isBroadcasting(ArgumentMatchers.any())).thenReturn(true) - whenever( - BluetoothUtils.hasConnectedBroadcastSource( - ArgumentMatchers.any(), - ArgumentMatchers.any() - ) - ) - .thenReturn(true) - - actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog) - verify(activityStarter, Mockito.never()) - .postStartActivityDismissingKeyguard( - ArgumentMatchers.any(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.any() - ) - } - } - } - - @Test - fun testOnClick_inAudioSharing_clickedDeviceNoSource_shouldLaunchSettings() { - with(kosmos) { - testScope.runTest { - whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) - whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice) - whenever(cachedBluetoothDevice.uiAccessibleProfiles) - .thenReturn(listOf(leAudioProfile)) - - whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) - whenever(localBluetoothManager.profileManager).thenReturn(profileManager) - whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile) - whenever(profileManager.leAudioBroadcastAssistantProfile) - .thenReturn(assistantProfile) - - whenever(BluetoothUtils.isBroadcasting(ArgumentMatchers.any())).thenReturn(true) - whenever( - BluetoothUtils.hasConnectedBroadcastSource( - ArgumentMatchers.any(), - ArgumentMatchers.any() - ) - ) - .thenReturn(false) - - actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog) - verify(activityStarter) - .postStartActivityDismissingKeyguard( - ArgumentMatchers.any(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.any() - ) - } - } - } - - @Test - fun testOnClick_noConnectedLeDevice_shouldNotLaunchSettings() { - with(kosmos) { - testScope.runTest { - whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) - - whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) - whenever(localBluetoothManager.profileManager).thenReturn(profileManager) - whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile) - whenever(profileManager.leAudioBroadcastAssistantProfile) - .thenReturn(assistantProfile) - - actionInteractorImpl.onClick(notConnectedDeviceItem, dialog) - verify(activityStarter, Mockito.never()) - .postStartActivityDismissingKeyguard( - ArgumentMatchers.any(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.any() - ) - } - } - } - - @Test - fun testOnClick_hasOneConnectedLeDevice_clickedNonLe_shouldNotLaunchSettings() { - with(kosmos) { - testScope.runTest { - whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) - - whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) - whenever(localBluetoothManager.profileManager).thenReturn(profileManager) - whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile) - whenever(profileManager.leAudioBroadcastAssistantProfile) - .thenReturn(assistantProfile) - - whenever( - assistantProfile.getDevicesMatchingConnectionStates(ArgumentMatchers.any()) - ) - .thenReturn(listOf(bluetoothDevice)) - - actionInteractorImpl.onClick(notConnectedDeviceItem, dialog) - verify(activityStarter, Mockito.never()) - .postStartActivityDismissingKeyguard( - ArgumentMatchers.any(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.any() - ) - } - } - } - - @Test - fun testOnClick_hasOneConnectedLeDevice_clickedLe_shouldLaunchSettings() { - with(kosmos) { - testScope.runTest { - whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice) - whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) - whenever(cachedBluetoothDevice.profiles).thenReturn(listOf(leAudioProfile)) - whenever(leAudioProfile.isEnabled(ArgumentMatchers.any())).thenReturn(true) - - whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) - whenever(localBluetoothManager.profileManager).thenReturn(profileManager) - whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile) - whenever(profileManager.leAudioBroadcastAssistantProfile) - .thenReturn(assistantProfile) - - whenever( - assistantProfile.getDevicesMatchingConnectionStates(ArgumentMatchers.any()) - ) - .thenReturn(listOf(bluetoothDevice)) - - actionInteractorImpl.onClick(notConnectedDeviceItem, dialog) - verify(activityStarter) - .postStartActivityDismissingKeyguard( - ArgumentMatchers.any(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.any() - ) - } - } - } - - @Test - fun testOnClick_hasOneConnectedLeDevice_clickedConnectedLe_shouldNotLaunchSettings() { - with(kosmos) { - testScope.runTest { - whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) - - whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) - whenever(localBluetoothManager.profileManager).thenReturn(profileManager) - whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile) - whenever(profileManager.leAudioBroadcastAssistantProfile) - .thenReturn(assistantProfile) - - whenever( - assistantProfile.getDevicesMatchingConnectionStates(ArgumentMatchers.any()) - ) - .thenReturn(listOf(bluetoothDevice)) - - actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog) - verify(activityStarter, Mockito.never()) - .postStartActivityDismissingKeyguard( - ArgumentMatchers.any(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.any() - ) - } - } - } - - @Test - fun testOnClick_hasTwoConnectedLeDevice_clickedNotConnectedLe_shouldNotLaunchSettings() { - with(kosmos) { - testScope.runTest { - whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) - - whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) - whenever(localBluetoothManager.profileManager).thenReturn(profileManager) - whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile) - whenever(profileManager.leAudioBroadcastAssistantProfile) - .thenReturn(assistantProfile) - - whenever( - assistantProfile.getDevicesMatchingConnectionStates(ArgumentMatchers.any()) - ) - .thenReturn(listOf(bluetoothDevice, bluetoothDeviceGroupId2)) - whenever(leAudioProfile.getGroupId(ArgumentMatchers.any())).thenAnswer { - val device = it.arguments.first() as BluetoothDevice - if (device == bluetoothDevice) GROUP_ID_1 else GROUP_ID_2 - } - - actionInteractorImpl.onClick(notConnectedDeviceItem, dialog) - verify(activityStarter, Mockito.never()) - .postStartActivityDismissingKeyguard( - ArgumentMatchers.any(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.any() - ) - } - } - } - - @Test - fun testOnClick_hasTwoConnectedLeDevice_clickedActiveLe_shouldLaunchSettings() { - with(kosmos) { - testScope.runTest { - whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice) - whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) - whenever(cachedBluetoothDevice.profiles).thenReturn(listOf(leAudioProfile)) - whenever(leAudioProfile.isEnabled(ArgumentMatchers.any())).thenReturn(true) - - whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) - whenever(localBluetoothManager.profileManager).thenReturn(profileManager) - whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile) - whenever(profileManager.leAudioBroadcastAssistantProfile) - .thenReturn(assistantProfile) - - whenever( - assistantProfile.getDevicesMatchingConnectionStates(ArgumentMatchers.any()) - ) - .thenReturn(listOf(bluetoothDevice, bluetoothDeviceGroupId2)) - whenever(leAudioProfile.getGroupId(ArgumentMatchers.any())).thenAnswer { - val device = it.arguments.first() as BluetoothDevice - if (device == bluetoothDevice) GROUP_ID_1 else GROUP_ID_2 - } - - actionInteractorImpl.onClick(activeMediaDeviceItem, dialog) - verify(activityStarter) - .postStartActivityDismissingKeyguard( - ArgumentMatchers.any(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.any() - ) } } } @@ -478,7 +139,5 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { const val DEVICE_NAME = "device" const val DEVICE_CONNECTION_SUMMARY = "active" const val DEVICE_ADDRESS = "address" - const val GROUP_ID_1 = 1 - const val GROUP_ID_2 = 2 } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt index ef441c1dc12c..10c3457066cb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt @@ -133,8 +133,8 @@ class DeviceItemFactoryTest : SysuiTestCase() { @Test fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_flagOff_returnsFalse() { - // Flags.FLAG_ENABLE_LE_AUDIO_SHARING off or the device doesn't support broadcast - // source or assistant. + // Flags.FLAG_ENABLE_LE_AUDIO_SHARING off or the device doesn't support broadcast + // source or assistant. `when`(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false) assertThat( @@ -145,9 +145,9 @@ class DeviceItemFactoryTest : SysuiTestCase() { } @Test - fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_isActiveDevice_returnsFalse() { - // Flags.FLAG_ENABLE_LE_AUDIO_SHARING on and the device support broadcast source and - // assistant. + fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_isActiveDevice_false() { + // Flags.FLAG_ENABLE_LE_AUDIO_SHARING on and the device support broadcast source and + // assistant. `when`(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) `when`(BluetoothUtils.isActiveMediaDevice(any())).thenReturn(true) @@ -159,9 +159,9 @@ class DeviceItemFactoryTest : SysuiTestCase() { } @Test - fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_isNotAvailable_returnsFalse() { - // Flags.FLAG_ENABLE_LE_AUDIO_SHARING on and the device support broadcast source and - // assistant. + fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_isNotAvailable_false() { + // Flags.FLAG_ENABLE_LE_AUDIO_SHARING on and the device support broadcast source and + // assistant. `when`(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) `when`(BluetoothUtils.isActiveMediaDevice(any())).thenReturn(false) `when`(BluetoothUtils.isAvailableMediaBluetoothDevice(any(), any())).thenReturn(true) @@ -177,8 +177,8 @@ class DeviceItemFactoryTest : SysuiTestCase() { @Test fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_returnsTrue() { - // Flags.FLAG_ENABLE_LE_AUDIO_SHARING on and the device support broadcast source and - // assistant. + // Flags.FLAG_ENABLE_LE_AUDIO_SHARING on and the device support broadcast source and + // assistant. `when`(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) `when`(BluetoothUtils.isActiveMediaDevice(any())).thenReturn(false) `when`(BluetoothUtils.isAvailableMediaBluetoothDevice(any(), any())).thenReturn(true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt index 194590c1f626..c39b9a606cfe 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt @@ -83,18 +83,6 @@ class DeviceItemInteractorTest : SysuiTestCase() { fun setUp() { dispatcher = UnconfinedTestDispatcher() testScope = TestScope(dispatcher) - interactor = - DeviceItemInteractor( - bluetoothTileDialogRepository, - audioManager, - adapter, - localBluetoothManager, - fakeSystemClock, - logger, - testScope.backgroundScope, - dispatcher - ) - `when`(deviceItem1.cachedBluetoothDevice).thenReturn(cachedDevice1) `when`(deviceItem2.cachedBluetoothDevice).thenReturn(cachedDevice2) `when`(cachedDevice1.address).thenReturn("ADDRESS") @@ -108,9 +96,19 @@ class DeviceItemInteractorTest : SysuiTestCase() { fun testUpdateDeviceItems_noCachedDevice_returnEmpty() { testScope.runTest { `when`(bluetoothTileDialogRepository.cachedDevices).thenReturn(emptyList()) - interactor.setDeviceItemFactoryListForTesting( - listOf(createFactory({ true }, deviceItem1)) - ) + interactor = + DeviceItemInteractor( + bluetoothTileDialogRepository, + audioManager, + adapter, + localBluetoothManager, + fakeSystemClock, + logger, + listOf(createFactory({ true }, deviceItem1)), + emptyList(), + testScope.backgroundScope, + dispatcher + ) val latest by collectLastValue(interactor.deviceItemUpdate) val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate) @@ -125,9 +123,19 @@ class DeviceItemInteractorTest : SysuiTestCase() { fun testUpdateDeviceItems_hasCachedDevice_filterNotMatch_returnEmpty() { testScope.runTest { `when`(bluetoothTileDialogRepository.cachedDevices).thenReturn(listOf(cachedDevice1)) - interactor.setDeviceItemFactoryListForTesting( - listOf(createFactory({ false }, deviceItem1)) - ) + interactor = + DeviceItemInteractor( + bluetoothTileDialogRepository, + audioManager, + adapter, + localBluetoothManager, + fakeSystemClock, + logger, + listOf(createFactory({ false }, deviceItem1)), + emptyList(), + testScope.backgroundScope, + dispatcher + ) val latest by collectLastValue(interactor.deviceItemUpdate) val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate) @@ -142,9 +150,19 @@ class DeviceItemInteractorTest : SysuiTestCase() { fun testUpdateDeviceItems_hasCachedDevice_filterMatch_returnDeviceItem() { testScope.runTest { `when`(bluetoothTileDialogRepository.cachedDevices).thenReturn(listOf(cachedDevice1)) - interactor.setDeviceItemFactoryListForTesting( - listOf(createFactory({ true }, deviceItem1)) - ) + interactor = + DeviceItemInteractor( + bluetoothTileDialogRepository, + audioManager, + adapter, + localBluetoothManager, + fakeSystemClock, + logger, + listOf(createFactory({ true }, deviceItem1)), + emptyList(), + testScope.backgroundScope, + dispatcher + ) val latest by collectLastValue(interactor.deviceItemUpdate) val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate) @@ -159,9 +177,22 @@ class DeviceItemInteractorTest : SysuiTestCase() { fun testUpdateDeviceItems_hasCachedDevice_filterMatch_returnMultipleDeviceItem() { testScope.runTest { `when`(adapter.mostRecentlyConnectedDevices).thenReturn(null) - interactor.setDeviceItemFactoryListForTesting( - listOf(createFactory({ false }, deviceItem1), createFactory({ true }, deviceItem2)) - ) + interactor = + DeviceItemInteractor( + bluetoothTileDialogRepository, + audioManager, + adapter, + localBluetoothManager, + fakeSystemClock, + logger, + listOf( + createFactory({ false }, deviceItem1), + createFactory({ true }, deviceItem2) + ), + emptyList(), + testScope.backgroundScope, + dispatcher + ) val latest by collectLastValue(interactor.deviceItemUpdate) val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate) @@ -176,18 +207,31 @@ class DeviceItemInteractorTest : SysuiTestCase() { fun testUpdateDeviceItems_sortByDisplayPriority() { testScope.runTest { `when`(adapter.mostRecentlyConnectedDevices).thenReturn(null) - interactor.setDeviceItemFactoryListForTesting( - listOf( - createFactory({ cachedDevice -> cachedDevice.device == device1 }, deviceItem1), - createFactory({ cachedDevice -> cachedDevice.device == device2 }, deviceItem2) - ) - ) - interactor.setDisplayPriorityForTesting( - listOf( - DeviceItemType.SAVED_BLUETOOTH_DEVICE, - DeviceItemType.CONNECTED_BLUETOOTH_DEVICE + interactor = + DeviceItemInteractor( + bluetoothTileDialogRepository, + audioManager, + adapter, + localBluetoothManager, + fakeSystemClock, + logger, + listOf( + createFactory( + { cachedDevice -> cachedDevice.device == device1 }, + deviceItem1 + ), + createFactory( + { cachedDevice -> cachedDevice.device == device2 }, + deviceItem2 + ) + ), + listOf( + DeviceItemType.SAVED_BLUETOOTH_DEVICE, + DeviceItemType.CONNECTED_BLUETOOTH_DEVICE + ), + testScope.backgroundScope, + dispatcher ) - ) `when`(deviceItem1.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE) `when`(deviceItem2.type).thenReturn(DeviceItemType.SAVED_BLUETOOTH_DEVICE) @@ -204,15 +248,28 @@ class DeviceItemInteractorTest : SysuiTestCase() { fun testUpdateDeviceItems_sameType_sortByRecentlyConnected() { testScope.runTest { `when`(adapter.mostRecentlyConnectedDevices).thenReturn(listOf(device2, device1)) - interactor.setDeviceItemFactoryListForTesting( - listOf( - createFactory({ cachedDevice -> cachedDevice.device == device1 }, deviceItem1), - createFactory({ cachedDevice -> cachedDevice.device == device2 }, deviceItem2) + interactor = + DeviceItemInteractor( + bluetoothTileDialogRepository, + audioManager, + adapter, + localBluetoothManager, + fakeSystemClock, + logger, + listOf( + createFactory( + { cachedDevice -> cachedDevice.device == device1 }, + deviceItem1 + ), + createFactory( + { cachedDevice -> cachedDevice.device == device2 }, + deviceItem2 + ) + ), + listOf(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE), + testScope.backgroundScope, + dispatcher ) - ) - interactor.setDisplayPriorityForTesting( - listOf(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE) - ) `when`(deviceItem1.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE) `when`(deviceItem2.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE) @@ -231,10 +288,19 @@ class DeviceItemInteractorTest : SysuiTestCase() { `when`(bluetoothTileDialogRepository.cachedDevices) .thenReturn(listOf(cachedDevice2, cachedDevice2, cachedDevice2, cachedDevice2)) `when`(adapter.mostRecentlyConnectedDevices).thenReturn(null) - interactor.setDeviceItemFactoryListForTesting( - listOf(createFactory({ true }, deviceItem2)) - ) - + interactor = + DeviceItemInteractor( + bluetoothTileDialogRepository, + audioManager, + adapter, + localBluetoothManager, + fakeSystemClock, + logger, + listOf(createFactory({ true }, deviceItem2)), + emptyList(), + testScope.backgroundScope, + dispatcher + ) val latest by collectLastValue(interactor.deviceItemUpdate) val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate) interactor.updateDeviceItems(mContext, DeviceFetchTrigger.FIRST_LOAD) diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt index 85e8ab43b2ee..5741d64c4d32 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt @@ -122,6 +122,7 @@ class ClipboardModelTest : SysuiTestCase() { @Test @Throws(IOException::class) + @DisableFlags(FLAG_CLIPBOARD_USE_DESCRIPTION_MIMETYPE) fun test_imageClipData_loadFailure() { whenever(mMockContext.contentResolver).thenReturn(mMockContentResolver) whenever(mMockContext.resources).thenReturn(mContext.resources) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt index edc8c837bf78..edc8c837bf78 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt index 2312bbd2d7f8..2312bbd2d7f8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryImplTest.kt new file mode 100644 index 000000000000..ff3186abecdc --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryImplTest.kt @@ -0,0 +1,158 @@ +/* + * 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.display.data.repository + +import android.content.testableContext +import android.platform.test.annotations.EnableFlags +import android.view.Display +import android.view.mockWindowManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.display.shared.model.DisplayWindowProperties +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.unconfinedTestDispatcher +import com.android.systemui.statusbar.core.StatusBarConnectedDisplays +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock + +@EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) +@RunWith(AndroidJUnit4::class) +@SmallTest +class DisplayWindowPropertiesRepositoryImplTest : SysuiTestCase() { + + private val kosmos = testKosmos().also { it.testDispatcher = it.unconfinedTestDispatcher } + private val fakeDisplayRepository = kosmos.displayRepository + private val testScope = kosmos.testScope + + private val applicationContext = kosmos.testableContext + private val applicationWindowManager = kosmos.mockWindowManager + + private val repo = + DisplayWindowPropertiesRepositoryImpl( + kosmos.applicationCoroutineScope, + applicationContext, + applicationWindowManager, + fakeDisplayRepository, + ) + + @Before + fun start() { + repo.start() + } + + @Before + fun addDisplays() = runBlocking { + fakeDisplayRepository.addDisplay(createDisplay(DEFAULT_DISPLAY_ID)) + fakeDisplayRepository.addDisplay(createDisplay(NON_DEFAULT_DISPLAY_ID)) + } + + @Test + fun get_defaultDisplayId_returnsDefaultProperties() = + testScope.runTest { + val displayContext = repo.get(DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO) + + assertThat(displayContext) + .isEqualTo( + DisplayWindowProperties( + displayId = DEFAULT_DISPLAY_ID, + windowType = WINDOW_TYPE_FOO, + context = applicationContext, + windowManager = applicationWindowManager, + ) + ) + } + + @Test + fun get_nonDefaultDisplayId_returnsNewStatusBarContext() = + testScope.runTest { + val displayContext = repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO) + + assertThat(displayContext.context).isNotSameInstanceAs(applicationContext) + } + + @Test + fun get_nonDefaultDisplayId_returnsNewWindowManager() = + testScope.runTest { + val displayContext = repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO) + + assertThat(displayContext.windowManager).isNotSameInstanceAs(applicationWindowManager) + } + + @Test + fun get_multipleCallsForDefaultDisplay_returnsSameInstance() = + testScope.runTest { + val displayContext = repo.get(DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO) + + assertThat(repo.get(DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)) + .isSameInstanceAs(displayContext) + } + + @Test + fun get_multipleCallsForNonDefaultDisplay_returnsSameInstance() = + testScope.runTest { + val displayContext = repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO) + + assertThat(repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)) + .isSameInstanceAs(displayContext) + } + + @Test + fun get_multipleCalls_differentType_returnsNewInstance() = + testScope.runTest { + val displayContext = repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO) + + assertThat(repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_BAR)) + .isNotSameInstanceAs(displayContext) + } + + @Test + fun get_afterDisplayRemoved_returnsNewInstance() = + testScope.runTest { + val displayContext = repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO) + + fakeDisplayRepository.removeDisplay(NON_DEFAULT_DISPLAY_ID) + fakeDisplayRepository.addDisplay(createDisplay(NON_DEFAULT_DISPLAY_ID)) + + assertThat(repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)) + .isNotSameInstanceAs(displayContext) + } + + @Test(expected = IllegalArgumentException::class) + fun get_nonExistingDisplayId_throws() = + testScope.runTest { repo.get(NON_EXISTING_DISPLAY_ID, WINDOW_TYPE_FOO) } + + private fun createDisplay(displayId: Int) = + mock<Display> { on { getDisplayId() } doReturn displayId } + + companion object { + private const val DEFAULT_DISPLAY_ID = Display.DEFAULT_DISPLAY + private const val NON_DEFAULT_DISPLAY_ID = DEFAULT_DISPLAY_ID + 1 + private const val NON_EXISTING_DISPLAY_ID = DEFAULT_DISPLAY_ID + 2 + private const val WINDOW_TYPE_FOO = 123 + private const val WINDOW_TYPE_BAR = 321 + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt index 8b1341114c68..8b1341114c68 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt index e1845a17a767..e1845a17a767 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt index 5b216620ec2b..5b216620ec2b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/ActivatableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/ActivatableTest.kt index 2ba670ceb76a..2ba670ceb76a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/ActivatableTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/ActivatableTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt index 73f724e7daef..73f724e7daef 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java index fe1b963f6801..fe1b963f6801 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt index 8d060e936cd9..8d060e936cd9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt index ee1c0e99d6ac..ee1c0e99d6ac 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt index 963973588236..963973588236 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/ScreenshotDetectionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDetectionControllerTest.kt index c50702868025..c50702868025 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/ScreenshotDetectionControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDetectionControllerTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerWifiTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerWifiTest.java index 6febb91db992..7a579bacc86d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerWifiTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerWifiTest.java @@ -58,7 +58,7 @@ public class NetworkControllerWifiTest extends NetworkControllerBaseTest { private static final int MIN_RSSI = -100; private static final int MAX_RSSI = -55; private WifiInfo mWifiInfo = mock(WifiInfo.class); - private VcnTransportInfo mVcnTransportInfo = mock(VcnTransportInfo.class); + private VcnTransportInfo mVcnTransportInfo = new VcnTransportInfo.Builder().build(); @Before public void setUp() throws Exception { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index 59fc0d157d54..87cda64ed8e5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -591,8 +591,8 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { ArgumentCaptor<FooterView> captor = ArgumentCaptor.forClass(FooterView.class); verify(mStackScroller).setFooterView(captor.capture()); - assertNotNull(captor.getValue().findViewById(R.id.manage_text).hasOnClickListeners()); - assertNotNull(captor.getValue().findViewById(R.id.dismiss_text).hasOnClickListeners()); + assertNotNull(captor.getValue().findViewById(R.id.manage_text)); + assertNotNull(captor.getValue().findViewById(R.id.dismiss_text)); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt index 328d31014ccc..c48898aad087 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt @@ -136,6 +136,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { private val wifiLogBuffer = LogBuffer("wifi", maxSize = 100, logcatEchoTracker = mock()) private val wifiPickerTrackerCallback = argumentCaptor<WifiPickerTracker.WifiPickerTrackerCallback>() + private val vcnTransportInfo = VcnTransportInfo.Builder().build() private val testDispatcher = StandardTestDispatcher() private val testScope = TestScope(testDispatcher) @@ -1003,6 +1004,18 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { assertThat(latest).isTrue() } + private fun newWifiNetwork(wifiInfo: WifiInfo): Network { + val network = mock<Network>() + val capabilities = + mock<NetworkCapabilities>().also { + whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) + whenever(it.transportInfo).thenReturn(wifiInfo) + } + whenever(connectivityManager.getNetworkCapabilities(network)).thenReturn(capabilities) + + return network + } + /** Regression test for b/272586234. */ @Test fun hasCarrierMergedConnection_carrierMergedViaWifiWithVcnTransport_isTrue() = @@ -1012,10 +1025,12 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { whenever(this.isCarrierMerged).thenReturn(true) whenever(this.isPrimary).thenReturn(true) } + val underlyingWifi = newWifiNetwork(carrierMergedInfo) val caps = mock<NetworkCapabilities>().also { whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) - whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo)) + whenever(it.transportInfo).thenReturn(vcnTransportInfo) + whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi)) } val latest by collectLastValue(underTest.hasCarrierMergedConnection) @@ -1034,10 +1049,12 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { whenever(this.isCarrierMerged).thenReturn(true) whenever(this.isPrimary).thenReturn(true) } + val underlyingWifi = newWifiNetwork(carrierMergedInfo) val caps = mock<NetworkCapabilities>().also { whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) - whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo)) + whenever(it.transportInfo).thenReturn(vcnTransportInfo) + whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi)) } val latest by collectLastValue(underTest.hasCarrierMergedConnection) @@ -1094,10 +1111,15 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { whenever(this.isCarrierMerged).thenReturn(true) whenever(this.isPrimary).thenReturn(true) } + + // The Wifi network that is under the VCN network + val physicalWifiNetwork = newWifiNetwork(carrierMergedInfo) + val underlyingCapabilities = mock<NetworkCapabilities>().also { whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) - whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo)) + whenever(it.transportInfo).thenReturn(vcnTransportInfo) + whenever(it.underlyingNetworks).thenReturn(listOf(physicalWifiNetwork)) } whenever(connectivityManager.getNetworkCapabilities(underlyingCarrierMergedNetwork)) .thenReturn(underlyingCapabilities) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt index 0945742fb325..88f262bec123 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt @@ -23,6 +23,7 @@ import android.net.NetworkCapabilities.TRANSPORT_CELLULAR import android.net.NetworkCapabilities.TRANSPORT_ETHERNET import android.net.NetworkCapabilities.TRANSPORT_VPN import android.net.NetworkCapabilities.TRANSPORT_WIFI +import android.net.TelephonyNetworkSpecifier import android.net.VpnTransportInfo import android.net.vcn.VcnTransportInfo import android.net.wifi.WifiInfo @@ -74,6 +75,8 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() { private val testScope = kosmos.testScope private val tunerService = mock<TunerService>() + private val vcnTransportInfo = VcnTransportInfo.Builder().build() + @Before fun setUp() { createAndSetRepo() @@ -343,6 +346,30 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() { assertThat(latest!!.wifi.isDefault).isTrue() } + private fun newWifiNetwork(wifiInfo: WifiInfo): Network { + val network = mock<Network>() + val capabilities = + mock<NetworkCapabilities>().also { + whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) + whenever(it.transportInfo).thenReturn(wifiInfo) + } + whenever(connectivityManager.getNetworkCapabilities(network)).thenReturn(capabilities) + + return network + } + + private fun newCellNetwork(subId: Int): Network { + val network = mock<Network>() + val capabilities = + mock<NetworkCapabilities>().also { + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(it.networkSpecifier).thenReturn(TelephonyNetworkSpecifier(subId)) + } + whenever(connectivityManager.getNetworkCapabilities(network)).thenReturn(capabilities) + + return network + } + @Test fun defaultConnections_carrierMergedViaWifiWithVcnTransport_wifiAndCarrierMergedDefault() = testScope.runTest { @@ -350,10 +377,12 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() { val carrierMergedInfo = mock<WifiInfo>().apply { whenever(this.isCarrierMerged).thenReturn(true) } + val underlyingWifi = newWifiNetwork(carrierMergedInfo) val capabilities = mock<NetworkCapabilities>().also { whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) - whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo)) + whenever(it.transportInfo).thenReturn(vcnTransportInfo) + whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi)) whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(false) whenever(it.hasTransport(TRANSPORT_ETHERNET)).thenReturn(false) } @@ -373,10 +402,12 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() { val carrierMergedInfo = mock<WifiInfo>().apply { whenever(this.isCarrierMerged).thenReturn(true) } + val underlyingWifi = newWifiNetwork(carrierMergedInfo) val capabilities = mock<NetworkCapabilities>().also { whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) - whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo)) + whenever(it.transportInfo).thenReturn(vcnTransportInfo) + whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi)) whenever(it.hasTransport(TRANSPORT_ETHERNET)).thenReturn(false) whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(false) } @@ -561,10 +592,12 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() { val underlyingCarrierMergedNetwork = mock<Network>() val carrierMergedInfo = mock<WifiInfo>().apply { whenever(this.isCarrierMerged).thenReturn(true) } + val underlyingWifi = newWifiNetwork(carrierMergedInfo) val underlyingCapabilities = mock<NetworkCapabilities>().also { whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) - whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo)) + whenever(it.transportInfo).thenReturn(vcnTransportInfo) + whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi)) } whenever(connectivityManager.getNetworkCapabilities(underlyingCarrierMergedNetwork)) .thenReturn(underlyingCapabilities) @@ -645,14 +678,15 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() { @Test fun vcnSubId_tracksVcnTransportInfo() = testScope.runTest { - val vcnInfo = VcnTransportInfo(SUB_1_ID) + val underlyingCell = newCellNetwork(SUB_1_ID) val latest by collectLastValue(underTest.vcnSubId) val capabilities = mock<NetworkCapabilities>().also { whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) - whenever(it.transportInfo).thenReturn(vcnInfo) + whenever(it.transportInfo).thenReturn(vcnTransportInfo) + whenever(it.underlyingNetworks).thenReturn(listOf(underlyingCell)) } getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) @@ -663,14 +697,15 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() { @Test fun vcnSubId_filersOutInvalid() = testScope.runTest { - val vcnInfo = VcnTransportInfo(INVALID_SUBSCRIPTION_ID) + val underlyingCell = newCellNetwork(INVALID_SUBSCRIPTION_ID) val latest by collectLastValue(underTest.vcnSubId) val capabilities = mock<NetworkCapabilities>().also { whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) - whenever(it.transportInfo).thenReturn(vcnInfo) + whenever(it.transportInfo).thenReturn(vcnTransportInfo) + whenever(it.underlyingNetworks).thenReturn(listOf(underlyingCell)) } getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) @@ -703,11 +738,12 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() { val latest by collectLastValue(underTest.vcnSubId) val wifiInfo = mock<WifiInfo>() - val vcnInfo = VcnTransportInfo(wifiInfo) + val underlyingWifi = newWifiNetwork(wifiInfo) val capabilities = mock<NetworkCapabilities>().also { whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) - whenever(it.transportInfo).thenReturn(vcnInfo) + whenever(it.transportInfo).thenReturn(vcnTransportInfo) + whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi)) } getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) @@ -721,14 +757,15 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() { val latest by collectLastValue(underTest.vcnSubId) val wifiInfo = mock<WifiInfo>() - val wifiVcnInfo = VcnTransportInfo(wifiInfo) - val sub1VcnInfo = VcnTransportInfo(SUB_1_ID) - val sub2VcnInfo = VcnTransportInfo(SUB_2_ID) + val underlyingWifi = newWifiNetwork(wifiInfo) + val underlyingCell1 = newCellNetwork(SUB_1_ID) + val underlyingCell2 = newCellNetwork(SUB_2_ID) val capabilities = mock<NetworkCapabilities>().also { whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) - whenever(it.transportInfo).thenReturn(wifiVcnInfo) + whenever(it.transportInfo).thenReturn(vcnTransportInfo) + whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi)) } // WIFI VCN info @@ -738,14 +775,16 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() { // Cellular VCN info with subId 1 whenever(capabilities.hasTransport(eq(TRANSPORT_CELLULAR))).thenReturn(true) - whenever(capabilities.transportInfo).thenReturn(sub1VcnInfo) + whenever(capabilities.transportInfo).thenReturn(vcnTransportInfo) + whenever(capabilities.underlyingNetworks).thenReturn(listOf(underlyingCell1)) getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) assertThat(latest).isEqualTo(SUB_1_ID) // Cellular VCN info with subId 2 - whenever(capabilities.transportInfo).thenReturn(sub2VcnInfo) + whenever(capabilities.transportInfo).thenReturn(vcnTransportInfo) + whenever(capabilities.underlyingNetworks).thenReturn(listOf(underlyingCell2)) getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) @@ -776,11 +815,12 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() { @Test fun getMainOrUnderlyingWifiInfo_vcnWithWifi_hasInfo() { val wifiInfo = mock<WifiInfo>() - val vcnInfo = VcnTransportInfo(wifiInfo) + val underlyingWifi = newWifiNetwork(wifiInfo) val capabilities = mock<NetworkCapabilities>().also { whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) - whenever(it.transportInfo).thenReturn(vcnInfo) + whenever(it.transportInfo).thenReturn(vcnTransportInfo) + whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi)) } val result = capabilities.getMainOrUnderlyingWifiInfo(connectivityManager) @@ -860,11 +900,15 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() { fun getMainOrUnderlyingWifiInfo_cellular_underlyingVcnWithWifi_hasInfo() { val wifiInfo = mock<WifiInfo>() val underlyingNetwork = mock<Network>() - val underlyingVcnInfo = VcnTransportInfo(wifiInfo) + + // The Wifi network that is under the VCN network + val physicalWifiNetwork = newWifiNetwork(wifiInfo) + val underlyingWifiCapabilities = mock<NetworkCapabilities>().also { whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) - whenever(it.transportInfo).thenReturn(underlyingVcnInfo) + whenever(it.transportInfo).thenReturn(vcnTransportInfo) + whenever(it.underlyingNetworks).thenReturn(listOf(physicalWifiNetwork)) } whenever(connectivityManager.getNetworkCapabilities(underlyingNetwork)) .thenReturn(underlyingWifiCapabilities) @@ -887,11 +931,15 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() { @DisableFlags(FLAG_STATUS_BAR_ALWAYS_CHECK_UNDERLYING_NETWORKS) fun getMainOrUnderlyingWifiInfo_notCellular_underlyingVcnWithWifi_noInfo() { val underlyingNetwork = mock<Network>() - val underlyingVcnInfo = VcnTransportInfo(mock<WifiInfo>()) + + // The Wifi network that is under the VCN network + val physicalWifiNetwork = newWifiNetwork(mock<WifiInfo>()) + val underlyingWifiCapabilities = mock<NetworkCapabilities>().also { whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) - whenever(it.transportInfo).thenReturn(underlyingVcnInfo) + whenever(it.transportInfo).thenReturn(vcnTransportInfo) + whenever(it.underlyingNetworks).thenReturn(listOf(physicalWifiNetwork)) } whenever(connectivityManager.getNetworkCapabilities(underlyingNetwork)) .thenReturn(underlyingWifiCapabilities) @@ -917,10 +965,15 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() { val underlyingCarrierMergedNetwork = mock<Network>() val carrierMergedInfo = mock<WifiInfo>().apply { whenever(this.isCarrierMerged).thenReturn(true) } + + // The Wifi network that is under the VCN network + val physicalWifiNetwork = newWifiNetwork(carrierMergedInfo) + val underlyingCapabilities = mock<NetworkCapabilities>().also { whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) - whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo)) + whenever(it.transportInfo).thenReturn(vcnTransportInfo) + whenever(it.underlyingNetworks).thenReturn(listOf(physicalWifiNetwork)) } whenever(connectivityManager.getNetworkCapabilities(underlyingCarrierMergedNetwork)) .thenReturn(underlyingCapabilities) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/window/MultiDisplayStatusBarWindowControllerStoreTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/window/MultiDisplayStatusBarWindowControllerStoreTest.kt new file mode 100644 index 000000000000..faaa4c415d28 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/window/MultiDisplayStatusBarWindowControllerStoreTest.kt @@ -0,0 +1,120 @@ +/* + * 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.window + +import android.platform.test.annotations.EnableFlags +import android.view.Display +import android.view.WindowManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.app.viewcapture.ViewCaptureAwareWindowManager +import com.android.app.viewcapture.mockViewCaptureAwareWindowManager +import com.android.systemui.SysuiTestCase +import com.android.systemui.display.data.repository.displayRepository +import com.android.systemui.display.data.repository.fakeDisplayWindowPropertiesRepository +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.unconfinedTestDispatcher +import com.android.systemui.statusbar.core.StatusBarConnectedDisplays +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock + +@RunWith(AndroidJUnit4::class) +@SmallTest +@EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) +class MultiDisplayStatusBarWindowControllerStoreTest : SysuiTestCase() { + + private val kosmos = testKosmos().also { it.testDispatcher = it.unconfinedTestDispatcher } + private val testScope = kosmos.testScope + private val fakeDisplayRepository = kosmos.displayRepository + + private val store = + MultiDisplayStatusBarWindowControllerStore( + backgroundApplicationScope = kosmos.applicationCoroutineScope, + controllerFactory = kosmos.fakeStatusBarWindowControllerFactory, + displayWindowPropertiesRepository = kosmos.fakeDisplayWindowPropertiesRepository, + viewCaptureAwareWindowManagerFactory = + object : ViewCaptureAwareWindowManager.Factory { + override fun create( + windowManager: WindowManager + ): ViewCaptureAwareWindowManager { + return kosmos.mockViewCaptureAwareWindowManager + } + }, + displayRepository = fakeDisplayRepository, + ) + + @Before + fun start() { + store.start() + } + + @Before + fun addDisplays() = runBlocking { + fakeDisplayRepository.addDisplay(createDisplay(DEFAULT_DISPLAY_ID)) + fakeDisplayRepository.addDisplay(createDisplay(NON_DEFAULT_DISPLAY_ID)) + } + + @Test + fun forDisplay_defaultDisplay_multipleCalls_returnsSameInstance() = + testScope.runTest { + val controller = store.defaultDisplay + + assertThat(store.defaultDisplay).isSameInstanceAs(controller) + } + + @Test + fun forDisplay_nonDefaultDisplay_multipleCalls_returnsSameInstance() = + testScope.runTest { + val controller = store.forDisplay(NON_DEFAULT_DISPLAY_ID) + + assertThat(store.forDisplay(NON_DEFAULT_DISPLAY_ID)).isSameInstanceAs(controller) + } + + @Test + fun forDisplay_nonDefaultDisplay_afterDisplayRemoved_returnsNewInstance() = + testScope.runTest { + val controller = store.forDisplay(NON_DEFAULT_DISPLAY_ID) + + fakeDisplayRepository.removeDisplay(NON_DEFAULT_DISPLAY_ID) + fakeDisplayRepository.addDisplay(createDisplay(NON_DEFAULT_DISPLAY_ID)) + + assertThat(store.forDisplay(NON_DEFAULT_DISPLAY_ID)).isNotSameInstanceAs(controller) + } + + @Test(expected = IllegalArgumentException::class) + fun forDisplay_nonExistingDisplayId_throws() = + testScope.runTest { store.forDisplay(NON_EXISTING_DISPLAY_ID) } + + private fun createDisplay(displayId: Int): Display = mock { + on { getDisplayId() } doReturn displayId + } + + companion object { + private const val DEFAULT_DISPLAY_ID = Display.DEFAULT_DISPLAY + private const val NON_DEFAULT_DISPLAY_ID = DEFAULT_DISPLAY_ID + 1 + private const val NON_EXISTING_DISPLAY_ID = DEFAULT_DISPLAY_ID + 2 + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/app/viewcapture/ViewCaptureAwareWindowManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/app/viewcapture/ViewCaptureAwareWindowManagerKosmos.kt new file mode 100644 index 000000000000..e1c6699348a9 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/app/viewcapture/ViewCaptureAwareWindowManagerKosmos.kt @@ -0,0 +1,26 @@ +/* + * 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.app.viewcapture + +import com.android.systemui.kosmos.Kosmos +import org.mockito.kotlin.mock + +val Kosmos.mockViewCaptureAwareWindowManager by + Kosmos.Fixture { mock<ViewCaptureAwareWindowManager>() } + +var Kosmos.viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager by + Kosmos.Fixture { mockViewCaptureAwareWindowManager } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelKosmos.kt new file mode 100644 index 000000000000..cac4ff3fe52b --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelKosmos.kt @@ -0,0 +1,38 @@ +/* + * 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.bluetooth.qsdialog + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.audioSharingButtonViewModel: AudioSharingButtonViewModel by + Kosmos.Fixture { + AudioSharingButtonViewModel( + localBluetoothManager, + audioSharingInteractor, + bluetoothStateInteractor, + deviceItemInteractor, + ) + } + +val Kosmos.audioSharingButtonViewModelFactory: AudioSharingButtonViewModel.Factory by + Kosmos.Fixture { + object : AudioSharingButtonViewModel.Factory { + override fun create(): AudioSharingButtonViewModel { + return audioSharingButtonViewModel + } + } + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorKosmos.kt new file mode 100644 index 000000000000..8019efc50391 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorKosmos.kt @@ -0,0 +1,50 @@ +/* + * 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.bluetooth.qsdialog + +import com.android.internal.logging.uiEventLogger +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.plugins.activityStarter + +val Kosmos.audioSharingDeviceItemActionInteractorImpl: AudioSharingDeviceItemActionInteractorImpl by + Kosmos.Fixture { + AudioSharingDeviceItemActionInteractorImpl( + activityStarter, + audioSharingInteractor, + dialogTransitionAnimator, + localBluetoothManager, + testDispatcher, + testDispatcher, + bluetoothTileDialogLogger, + uiEventLogger, + audioSharingDialogDelegateFactory, + deviceItemActionInteractorImpl, + ) + } + +val Kosmos.audioSharingDialogDelegateFactory: AudioSharingDialogDelegate.Factory by + Kosmos.Fixture { + object : AudioSharingDialogDelegate.Factory { + override fun create( + cachedBluetoothDevice: CachedBluetoothDevice + ): AudioSharingDialogDelegate { + return audioSharingDialogDelegate + } + } + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelKosmos.kt new file mode 100644 index 000000000000..b8899de8fdc7 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelKosmos.kt @@ -0,0 +1,65 @@ +/* + * 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.bluetooth.qsdialog + +import android.content.applicationContext +import com.android.internal.logging.uiEventLogger +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.phone.systemUIDialogDotFactory +import kotlinx.coroutines.CoroutineScope +import org.mockito.kotlin.mock + +val Kosmos.cachedBluetoothDevice: CachedBluetoothDevice by Kosmos.Fixture { mock {} } + +val Kosmos.audioSharingDialogViewModel: AudioSharingDialogViewModel by + Kosmos.Fixture { + AudioSharingDialogViewModel( + deviceItemInteractor, + audioSharingInteractor, + applicationContext, + localBluetoothManager, + cachedBluetoothDevice, + testScope.backgroundScope, + testDispatcher + ) + } + +val Kosmos.audioSharingDialogViewModelFactory: AudioSharingDialogViewModel.Factory by + Kosmos.Fixture { + object : AudioSharingDialogViewModel.Factory { + override fun create( + cachedBluetoothDevice: CachedBluetoothDevice, + coroutineScope: CoroutineScope + ): AudioSharingDialogViewModel { + return audioSharingDialogViewModel + } + } + } + +val Kosmos.audioSharingDialogDelegate: AudioSharingDialogDelegate by + Kosmos.Fixture { + AudioSharingDialogDelegate( + cachedBluetoothDevice, + testScope.backgroundScope, + audioSharingDialogViewModelFactory, + systemUIDialogDotFactory, + uiEventLogger + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorKosmos.kt new file mode 100644 index 000000000000..4f4d1da42303 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorKosmos.kt @@ -0,0 +1,29 @@ +/* + * 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.bluetooth.qsdialog + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher + +val Kosmos.audioSharingInteractor: AudioSharingInteractor by + Kosmos.Fixture { + AudioSharingInteractorImpl( + localBluetoothManager, + bluetoothTileDialogAudioSharingRepository, + testDispatcher, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryKosmos.kt new file mode 100644 index 000000000000..d15d0e5ac7ff --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryKosmos.kt @@ -0,0 +1,22 @@ +/* + * 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.bluetooth.qsdialog + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.bluetoothTileDialogAudioSharingRepository by + Kosmos.Fixture { FakeAudioSharingRepository() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorKosmos.kt index 969e26a8d884..969e26a8d884 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorKosmos.kt diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractorKosmos.kt new file mode 100644 index 000000000000..aaa918c9ff35 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractorKosmos.kt @@ -0,0 +1,31 @@ +/* + * 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.bluetooth.qsdialog + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope + +val Kosmos.bluetoothStateInteractor: BluetoothStateInteractor by + Kosmos.Fixture { + BluetoothStateInteractor( + localBluetoothManager, + bluetoothTileDialogLogger, + testScope.backgroundScope, + testDispatcher + ) + } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt index 5ff46346b386..b5b2f5e3e802 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt @@ -20,8 +20,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testDispatcher -import com.android.systemui.plugins.activityStarter -import com.android.systemui.util.mockito.mock +import org.mockito.kotlin.mock val Kosmos.bluetoothTileDialogLogger: BluetoothTileDialogLogger by Kosmos.Fixture { mock {} } @@ -29,14 +28,10 @@ val Kosmos.localBluetoothManager: LocalBluetoothManager by Kosmos.Fixture { mock val Kosmos.dialogTransitionAnimator: DialogTransitionAnimator by Kosmos.Fixture { mock {} } -val Kosmos.deviceItemActionInteractor: DeviceItemActionInteractor by +val Kosmos.deviceItemActionInteractorImpl: DeviceItemActionInteractorImpl by Kosmos.Fixture { - DeviceItemActionInteractor( - activityStarter, - dialogTransitionAnimator, - localBluetoothManager, + DeviceItemActionInteractorImpl( testDispatcher, - bluetoothTileDialogLogger, uiEventLogger, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt new file mode 100644 index 000000000000..a839f17aad82 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt @@ -0,0 +1,70 @@ +/* + * 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.bluetooth.qsdialog + +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class FakeAudioSharingRepository : AudioSharingRepository { + private var mutableAvailable: Boolean = false + + private val mutableInAudioSharing: MutableStateFlow<Boolean> = MutableStateFlow(false) + + private val mutableAudioSourceStateUpdate = MutableSharedFlow<Unit>() + + var sourceAdded: Boolean = false + private set + + private var profile: LocalBluetoothLeBroadcast? = null + + override val leAudioBroadcastProfile: LocalBluetoothLeBroadcast? + get() = profile + + override val audioSourceStateUpdate: Flow<Unit> = mutableAudioSourceStateUpdate + + override val inAudioSharing: StateFlow<Boolean> = mutableInAudioSharing + + override suspend fun audioSharingAvailable(): Boolean = mutableAvailable + + override suspend fun addSource() { + sourceAdded = true + } + + override suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) {} + + override suspend fun startAudioSharing() {} + + fun setAudioSharingAvailable(available: Boolean) { + mutableAvailable = available + } + + fun setInAudioSharing(state: Boolean) { + mutableInAudioSharing.value = state + } + + fun setLeAudioBroadcastProfile(leAudioBroadcastProfile: LocalBluetoothLeBroadcast?) { + profile = leAudioBroadcastProfile + } + + fun emitAudioSourceStateUpdate() { + mutableAudioSourceStateUpdate.tryEmit(Unit) + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayScopeRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayScopeRepositoryKosmos.kt new file mode 100644 index 000000000000..ff4ba61b6965 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayScopeRepositoryKosmos.kt @@ -0,0 +1,26 @@ +/* + * 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.display.data.repository + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher + +val Kosmos.fakeDisplayScopeRepository by + Kosmos.Fixture { FakeDisplayScopeRepository(testDispatcher) } + +var Kosmos.displayScopeRepository: DisplayScopeRepository by + Kosmos.Fixture { fakeDisplayScopeRepository } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryKosmos.kt new file mode 100644 index 000000000000..65b18c102a16 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryKosmos.kt @@ -0,0 +1,25 @@ +/* + * 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.display.data.repository + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.fakeDisplayWindowPropertiesRepository by + Kosmos.Fixture { FakeDisplayWindowPropertiesRepository() } + +var Kosmos.displayWindowPropertiesRepository: DisplayWindowPropertiesRepository by + Kosmos.Fixture { fakeDisplayWindowPropertiesRepository } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayScopeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayScopeRepository.kt new file mode 100644 index 000000000000..3c2592471694 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayScopeRepository.kt @@ -0,0 +1,30 @@ +/* + * 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.display.data.repository + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope + +class FakeDisplayScopeRepository(private val dispatcher: CoroutineDispatcher) : + DisplayScopeRepository { + + private val perDisplayScopes = mutableMapOf<Int, CoroutineScope>() + + override fun scopeForDisplay(displayId: Int): CoroutineScope { + return perDisplayScopes.computeIfAbsent(displayId) { CoroutineScope(dispatcher) } + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayWindowPropertiesRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayWindowPropertiesRepository.kt new file mode 100644 index 000000000000..9282f275b20b --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayWindowPropertiesRepository.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.display.data.repository + +import com.android.systemui.display.shared.model.DisplayWindowProperties +import com.google.common.collect.HashBasedTable +import org.mockito.kotlin.mock + +class FakeDisplayWindowPropertiesRepository : DisplayWindowPropertiesRepository { + + private val properties = HashBasedTable.create<Int, Int, DisplayWindowProperties>() + + override fun get(displayId: Int, windowType: Int): DisplayWindowProperties { + return properties.get(displayId, windowType) + ?: DisplayWindowProperties( + displayId = displayId, + windowType = windowType, + context = mock(), + windowManager = mock(), + ) + .also { properties.put(displayId, windowType, it) } + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt index f97f30383398..522c387a0b08 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt @@ -23,6 +23,7 @@ import android.os.fakeExecutorHandler import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.bouncerRepository import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository +import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor import com.android.systemui.classifier.falsingCollector import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository @@ -152,6 +153,7 @@ class KosmosJavaAdapter() { val wifiInteractor by lazy { kosmos.wifiInteractor } val fakeWifiRepository by lazy { kosmos.fakeWifiRepository } val volumeDialogInteractor by lazy { kosmos.volumeDialogInteractor } + val alternateBouncerInteractor by lazy { kosmos.alternateBouncerInteractor } val ongoingActivityChipsViewModel by lazy { kosmos.ongoingActivityChipsViewModel } val scrimController by lazy { kosmos.scrimController } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt index cfc31c7f301c..10b073e8f331 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.plugins.statusbar import com.android.internal.logging.uiEventLogger +import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor import com.android.systemui.jank.interactionJankMonitor import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor @@ -45,5 +46,6 @@ var Kosmos.statusBarStateController: SysuiStatusBarStateController by { sceneContainerOcclusionInteractor }, { keyguardClockInteractor }, { sceneBackInteractor }, + { alternateBouncerInteractor }, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt new file mode 100644 index 000000000000..10f328be12d2 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt @@ -0,0 +1,27 @@ +/* + * 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.window + +import android.content.Context +import com.android.app.viewcapture.ViewCaptureAwareWindowManager + +class FakeStatusBarWindowControllerFactory : StatusBarWindowController.Factory { + override fun create( + context: Context, + viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager, + ) = FakeStatusBarWindowController() +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerStore.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerStore.kt new file mode 100644 index 000000000000..d19e3227027c --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerStore.kt @@ -0,0 +1,31 @@ +/* + * 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.window + +import android.view.Display + +class FakeStatusBarWindowControllerStore : StatusBarWindowControllerStore { + + private val perDisplayControllers = mutableMapOf<Int, FakeStatusBarWindowController>() + + override val defaultDisplay + get() = forDisplay(Display.DEFAULT_DISPLAY) + + override fun forDisplay(displayId: Int): StatusBarWindowController { + return perDisplayControllers.computeIfAbsent(displayId) { FakeStatusBarWindowController() } + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt index c198b35be289..6c6f243f3953 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt @@ -21,3 +21,15 @@ import com.android.systemui.kosmos.Kosmos val Kosmos.fakeStatusBarWindowController by Kosmos.Fixture { FakeStatusBarWindowController() } var Kosmos.statusBarWindowController by Kosmos.Fixture { fakeStatusBarWindowController } + +val Kosmos.fakeStatusBarWindowControllerStore by + Kosmos.Fixture { FakeStatusBarWindowControllerStore() } + +var Kosmos.statusBarWindowControllerStore: StatusBarWindowControllerStore by + Kosmos.Fixture { fakeStatusBarWindowControllerStore } + +val Kosmos.fakeStatusBarWindowControllerFactory by + Kosmos.Fixture { FakeStatusBarWindowControllerFactory() } + +var Kosmos.statusBarWindowControllerFactory: StatusBarWindowController.Factory by + Kosmos.Fixture { fakeStatusBarWindowControllerFactory } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt index a4719e5a2492..5da6ee95234c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow class FakeAudioSharingRepository : AudioSharingRepository { + private var mutableAvailable: Boolean = false private val mutableInAudioSharing: MutableStateFlow<Boolean> = MutableStateFlow(false) private val mutablePrimaryGroupId: MutableStateFlow<Int> = MutableStateFlow(TEST_GROUP_ID_INVALID) @@ -34,8 +35,14 @@ class FakeAudioSharingRepository : AudioSharingRepository { override val secondaryGroupId: StateFlow<Int> = mutableSecondaryGroupId override val volumeMap: StateFlow<GroupIdToVolumes> = mutableVolumeMap + override suspend fun audioSharingAvailable(): Boolean = mutableAvailable + override suspend fun setSecondaryVolume(volume: Int) {} + fun setAudioSharingAvailable(available: Boolean) { + mutableAvailable = available + } + fun setInAudioSharing(state: Boolean) { mutableInAudioSharing.value = state } diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java index c5fef191c52c..5d574083b326 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java +++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java @@ -65,8 +65,6 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.infra.AndroidFuture; import com.android.internal.util.DumpUtils; import com.android.server.SystemService.TargetUser; -import com.android.server.appfunctions.RemoteServiceCaller.RunServiceCallCallback; -import com.android.server.appfunctions.RemoteServiceCaller.ServiceUsageCompleteListener; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -233,6 +231,9 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { "Caller does not have permission to execute the" + " appfunction", /* extras= */ null)); + throw new SecurityException( + "Caller does not have permission to execute the" + + " appfunction"); } }) .thenCompose( @@ -380,7 +381,8 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { runtimeMetadataSearchSession)); AppFunctionRuntimeMetadata newMetadata = new AppFunctionRuntimeMetadata.Builder(existingMetadata) - .setEnabled(enabledState).build(); + .setEnabled(enabledState) + .build(); AppSearchBatchResult<String, Void> putDocumentBatchResult = runtimeMetadataSearchSession .put( diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index 3bcca1c22c89..2968ff3d0df6 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -16,10 +16,13 @@ package com.android.server.companion.virtual; +import static android.Manifest.permission.ADD_ALWAYS_UNLOCKED_DISPLAY; +import static android.Manifest.permission.ADD_TRUSTED_DISPLAY; import static android.app.admin.DevicePolicyManager.NEARBY_STREAMING_ENABLED; import static android.app.admin.DevicePolicyManager.NEARBY_STREAMING_NOT_CONTROLLED_BY_POLICY; import static android.app.admin.DevicePolicyManager.NEARBY_STREAMING_SAME_MANAGED_ACCOUNT_ONLY; import static android.companion.virtual.VirtualDeviceParams.ACTIVITY_POLICY_DEFAULT_ALLOWED; +import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM; import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT; import static android.companion.virtual.VirtualDeviceParams.NAVIGATION_POLICY_DEFAULT_ALLOWED; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_ACTIVITY; @@ -425,6 +428,27 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub mDisplayManager = displayManager; mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class); mPowerManager = context.getSystemService(PowerManager.class); + + if (mDevicePolicies.get(POLICY_TYPE_CLIPBOARD, DEVICE_POLICY_DEFAULT) + != DEVICE_POLICY_DEFAULT) { + if (mContext.checkCallingOrSelfPermission(ADD_TRUSTED_DISPLAY) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Requires ADD_TRUSTED_DISPLAY permission to " + + "set a custom clipboard policy."); + } + } + + int flags = DEFAULT_VIRTUAL_DISPLAY_FLAGS; + if (mParams.getLockState() == VirtualDeviceParams.LOCK_STATE_ALWAYS_UNLOCKED) { + if (mContext.checkCallingOrSelfPermission(ADD_ALWAYS_UNLOCKED_DISPLAY) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Requires ADD_ALWAYS_UNLOCKED_DISPLAY permission to " + + "create an always unlocked virtual device."); + } + flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED; + } + mBaseVirtualDisplayFlags = flags; + if (inputController == null) { mInputController = new InputController( context.getMainThreadHandler(), @@ -467,12 +491,6 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub : mParams.getAllowedActivities(); } - int flags = DEFAULT_VIRTUAL_DISPLAY_FLAGS; - if (mParams.getLockState() == VirtualDeviceParams.LOCK_STATE_ALWAYS_UNLOCKED) { - flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED; - } - mBaseVirtualDisplayFlags = flags; - if (Flags.vdmCustomIme() && mParams.getInputMethodComponent() != null) { final String imeId = mParams.getInputMethodComponent().flattenToShortString(); Slog.d(TAG, "Setting custom input method " + imeId + " as default for virtual device " @@ -884,8 +902,12 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub synchronized (mVirtualDeviceLock) { mDevicePolicies.put(policyType, devicePolicy); for (int i = 0; i < mVirtualDisplays.size(); i++) { - mVirtualDisplays.valueAt(i).getWindowPolicyController() - .setShowInHostDeviceRecents(devicePolicy == DEVICE_POLICY_DEFAULT); + VirtualDisplayWrapper wrapper = mVirtualDisplays.valueAt(i); + if (wrapper.isTrusted()) { + wrapper.getWindowPolicyController() + .setShowInHostDeviceRecents( + devicePolicy == DEVICE_POLICY_DEFAULT); + } } } break; @@ -905,7 +927,20 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub break; case POLICY_TYPE_CLIPBOARD: if (Flags.crossDeviceClipboard()) { + if (policyType == DEVICE_POLICY_CUSTOM + && mContext.checkCallingOrSelfPermission(ADD_TRUSTED_DISPLAY) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Requires ADD_TRUSTED_DISPLAY permission to " + + "set a custom clipboard policy."); + } synchronized (mVirtualDeviceLock) { + for (int i = 0; i < mVirtualDisplays.size(); i++) { + VirtualDisplayWrapper wrapper = mVirtualDisplays.valueAt(i); + if (!wrapper.isTrusted() && !wrapper.isMirror()) { + throw new SecurityException("All displays must be trusted for " + + "devices with custom clipboard policy."); + } + } mDevicePolicies.put(policyType, devicePolicy); } } @@ -936,8 +971,11 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub checkDisplayOwnedByVirtualDeviceLocked(displayId); switch (policyType) { case POLICY_TYPE_RECENTS: - mVirtualDisplays.get(displayId).getWindowPolicyController() - .setShowInHostDeviceRecents(devicePolicy == DEVICE_POLICY_DEFAULT); + VirtualDisplayWrapper wrapper = mVirtualDisplays.get(displayId); + if (wrapper.isTrusted()) { + wrapper.getWindowPolicyController() + .setShowInHostDeviceRecents(devicePolicy == DEVICE_POLICY_DEFAULT); + } break; case POLICY_TYPE_ACTIVITY: mVirtualDisplays.get(displayId).getWindowPolicyController() @@ -1247,10 +1285,13 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub try { synchronized (mVirtualDeviceLock) { mDefaultShowPointerIcon = showPointerIcon; - } - final int[] displayIds = getDisplayIds(); - for (int i = 0; i < displayIds.length; ++i) { - mInputController.setShowPointerIcon(showPointerIcon, displayIds[i]); + for (int i = 0; i < mVirtualDisplays.size(); i++) { + VirtualDisplayWrapper wrapper = mVirtualDisplays.valueAt(i); + if (wrapper.isTrusted() || wrapper.isMirror()) { + mInputController.setShowPointerIcon( + mDefaultShowPointerIcon, mVirtualDisplays.keyAt(i)); + } + } } } finally { Binder.restoreCallingIdentity(ident); @@ -1491,6 +1532,12 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub boolean isTrustedDisplay = (mDisplayManagerInternal.getDisplayInfo(displayId).flags & Display.FLAG_TRUSTED) == Display.FLAG_TRUSTED; + if (!isTrustedDisplay) { + if (getDevicePolicy(POLICY_TYPE_CLIPBOARD) != DEVICE_POLICY_DEFAULT) { + throw new SecurityException("All displays must be trusted for devices with custom" + + "clipboard policy."); + } + } boolean showPointer; synchronized (mVirtualDeviceLock) { @@ -1500,7 +1547,8 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub "Virtual device already has a virtual display with ID " + displayId); } - PowerManager.WakeLock wakeLock = createAndAcquireWakeLockForDisplay(displayId); + PowerManager.WakeLock wakeLock = + isTrustedDisplay ? createAndAcquireWakeLockForDisplay(displayId) : null; mVirtualDisplays.put(displayId, new VirtualDisplayWrapper(callback, gwpc, wakeLock, isTrustedDisplay, isMirrorDisplay)); showPointer = mDefaultShowPointerIcon; @@ -1508,14 +1556,15 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub final long token = Binder.clearCallingIdentity(); try { - mInputController.setShowPointerIcon(showPointer, displayId); mInputController.setMousePointerAccelerationEnabled(false, displayId); mInputController.setDisplayEligibilityForPointerCapture(/* isEligible= */ false, displayId); - // WM throws a SecurityException if the display is untrusted. if (isTrustedDisplay) { + mInputController.setShowPointerIcon(showPointer, displayId); mInputController.setDisplayImePolicy(displayId, WindowManager.DISPLAY_IME_POLICY_LOCAL); + } else { + gwpc.setShowInHostDeviceRecents(true); } } finally { Binder.restoreCallingIdentity(token); @@ -1616,6 +1665,11 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub != PackageManager.PERMISSION_GRANTED) { synchronized (mVirtualDeviceLock) { checkDisplayOwnedByVirtualDeviceLocked(displayId); + VirtualDisplayWrapper wrapper = mVirtualDisplays.get(displayId); + if (!wrapper.isTrusted() && !wrapper.isMirror()) { + throw new SecurityException( + "Cannot create input device associated with an untrusted display"); + } } } } @@ -1665,7 +1719,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub * @param virtualDisplayWrapper - VirtualDisplayWrapper to release resources for. */ private void releaseOwnedVirtualDisplayResources(VirtualDisplayWrapper virtualDisplayWrapper) { - virtualDisplayWrapper.getWakeLock().release(); + virtualDisplayWrapper.releaseWakeLock(); virtualDisplayWrapper.getWindowPolicyController().unregisterRunningAppsChangedListener( this); } @@ -1833,10 +1887,10 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub VirtualDisplayWrapper(@NonNull IVirtualDisplayCallback token, @NonNull GenericWindowPolicyController windowPolicyController, - @NonNull PowerManager.WakeLock wakeLock, boolean isTrusted, boolean isMirror) { + @Nullable PowerManager.WakeLock wakeLock, boolean isTrusted, boolean isMirror) { mToken = Objects.requireNonNull(token); mWindowPolicyController = Objects.requireNonNull(windowPolicyController); - mWakeLock = Objects.requireNonNull(wakeLock); + mWakeLock = wakeLock; mIsTrusted = isTrusted; mIsMirror = isMirror; } @@ -1845,8 +1899,10 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub return mWindowPolicyController; } - PowerManager.WakeLock getWakeLock() { - return mWakeLock; + void releaseWakeLock() { + if (mWakeLock != null && mWakeLock.isHeld()) { + mWakeLock.release(); + } } boolean isTrusted() { diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index 2485626a8f42..5236b0399f25 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -3629,42 +3629,68 @@ public final class ProcessList { } @GuardedBy({"mService", "mProcLock"}) - private int updateLruProcessInternalLSP(ProcessRecord app, long now, int index, - int lruSeq, String what, Object obj, ProcessRecord srcApp) { + private int offerLruProcessInternalLSP(ProcessRecord app, long now, String what, Object obj, + ProcessRecord srcApp) { app.setLastActivityTime(now); if (app.hasActivitiesOrRecentTasks()) { // Don't want to touch dependent processes that are hosting activities. - return index; + return -1; } - int lrui = mLruProcesses.lastIndexOf(app); + final int lrui = mLruProcesses.lastIndexOf(app); if (lrui < 0) { Slog.wtf(TAG, "Adding dependent process " + app + " not on LRU list: " + what + " " + obj + " from " + srcApp); - return index; } + return lrui; + } - if (lrui >= index) { - // Don't want to cause this to move dependent processes *back* in the - // list as if they were less frequently used. - return index; - } + /** + * This method is called after the indices array is populated by the indices offered by + * {@link #offerLruProcessInternalLSP} to actually move the processes to the desired locations + * in the LRU list. Since the indices array is a SparseBooleanArray, the indices are sorted + * and this allows us to preserve the previous order of the processes relative to each other. + * Key of the indices array holds the current index of the process in the LRU list and the value + * is a boolean indicating whether the process is an activity process or not. Activity processes + * are moved to the nextActivityIndex and non-activity processes are moved to the nextIndex + * positions, which are provided by the caller. + * + * @param indices The indices of the processes to move. + * @param nextActivityIndex The next index to insert an activity process. + * @param nextIndex The next index to insert a non-activity process. + */ + @GuardedBy({"mService", "mProcLock"}) + private void completeLruProcessInternalLSP(SparseBooleanArray indices, int nextActivityIndex, + int nextIndex) { + for (int i = indices.size() - 1; i >= 0; i--) { + final int lrui = indices.keyAt(i); + if (lrui < 0) { + // Rest of the indices are invalid, we can return early. + return; + } + final boolean isActivity = indices.valueAt(i); + int index = isActivity ? nextActivityIndex : nextIndex; - if (lrui >= mLruProcessActivityStart && index < mLruProcessActivityStart) { - // Don't want to touch dependent processes that are hosting activities. - return index; - } + if (lrui >= index) { + // Don't want to cause this to move dependent processes *back* in the + // list as if they were less frequently used. + continue; + } - mLruProcesses.remove(lrui); - if (index > 0) { + final ProcessRecord app = mLruProcesses.remove(lrui); index--; + if (DEBUG_LRU) Slog.d(TAG_LRU, "Moving dep from " + lrui + " to " + index + + " in LRU list: " + app); + mLruProcesses.add(index, app); + app.setLruSeq(mLruSeq); + + if (isActivity) { + nextActivityIndex = index; + } else { + nextIndex = index; + } } - if (DEBUG_LRU) Slog.d(TAG_LRU, "Moving dep from " + lrui + " to " + index - + " in LRU list: " + app); - mLruProcesses.add(index, app); - app.setLruSeq(lruSeq); - return index; } /** @@ -4058,6 +4084,15 @@ public final class ProcessList { app.setLruSeq(mLruSeq); + // Key of the indices array holds the current index of the process in the LRU list and the + // value is a boolean indicating whether the process is an activity process or not. + // Activity processes will be moved to the nextActivityIndex and non-activity processes will + // be moved to the nextIndex positions when completeLruProcessInternalLSP is called. + // Since SparseBooleanArray's keys are sorted, we'll be able to keep the existing order of + // the processes relative to each other after the move. + final SparseBooleanArray indices = new SparseBooleanArray(psr.numberOfConnections() + + app.mProviders.numberOfProviderConnections()); + // If the app is currently using a content provider or service, // bump those processes as well. for (int j = psr.numberOfConnections() - 1; j >= 0; j--) { @@ -4069,16 +4104,12 @@ public final class ProcessList { && !cr.binding.service.app.isPersistent()) { if (cr.binding.service.app.mServices.hasClientActivities()) { if (nextActivityIndex >= 0) { - nextActivityIndex = updateLruProcessInternalLSP(cr.binding.service.app, - now, - nextActivityIndex, mLruSeq, - "service connection", cr, app); + indices.append(offerLruProcessInternalLSP(cr.binding.service.app, now, + "service connection", cr, app), true); } } else { - nextIndex = updateLruProcessInternalLSP(cr.binding.service.app, - now, - nextIndex, mLruSeq, - "service connection", cr, app); + indices.append(offerLruProcessInternalLSP(cr.binding.service.app, now, + "service connection", cr, app), false); } } } @@ -4086,10 +4117,11 @@ public final class ProcessList { for (int j = ppr.numberOfProviderConnections() - 1; j >= 0; j--) { ContentProviderRecord cpr = ppr.getProviderConnectionAt(j).provider; if (cpr.proc != null && cpr.proc.getLruSeq() != mLruSeq && !cpr.proc.isPersistent()) { - nextIndex = updateLruProcessInternalLSP(cpr.proc, now, nextIndex, mLruSeq, - "provider reference", cpr, app); + indices.append(offerLruProcessInternalLSP(cpr.proc, now, + "provider reference", cpr, app), false); } } + completeLruProcessInternalLSP(indices, nextActivityIndex, nextIndex); } @GuardedBy(anyOf = {"mService", "mProcLock"}) diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index d206b20f5b25..fdf7dec31cad 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -286,7 +286,6 @@ import java.util.Objects; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.CancellationException; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -8049,7 +8048,14 @@ public class AudioService extends IAudioService.Stub } synchronized (mAbsoluteVolumeDeviceInfoMapLock) { if (mAbsoluteVolumeDeviceInfoMap.containsKey(audioSystemDeviceOut)) { - return mAbsoluteVolumeDeviceInfoMap.get(audioSystemDeviceOut).mDeviceVolumeBehavior; + final AbsoluteVolumeDeviceInfo deviceInfo = mAbsoluteVolumeDeviceInfoMap.get( + audioSystemDeviceOut); + if (deviceInfo != null) { + return deviceInfo.mDeviceVolumeBehavior; + } + + Log.e(TAG, + "Null absolute volume device info stored for key " + audioSystemDeviceOut); } } @@ -15043,6 +15049,11 @@ public class AudioService extends IAudioService.Stub private void addAudioSystemDeviceOutToAbsVolumeDevices(int audioSystemDeviceOut, AbsoluteVolumeDeviceInfo info) { + if (info == null) { + Log.e(TAG, "Cannot add null absolute volume info for audioSystemDeviceOut " + + audioSystemDeviceOut); + return; + } if (DEBUG_VOL) { Log.d(TAG, "Adding DeviceType: 0x" + Integer.toHexString(audioSystemDeviceOut) + " to mAbsoluteVolumeDeviceInfoMap with behavior " diff --git a/services/core/java/com/android/server/compat/PlatformCompat.java b/services/core/java/com/android/server/compat/PlatformCompat.java index a9fe8cb01b3a..8d64383b32b9 100644 --- a/services/core/java/com/android/server/compat/PlatformCompat.java +++ b/services/core/java/com/android/server/compat/PlatformCompat.java @@ -242,7 +242,8 @@ public class PlatformCompat extends IPlatformCompat.Stub { boolean enabled = true; final int userId = UserHandle.getUserId(uid); for (String packageName : packages) { - final var appInfo = getApplicationInfo(packageName, userId); + final var appInfo = + fixTargetSdk(getApplicationInfo(packageName, userId), uid); enabled &= isChangeEnabledInternal(changeId, appInfo); } return enabled; @@ -261,7 +262,8 @@ public class PlatformCompat extends IPlatformCompat.Stub { boolean enabled = true; final int userId = UserHandle.getUserId(uid); for (String packageName : packages) { - final var appInfo = getApplicationInfo(packageName, userId); + final var appInfo = + fixTargetSdk(getApplicationInfo(packageName, userId), uid); enabled &= isChangeEnabledInternalNoLogging(changeId, appInfo); } return enabled; @@ -504,6 +506,15 @@ public class PlatformCompat extends IPlatformCompat.Stub { packageName, 0, Process.myUid(), userId); } + private ApplicationInfo fixTargetSdk(ApplicationInfo appInfo, int uid) { + // b/282922910 - we don't want apps sharing system uid and targeting + // older target sdk to impact all system uid apps + if (Flags.systemUidTargetSystemSdk() && uid == Process.SYSTEM_UID) { + appInfo.targetSdkVersion = Build.VERSION.SDK_INT; + } + return appInfo; + } + private void killPackage(String packageName) { int uid = LocalServices.getService(PackageManagerInternal.class).getPackageUid(packageName, 0, UserHandle.myUserId()); diff --git a/services/core/java/com/android/server/compat/platform_compat_flags.aconfig b/services/core/java/com/android/server/compat/platform_compat_flags.aconfig new file mode 100644 index 000000000000..fb323238c38e --- /dev/null +++ b/services/core/java/com/android/server/compat/platform_compat_flags.aconfig @@ -0,0 +1,10 @@ +package: "com.android.server.compat" +container: "system" + +flag { + name: "system_uid_target_system_sdk" + namespace: "app_compat" + description: "Compat framework feature flag for forcing all system uid apps to target system sdk" + bug: "29702703" + is_fixed_read_only: true +} diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 179ec63458dd..c7a70fafbf26 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -1769,10 +1769,11 @@ public final class DisplayManagerService extends SystemService { flags &= ~VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP; } // Put the display in the virtual device's display group only if it's not a mirror display, - // and if it doesn't need its own display group. So effectively, mirror displays go into the - // default display group. + // it is a trusted display, and it doesn't need its own display group. So effectively, + // mirror and untrusted displays go into the default display group. if ((flags & VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) == 0 && (flags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) == 0 + && (flags & VIRTUAL_DISPLAY_FLAG_TRUSTED) == VIRTUAL_DISPLAY_FLAG_TRUSTED && virtualDevice != null) { flags |= VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP; } @@ -1848,9 +1849,7 @@ public final class DisplayManagerService extends SystemService { if (callingUid != Process.SYSTEM_UID && (flags & VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) != 0) { - // The virtualDevice instance has been validated above using isValidVirtualDevice - if (virtualDevice == null - && !checkCallingPermission(ADD_TRUSTED_DISPLAY, "createVirtualDisplay()")) { + if (!checkCallingPermission(ADD_TRUSTED_DISPLAY, "createVirtualDisplay()")) { throw new SecurityException("Requires ADD_TRUSTED_DISPLAY permission to " + "create a virtual display which is not in the default DisplayGroup."); } @@ -5667,6 +5666,11 @@ public final class DisplayManagerService extends SystemService { displayPowerController.stylusGestureStarted(eventTime); } } + + @Override + public boolean isDisplayReadyForMirroring(int displayId) { + return mExternalDisplayPolicy.isDisplayReadyForMirroring(displayId); + } } class DesiredDisplayModeSpecsObserver diff --git a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java index 28a0b28a0167..f34d2cc6e684 100644 --- a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java +++ b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java @@ -375,6 +375,54 @@ class ExternalDisplayPolicy { } } + boolean isDisplayReadyForMirroring(int displayId) { + if (!mFlags.isWaitingConfirmationBeforeMirroringEnabled()) { + if (DEBUG) { + Slog.d(TAG, "isDisplayReadyForMirroring: mirroring CONFIRMED - " + + " flag 'waiting for confirmation before mirroring' is disabled"); + } + return true; + } + + synchronized (mSyncRoot) { + if (!mIsBootCompleted) { + if (DEBUG) { + Slog.d(TAG, "isDisplayReadyForMirroring: mirroring is not confirmed - " + + "boot is in progress"); + } + return false; + } + + var logicalDisplay = mLogicalDisplayMapper.getDisplayLocked(displayId); + if (logicalDisplay == null) { + if (DEBUG) { + Slog.d(TAG, "isDisplayReadyForMirroring: mirroring is not confirmed - " + + "logicalDisplay is null"); + } + return false; + } + + if (!isExternalDisplayLocked(logicalDisplay)) { + if (DEBUG) { + Slog.d(TAG, "isDisplayReadyForMirroring: mirroring is not confirmed - " + + "logicalDisplay" + logicalDisplay.getDisplayIdLocked() + + " type is " + logicalDisplay.getDisplayInfoLocked().type); + } + return false; + } + + if (!logicalDisplay.isEnabledLocked()) { + if (DEBUG) { + Slog.d(TAG, "isDisplayReadyForMirroring: mirroring is not confirmed - " + + "logicalDisplay is disabled"); + } + return false; + } + } + + return true; + } + private final class SkinThermalStatusObserver extends IThermalEventListener.Stub { @Override public void notifyThrottling(@NonNull final Temperature temp) { diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java index 06a910396d6c..09fa4e6aa628 100644 --- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java +++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java @@ -16,6 +16,7 @@ package com.android.server.display; +import static android.hardware.devicestate.DeviceState.PROPERTY_EMULATED_ONLY; import static android.hardware.devicestate.DeviceState.PROPERTY_POWER_CONFIGURATION_TRIGGER_SLEEP; import static android.hardware.devicestate.DeviceState.PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE; import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE; @@ -594,6 +595,13 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { boolean shouldDeviceBeWoken(DeviceState pendingState, DeviceState currentState, boolean isInteractive, boolean isBootCompleted) { if (mDeviceStateManagerFlags.deviceStatePropertyMigration()) { + if (currentState.hasProperties(PROPERTY_EMULATED_ONLY) + && !pendingState.hasProperties(PROPERTY_EMULATED_ONLY)) { + // Do not wake the device, since this transition may occur due to the user pressing + // the power button to exit an emulated state. + return false; + } + return pendingState.hasProperty(PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE) && !currentState.equals(INVALID_DEVICE_STATE) && !currentState.hasProperty(PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE) 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 99ced7f8bb00..b2e98bc05e75 100644 --- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java +++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java @@ -217,6 +217,11 @@ public class DisplayManagerFlags { Flags::enableUserRefreshRateForExternalDisplay ); + private final FlagState mEnableWaitingConfirmationBeforeMirroring = new FlagState( + Flags.FLAG_ENABLE_WAITING_CONFIRMATION_BEFORE_MIRRORING, + Flags::enableWaitingConfirmationBeforeMirroring + ); + private final FlagState mEnableBatteryStatsForAllDisplays = new FlagState( Flags.FLAG_ENABLE_BATTERY_STATS_FOR_ALL_DISPLAYS, Flags::enableBatteryStatsForAllDisplays @@ -445,6 +450,14 @@ public class DisplayManagerFlags { } /** + * @return {@code true} if mirroring won't be enabled until boot completes and the user enables + * the display. + */ + public boolean isWaitingConfirmationBeforeMirroringEnabled() { + return mEnableWaitingConfirmationBeforeMirroring.isEnabled(); + } + + /** * @return {@code true} if battery stats is enabled for all displays, not just the primary * display. */ @@ -511,6 +524,7 @@ public class DisplayManagerFlags { pw.println(" " + mVirtualDisplayLimit); pw.println(" " + mNormalBrightnessForDozeParameter); pw.println(" " + mIdleScreenConfigInSubscribingLightSensor); + pw.println(" " + mEnableWaitingConfirmationBeforeMirroring); pw.println(" " + mEnableBatteryStatsForAllDisplays); pw.println(" " + mBlockAutobrightnessChangesOnStylusUsage); pw.println(" " + mIsUserRefreshRateForExternalDisplayEnabled); diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig index 2f04d9e5fdbb..df626385c5cc 100644 --- a/services/core/java/com/android/server/display/feature/display_flags.aconfig +++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig @@ -367,6 +367,17 @@ flag { } flag { + name: "enable_waiting_confirmation_before_mirroring" + namespace: "display_manager" + description: "Allow ContentRecorder checking whether user confirmed mirroring after boot" + bug: "361698995" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "enable_battery_stats_for_all_displays" namespace: "display_manager" description: "Flag to enable battery stats for all displays." diff --git a/services/core/java/com/android/server/input/InputSettingsObserver.java b/services/core/java/com/android/server/input/InputSettingsObserver.java index d70bd8b17ddf..d1a6d3b9bb00 100644 --- a/services/core/java/com/android/server/input/InputSettingsObserver.java +++ b/services/core/java/com/android/server/input/InputSettingsObserver.java @@ -63,6 +63,12 @@ class InputSettingsObserver extends ContentObserver { mObservers = Map.ofEntries( Map.entry(Settings.System.getUriFor(Settings.System.POINTER_SPEED), (reason) -> updateMousePointerSpeed()), + Map.entry(Settings.System.getUriFor( + Settings.System.MOUSE_REVERSE_VERTICAL_SCROLLING), + (reason) -> updateMouseReverseVerticalScrolling()), + Map.entry(Settings.System.getUriFor( + Settings.System.MOUSE_SWAP_PRIMARY_BUTTON), + (reason) -> updateMouseSwapPrimaryButton()), Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_POINTER_SPEED), (reason) -> updateTouchpadPointerSpeed()), Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_NATURAL_SCROLLING), @@ -163,6 +169,16 @@ class InputSettingsObserver extends ContentObserver { mNative.setPointerSpeed(constrainPointerSpeedValue(speed)); } + private void updateMouseReverseVerticalScrolling() { + mNative.setMouseReverseVerticalScrollingEnabled( + InputSettings.isMouseReverseVerticalScrollingEnabled(mContext)); + } + + private void updateMouseSwapPrimaryButton() { + mNative.setMouseSwapPrimaryButtonEnabled( + InputSettings.isMouseSwapPrimaryButtonEnabled(mContext)); + } + private void updateTouchpadPointerSpeed() { mNative.setTouchpadPointerSpeed( constrainPointerSpeedValue(InputSettings.getTouchpadPointerSpeed(mContext))); diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java index 4404d63e02fc..21e8bccd2883 100644 --- a/services/core/java/com/android/server/input/NativeInputManagerService.java +++ b/services/core/java/com/android/server/input/NativeInputManagerService.java @@ -127,6 +127,10 @@ interface NativeInputManagerService { void setMousePointerAccelerationEnabled(int displayId, boolean enabled); + void setMouseReverseVerticalScrollingEnabled(boolean enabled); + + void setMouseSwapPrimaryButtonEnabled(boolean enabled); + void setTouchpadPointerSpeed(int speed); void setTouchpadNaturalScrollingEnabled(boolean enabled); @@ -388,6 +392,12 @@ interface NativeInputManagerService { public native void setMousePointerAccelerationEnabled(int displayId, boolean enabled); @Override + public native void setMouseReverseVerticalScrollingEnabled(boolean enabled); + + @Override + public native void setMouseSwapPrimaryButtonEnabled(boolean enabled); + + @Override public native void setTouchpadPointerSpeed(int speed); @Override diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index f0fb33eaaee7..35b517118aab 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -84,6 +84,7 @@ import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.pm.UserInfo; import android.content.res.Resources; +import android.hardware.display.DisplayManagerInternal; import android.hardware.input.InputManager; import android.inputmethodservice.InputMethodService; import android.inputmethodservice.InputMethodService.BackDispositionMode; @@ -119,6 +120,7 @@ import android.util.Printer; import android.util.Slog; import android.util.SparseArray; import android.util.proto.ProtoOutputStream; +import android.view.Display; import android.view.InputChannel; import android.view.InputDevice; import android.view.MotionEvent; @@ -448,6 +450,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. private AudioManagerInternal mAudioManagerInternal = null; @Nullable private VirtualDeviceManagerInternal mVdmInternal = null; + @Nullable + private DisplayManagerInternal mDisplayManagerInternal = null; // Mapping from deviceId to the device-specific imeId for that device. @GuardedBy("ImfLock.class") @@ -2165,7 +2169,18 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final var bindingController = getInputMethodBindingController(userId); final int oldDeviceId = bindingController.getDeviceIdToShowIme(); final int displayIdToShowIme = bindingController.getDisplayIdToShowIme(); - final int newDeviceId = mVdmInternal.getDeviceIdForDisplayId(displayIdToShowIme); + int newDeviceId = mVdmInternal.getDeviceIdForDisplayId(displayIdToShowIme); + if (newDeviceId != DEVICE_ID_DEFAULT) { + // Only show custom IME on trusted displays. + if (mDisplayManagerInternal == null) { + mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class); + } + int displayFlags = mDisplayManagerInternal.getDisplayInfo(displayIdToShowIme).flags; + if ((displayFlags & Display.FLAG_TRUSTED) != Display.FLAG_TRUSTED) { + // If the display is not trusted, fallback to the default device IME. + newDeviceId = DEVICE_ID_DEFAULT; + } + } bindingController.setDeviceIdToShowIme(newDeviceId); if (newDeviceId == DEVICE_ID_DEFAULT) { if (oldDeviceId == DEVICE_ID_DEFAULT) { diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySnapshotStorage.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySnapshotStorage.java index c02b103f1d33..404c8411f750 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySnapshotStorage.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySnapshotStorage.java @@ -19,7 +19,6 @@ package com.android.server.locksettings.recoverablekeystore.storage; import android.annotation.Nullable; import android.os.Environment; import android.security.keystore.recovery.KeyChainSnapshot; -import android.util.Log; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; @@ -29,9 +28,11 @@ import com.android.server.locksettings.recoverablekeystore.serialization import com.android.server.locksettings.recoverablekeystore.serialization .KeyChainSnapshotParserException; import com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSerializer; +import com.android.server.utils.Slogf; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.security.cert.CertificateEncodingException; @@ -81,12 +82,14 @@ public class RecoverySnapshotStorage { public synchronized void put(int uid, KeyChainSnapshot snapshot) { mSnapshotByUid.put(uid, snapshot); - try { - writeToDisk(uid, snapshot); + File snapshotFile = getSnapshotFile(uid); + try (FileOutputStream fileOutputStream = new FileOutputStream(snapshotFile)) { + KeyChainSnapshotSerializer.serialize(snapshot, fileOutputStream); } catch (IOException | CertificateEncodingException e) { - Log.e(TAG, - String.format(Locale.US, "Error persisting snapshot for %d to disk", uid), - e); + // If we fail to write the latest snapshot, we should delete any older snapshot that + // happens to be around. Otherwise snapshot syncs might end up going 'back in time'. + snapshotFile.delete(); + Slogf.e(TAG, e, "Error persisting snapshot for %d to disk", uid); } } @@ -100,10 +103,17 @@ public class RecoverySnapshotStorage { return snapshot; } - try { - return readFromDisk(uid); + File snapshotFile = getSnapshotFile(uid); + try (FileInputStream fileInputStream = new FileInputStream(snapshotFile)) { + return KeyChainSnapshotDeserializer.deserialize(fileInputStream); + } catch (FileNotFoundException e) { + Slogf.i(TAG, "Snapshot for uid %d not found", uid); + return null; } catch (IOException | KeyChainSnapshotParserException e) { - Log.e(TAG, String.format(Locale.US, "Error reading snapshot for %d from disk", uid), e); + // If we fail to read the latest snapshot, we should delete it in case it is in some way + // corrupted. We can regenerate snapshots anyway. + snapshotFile.delete(); + Slogf.e(TAG, e, "Error reading snapshot for %d from disk", uid); return null; } } @@ -116,50 +126,6 @@ public class RecoverySnapshotStorage { getSnapshotFile(uid).delete(); } - /** - * Writes the snapshot for recovery agent {@code uid} to disk. - * - * @throws IOException if an IO error occurs writing to disk. - */ - private void writeToDisk(int uid, KeyChainSnapshot snapshot) - throws IOException, CertificateEncodingException { - File snapshotFile = getSnapshotFile(uid); - - try ( - FileOutputStream fileOutputStream = new FileOutputStream(snapshotFile) - ) { - KeyChainSnapshotSerializer.serialize(snapshot, fileOutputStream); - } catch (IOException | CertificateEncodingException e) { - // If we fail to write the latest snapshot, we should delete any older snapshot that - // happens to be around. Otherwise snapshot syncs might end up going 'back in time'. - snapshotFile.delete(); - throw e; - } - } - - /** - * Reads the last snapshot for recovery agent {@code uid} from disk. - * - * @return The snapshot, or null if none existed. - * @throws IOException if an IO error occurs reading from disk. - */ - @Nullable - private KeyChainSnapshot readFromDisk(int uid) - throws IOException, KeyChainSnapshotParserException { - File snapshotFile = getSnapshotFile(uid); - - try ( - FileInputStream fileInputStream = new FileInputStream(snapshotFile) - ) { - return KeyChainSnapshotDeserializer.deserialize(fileInputStream); - } catch (IOException | KeyChainSnapshotParserException e) { - // If we fail to read the latest snapshot, we should delete it in case it is in some way - // corrupted. We can regenerate snapshots anyway. - snapshotFile.delete(); - throw e; - } - } - private File getSnapshotFile(int uid) { File folder = getStorageFolder(); String fileName = getSnapshotFileName(uid); diff --git a/services/core/java/com/android/server/pm/InstallRequest.java b/services/core/java/com/android/server/pm/InstallRequest.java index 1f79ac0afdac..089bbb7b0022 100644 --- a/services/core/java/com/android/server/pm/InstallRequest.java +++ b/services/core/java/com/android/server/pm/InstallRequest.java @@ -16,7 +16,6 @@ package com.android.server.pm; -import static android.content.pm.Flags.improveInstallFreeze; import static android.content.pm.PackageInstaller.SessionParams.USER_ACTION_UNSPECIFIED; import static android.content.pm.PackageManager.INSTALL_REASON_UNKNOWN; import static android.content.pm.PackageManager.INSTALL_SCENARIO_DEFAULT; @@ -1050,13 +1049,13 @@ final class InstallRequest { } public void onFreezeStarted() { - if (mPackageMetrics != null && improveInstallFreeze()) { + if (mPackageMetrics != null) { mPackageMetrics.onStepStarted(PackageMetrics.STEP_FREEZE_INSTALL); } } public void onFreezeCompleted() { - if (mPackageMetrics != null && improveInstallFreeze()) { + if (mPackageMetrics != null) { mPackageMetrics.onStepFinished(PackageMetrics.STEP_FREEZE_INSTALL); } } diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 8bab9de903ba..708e0679d6d4 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -1101,7 +1101,7 @@ public class UserManagerService extends IUserManager.Stub { if (android.multiuser.Flags.cachesNotInvalidatedAtStartReadOnly()) { UserManager.invalidateIsUserUnlockedCache(); UserManager.invalidateQuietModeEnabledCache(); - UserManager.invalidateUserSerialNumberCache(); + UserManager.invalidateCacheOnUserListChange(); } } @@ -4448,7 +4448,7 @@ public class UserManagerService extends IUserManager.Stub { if (userData != null) { synchronized (mUsersLock) { - mUsers.put(userData.info.id, userData); + addUserDataLU(userData); if (mNextSerialNumber < 0 || mNextSerialNumber <= userData.info.id) { mNextSerialNumber = userData.info.id + 1; @@ -5724,7 +5724,7 @@ public class UserManagerService extends IUserManager.Stub { userData.info = userInfo; userData.userProperties = new UserProperties( userTypeDetails.getDefaultUserPropertiesReference()); - mUsers.put(userId, userData); + addUserDataLU(userData); } writeUserLP(userData); writeUserListLP(); @@ -6138,7 +6138,7 @@ public class UserManagerService extends IUserManager.Stub { final UserData userData = new UserData(); userData.info = userInfo; synchronized (mUsersLock) { - mUsers.put(userInfo.id, userData); + addUserDataLU(userData); } updateUserIds(); return userData; @@ -6148,8 +6148,7 @@ public class UserManagerService extends IUserManager.Stub { @VisibleForTesting void removeUserInfo(@UserIdInt int userId) { synchronized (mUsersLock) { - UserManager.invalidateUserSerialNumberCache(); - mUsers.remove(userId); + removeUserDataLU(userId); } } @@ -6579,8 +6578,7 @@ public class UserManagerService extends IUserManager.Stub { // Remove this user from the list synchronized (mUsersLock) { - UserManager.invalidateUserSerialNumberCache(); - mUsers.remove(userId); + removeUserDataLU(userId); mIsUserManaged.delete(userId); } synchronized (mUserStates) { @@ -6969,6 +6967,26 @@ public class UserManagerService extends IUserManager.Stub { } /** + * Adding user data to mUsers list in one place to invalidate related caches. + */ + @GuardedBy("mUsersLock") + private void addUserDataLU(UserData userData) { + if (android.multiuser.Flags.invalidateCacheOnUsersChangedReadOnly()) { + UserManager.invalidateCacheOnUserListChange(); + } + mUsers.put(userData.info.id, userData); + } + + /** + * Removing user data to mUsers list in one place to invalidate related caches. + */ + @GuardedBy("mUsersLock") + private void removeUserDataLU(@UserIdInt int userId) { + UserManager.invalidateCacheOnUserListChange(); + mUsers.remove(userId); + } + + /** * Caches the list of user ids in an array, adjusting the array size when necessary. */ private void updateUserIds() { diff --git a/services/core/java/com/android/server/wm/ConfigurationContainer.java b/services/core/java/com/android/server/wm/ConfigurationContainer.java index 670a61dca5c8..05dcbb7f9af4 100644 --- a/services/core/java/com/android/server/wm/ConfigurationContainer.java +++ b/services/core/java/com/android/server/wm/ConfigurationContainer.java @@ -25,6 +25,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.app.WindowConfiguration.ROTATION_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.app.WindowConfiguration.activityTypeToString; @@ -268,7 +269,16 @@ public abstract class ConfigurationContainer<E extends ConfigurationContainer> { } final DisplayPolicy.DecorInsets.Info decor = displayContent.getDisplayPolicy().getDecorInsetsInfo(rotation, dw, dh); - outAppBounds.intersectUnchecked(decor.mOverrideNonDecorFrame); + if (!outAppBounds.intersect(decor.mOverrideNonDecorFrame)) { + // TODO (b/364883053): When a split screen is requested from an app intent for a new + // task, the bounds is not the final bounds, and this is also not a bounds change + // event handled correctly with the offset. Revert back to legacy method for this + // case. + if (inOutConfig.windowConfiguration.getWindowingMode() + == WINDOWING_MODE_MULTI_WINDOW) { + outAppBounds.inset(decor.mOverrideNonDecorInsets); + } + } if (task != null && (task.mOffsetYForInsets != 0 || task.mOffsetXForInsets != 0)) { outAppBounds.offset(-task.mOffsetXForInsets, -task.mOffsetYForInsets); } diff --git a/services/core/java/com/android/server/wm/ContentRecorder.java b/services/core/java/com/android/server/wm/ContentRecorder.java index bc33946ef618..0b5872b3e601 100644 --- a/services/core/java/com/android/server/wm/ContentRecorder.java +++ b/services/core/java/com/android/server/wm/ContentRecorder.java @@ -285,6 +285,11 @@ final class ContentRecorder implements WindowContainerListener { } } + private boolean isDisplayReadyForMirroring() { + return mDisplayContent.getDisplayInfo().type != Display.TYPE_EXTERNAL + || mDisplayContent.mWmService.mDisplayManagerInternal.isDisplayReadyForMirroring( + mDisplayContent.getDisplayId()); + } /** * Ensure recording does not fall back to the display stack; ensure the recording is stopped @@ -335,7 +340,7 @@ final class ContentRecorder implements WindowContainerListener { return; } - if (mContentRecordingSession.isWaitingForConsent()) { + if (mContentRecordingSession.isWaitingForConsent() || !isDisplayReadyForMirroring()) { ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, "Content Recording: waiting to record, so do " + "nothing"); return; diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index efca90217e83..248ed1a58b75 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -337,6 +337,8 @@ public: int32_t getMousePointerSpeed(); void setPointerSpeed(int32_t speed); void setMousePointerAccelerationEnabled(ui::LogicalDisplayId displayId, bool enabled); + void setMouseReverseVerticalScrollingEnabled(bool enabled); + void setMouseSwapPrimaryButtonEnabled(bool enabled); void setTouchpadPointerSpeed(int32_t speed); void setTouchpadNaturalScrollingEnabled(bool enabled); void setTouchpadTapToClickEnabled(bool enabled); @@ -482,6 +484,12 @@ private: // True if stylus button reporting through motion events is enabled. bool stylusButtonMotionEventsEnabled{true}; + // True if mouse vertical scrolling is reversed. + bool mouseReverseVerticalScrollingEnabled{false}; + + // True if the mouse primary button is swapped (left/right buttons). + bool mouseSwapPrimaryButtonEnabled{false}; + // The touchpad pointer speed, as a number from -7 (slowest) to 7 (fastest). int32_t touchpadPointerSpeed{0}; @@ -762,6 +770,10 @@ void NativeInputManager::getReaderConfiguration(InputReaderConfiguration* outCon outConfig->defaultPointerDisplayId = mLocked.pointerDisplayId; + outConfig->mouseReverseVerticalScrollingEnabled = + mLocked.mouseReverseVerticalScrollingEnabled; + outConfig->mouseSwapPrimaryButtonEnabled = mLocked.mouseSwapPrimaryButtonEnabled; + outConfig->touchpadPointerSpeed = mLocked.touchpadPointerSpeed; outConfig->touchpadNaturalScrollingEnabled = mLocked.touchpadNaturalScrollingEnabled; outConfig->touchpadTapToClickEnabled = mLocked.touchpadTapToClickEnabled; @@ -1317,6 +1329,36 @@ int32_t NativeInputManager::getMousePointerSpeed() { return mLocked.pointerSpeed; } +void NativeInputManager::setMouseReverseVerticalScrollingEnabled(bool enabled) { + { // acquire lock + std::scoped_lock _l(mLock); + + if (mLocked.mouseReverseVerticalScrollingEnabled == enabled) { + return; + } + + mLocked.mouseReverseVerticalScrollingEnabled = enabled; + } // release lock + + mInputManager->getReader().requestRefreshConfiguration( + InputReaderConfiguration::Change::MOUSE_SETTINGS); +} + +void NativeInputManager::setMouseSwapPrimaryButtonEnabled(bool enabled) { + { // acquire lock + std::scoped_lock _l(mLock); + + if (mLocked.mouseSwapPrimaryButtonEnabled == enabled) { + return; + } + + mLocked.mouseSwapPrimaryButtonEnabled = enabled; + } // release lock + + mInputManager->getReader().requestRefreshConfiguration( + InputReaderConfiguration::Change::MOUSE_SETTINGS); +} + void NativeInputManager::setPointerSpeed(int32_t speed) { { // acquire lock std::scoped_lock _l(mLock); @@ -3002,6 +3044,18 @@ static jint nativeGetLastUsedInputDeviceId(JNIEnv* env, jobject nativeImplObj) { return static_cast<jint>(im->getInputManager()->getReader().getLastUsedInputDeviceId()); } +static void nativeSetMouseReverseVerticalScrollingEnabled(JNIEnv* env, jobject nativeImplObj, + bool enabled) { + NativeInputManager* im = getNativeInputManager(env, nativeImplObj); + im->setMouseReverseVerticalScrollingEnabled(enabled); +} + +static void nativeSetMouseSwapPrimaryButtonEnabled(JNIEnv* env, jobject nativeImplObj, + bool enabled) { + NativeInputManager* im = getNativeInputManager(env, nativeImplObj); + im->setMouseSwapPrimaryButtonEnabled(enabled); +} + // ---------------------------------------------------------------------------- static const JNINativeMethod gInputManagerMethods[] = { @@ -3048,6 +3102,9 @@ static const JNINativeMethod gInputManagerMethods[] = { {"setPointerSpeed", "(I)V", (void*)nativeSetPointerSpeed}, {"setMousePointerAccelerationEnabled", "(IZ)V", (void*)nativeSetMousePointerAccelerationEnabled}, + {"setMouseReverseVerticalScrollingEnabled", "(Z)V", + (void*)nativeSetMouseReverseVerticalScrollingEnabled}, + {"setMouseSwapPrimaryButtonEnabled", "(Z)V", (void*)nativeSetMouseSwapPrimaryButtonEnabled}, {"setTouchpadPointerSpeed", "(I)V", (void*)nativeSetTouchpadPointerSpeed}, {"setTouchpadNaturalScrollingEnabled", "(Z)V", (void*)nativeSetTouchpadNaturalScrollingEnabled}, diff --git a/services/core/jni/com_android_server_utils_AnrTimer.cpp b/services/core/jni/com_android_server_utils_AnrTimer.cpp index 2836d46b0f1a..2add5b09f15b 100644 --- a/services/core/jni/com_android_server_utils_AnrTimer.cpp +++ b/services/core/jni/com_android_server_utils_AnrTimer.cpp @@ -349,7 +349,7 @@ class AnrTimerTracer { return nullptr; } - // Return the currently watched pids. The lock must be held. + // Return the currently watched pids as a comma-separated list. The lock must be held. std::string watchedPidsLocked() const { if (watched_.size() == 0) return "none"; bool first = true; @@ -357,6 +357,7 @@ class AnrTimerTracer { for (auto i = watched_.cbegin(); i != watched_.cend(); i++) { if (first) { result += StringPrintf("%d", *i); + first = false; } else { result += StringPrintf(",%d", *i); } diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java index 1a1c8e50ba0a..94eab9cda968 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -25,6 +25,7 @@ import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_ import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION; +import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED; import static android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY; import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK; import static android.view.Display.HdrCapabilities.HDR_TYPE_INVALID; @@ -1068,9 +1069,9 @@ public class DisplayManagerServiceTest { firstDisplayId); } - /** Tests that the virtual device is created in a device display group. */ + /** Tests that a trusted virtual display is created in a device display group. */ @Test - public void createVirtualDisplay_addsDisplaysToDeviceDisplayGroups() throws Exception { + public void createVirtualDisplay_addsTrustedDisplaysToDeviceDisplayGroups() throws Exception { DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerInternal localService = displayManager.new LocalService(); @@ -1081,12 +1082,16 @@ public class DisplayManagerServiceTest { IVirtualDevice virtualDevice = mock(IVirtualDevice.class); when(virtualDevice.getDeviceId()).thenReturn(1); when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true); + + when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY)) + .thenReturn(PackageManager.PERMISSION_GRANTED); + // Create a first virtual display. A display group should be created for this display on the // virtual device. final VirtualDisplayConfig.Builder builder1 = new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320) - .setUniqueId("uniqueId --- device display group 1"); - + .setUniqueId("uniqueId --- device display group") + .setFlags(VIRTUAL_DISPLAY_FLAG_TRUSTED); int displayId1 = localService.createVirtualDisplay( builder1.build(), @@ -1097,12 +1102,14 @@ public class DisplayManagerServiceTest { verify(mMockProjectionService, never()).setContentRecordingSession(any(), nullable(IMediaProjection.class)); int displayGroupId1 = localService.getDisplayInfo(displayId1).displayGroupId; + assertNotEquals(displayGroupId1, Display.DEFAULT_DISPLAY_GROUP); // Create a second virtual display. This should be added to the previously created display // group. final VirtualDisplayConfig.Builder builder2 = new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320) - .setUniqueId("uniqueId --- device display group 1"); + .setUniqueId("uniqueId --- device display group") + .setFlags(VIRTUAL_DISPLAY_FLAG_TRUSTED); int displayId2 = localService.createVirtualDisplay( @@ -1121,6 +1128,36 @@ public class DisplayManagerServiceTest { displayGroupId2); } + /** Tests that an untrusted virtual display is created in the default display group. */ + @Test + public void createVirtualDisplay_addsUntrustedDisplayToDefaultDisplayGroups() throws Exception { + DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + DisplayManagerInternal localService = displayManager.new LocalService(); + + registerDefaultDisplays(displayManager); + when(mMockAppToken.asBinder()).thenReturn(mMockAppToken); + + IVirtualDevice virtualDevice = mock(IVirtualDevice.class); + when(virtualDevice.getDeviceId()).thenReturn(1); + when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true); + // Create the virtual display. It is untrusted, so it should go into the default group. + final VirtualDisplayConfig.Builder builder = + new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320) + .setUniqueId("uniqueId --- device display group"); + + int displayId = + localService.createVirtualDisplay( + builder.build(), + mMockAppToken /* callback */, + virtualDevice /* virtualDeviceToken */, + mock(DisplayWindowPolicyController.class), + PACKAGE_NAME); + verify(mMockProjectionService, never()).setContentRecordingSession(any(), + nullable(IMediaProjection.class)); + int displayGroupId = localService.getDisplayInfo(displayId).displayGroupId; + assertEquals(displayGroupId, Display.DEFAULT_DISPLAY_GROUP); + } + /** * Tests that the virtual display is not added to the device display group when * VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP is set. @@ -1138,11 +1175,15 @@ public class DisplayManagerServiceTest { when(virtualDevice.getDeviceId()).thenReturn(1); when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true); + when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY)) + .thenReturn(PackageManager.PERMISSION_GRANTED); + // Create a first virtual display. A display group should be created for this display on the // virtual device. final VirtualDisplayConfig.Builder builder1 = new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320) - .setUniqueId("uniqueId --- device display group"); + .setUniqueId("uniqueId --- device display group") + .setFlags(VIRTUAL_DISPLAY_FLAG_TRUSTED); int displayId1 = localService.createVirtualDisplay( @@ -1154,12 +1195,14 @@ public class DisplayManagerServiceTest { verify(mMockProjectionService, never()).setContentRecordingSession(any(), nullable(IMediaProjection.class)); int displayGroupId1 = localService.getDisplayInfo(displayId1).displayGroupId; + assertNotEquals(displayGroupId1, Display.DEFAULT_DISPLAY_GROUP); // Create a second virtual display. With the flag VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP, // the display should not be added to the previously created display group. final VirtualDisplayConfig.Builder builder2 = new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320) - .setFlags(VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) + .setFlags(VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP + | VIRTUAL_DISPLAY_FLAG_TRUSTED) .setUniqueId("uniqueId --- own display group"); when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true); @@ -1174,6 +1217,7 @@ public class DisplayManagerServiceTest { verify(mMockProjectionService, never()).setContentRecordingSession(any(), nullable(IMediaProjection.class)); int displayGroupId2 = localService.getDisplayInfo(displayId2).displayGroupId; + assertNotEquals(displayGroupId2, Display.DEFAULT_DISPLAY_GROUP); assertNotEquals( "Display 1 should be in the device display group and display 2 in its own display" @@ -1208,7 +1252,8 @@ public class DisplayManagerServiceTest { final VirtualDisplayConfig deviceDisplayGroupDisplayConfig = new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320) .setUniqueId("uniqueId --- device display group 1") - .setFlags(VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED) + .setFlags(VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED + | VIRTUAL_DISPLAY_FLAG_TRUSTED) .build(); int deviceDisplayGroupDisplayId = @@ -1235,6 +1280,7 @@ public class DisplayManagerServiceTest { .setUniqueId("uniqueId --- own display group 1") .setFlags( VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED + | VIRTUAL_DISPLAY_FLAG_TRUSTED | VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) .build(); @@ -1852,7 +1898,7 @@ public class DisplayManagerServiceTest { /** * Tests that specifying VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP is allowed when the permission - * ADD_TRUSTED_DISPLAY is granted. + * ADD_TRUSTED_DISPLAY is granted and that display is not in the default display group. */ @Test public void testOwnDisplayGroup_allowCreationWithAddTrustedDisplayPermission() @@ -1881,6 +1927,9 @@ public class DisplayManagerServiceTest { DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayId); assertNotNull(ddi); assertNotEquals(0, ddi.flags & DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP); + + int displayGroupId = bs.getDisplayInfo(displayId).displayGroupId; + assertNotEquals(displayGroupId, Display.DEFAULT_DISPLAY_GROUP); } /** @@ -1915,11 +1964,11 @@ public class DisplayManagerServiceTest { } /** - * Tests that specifying VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP is allowed when called with - * a virtual device, even if ADD_TRUSTED_DISPLAY is not granted. + * Tests that specifying VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP is not allowed when called with + * a virtual device, if ADD_TRUSTED_DISPLAY is not granted. */ @Test - public void testOwnDisplayGroup_allowCreationWithVirtualDevice() throws Exception { + public void testOwnDisplayGroup_disallowCreationWithVirtualDevice() throws Exception { DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerInternal localService = displayManager.new LocalService(); @@ -1940,16 +1989,16 @@ public class DisplayManagerServiceTest { when(virtualDevice.getDeviceId()).thenReturn(1); when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true); - int displayId = localService.createVirtualDisplay(builder.build(), - mMockAppToken /* callback */, virtualDevice /* virtualDeviceToken */, - mock(DisplayWindowPolicyController.class), PACKAGE_NAME); - verify(mMockProjectionService, never()).setContentRecordingSession(any(), - nullable(IMediaProjection.class)); - performTraversalInternal(displayManager); - displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */); - DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayId); - assertNotNull(ddi); - assertNotEquals(0, ddi.flags & DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP); + try { + localService.createVirtualDisplay(builder.build(), + mMockAppToken /* callback */, virtualDevice /* virtualDeviceToken */, + mock(DisplayWindowPolicyController.class), PACKAGE_NAME); + fail("Creating virtual display with VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP without " + + "ADD_TRUSTED_DISPLAY permission should throw SecurityException even if " + + "called with a virtual device."); + } catch (SecurityException e) { + // SecurityException is expected + } } /** diff --git a/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java b/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java index f72816889c3b..782262d3f7c9 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java @@ -18,6 +18,7 @@ package com.android.server.display; import static android.hardware.display.DisplayManagerGlobal.EVENT_DISPLAY_CONNECTED; import static android.view.Display.TYPE_EXTERNAL; +import static android.view.Display.TYPE_INTERNAL; import static com.google.common.truth.Truth.assertThat; @@ -36,6 +37,7 @@ import android.os.IThermalEventListener; import android.os.IThermalService; import android.os.RemoteException; import android.os.Temperature; +import android.view.Display; import android.view.DisplayInfo; import androidx.test.filters.SmallTest; @@ -97,6 +99,8 @@ public class ExternalDisplayPolicyTest { @Mock private LogicalDisplay mMockedLogicalDisplay; @Mock + private LogicalDisplay mMockedDefaultDisplay; + @Mock private DisplayNotificationManager mMockedDisplayNotificationManager; @Mock private ExternalDisplayStatsService mMockedExternalDisplayStatsService; @@ -141,6 +145,15 @@ public class ExternalDisplayPolicyTest { when(mMockedLogicalDisplay.getDisplayInfoLocked()).thenReturn(mockedLogicalDisplayInfo); when(mMockedLogicalDisplayMapper.getDisplayLocked(EXTERNAL_DISPLAY_ID)).thenReturn( mMockedLogicalDisplay); + + // Initialize default logical display + when(mMockedDefaultDisplay.getDisplayIdLocked()).thenReturn(Display.DEFAULT_DISPLAY); + when(mMockedDefaultDisplay.isEnabledLocked()).thenReturn(true); + final var mockedDefaultDisplayInfo = new DisplayInfo(); + mockedDefaultDisplayInfo.type = TYPE_INTERNAL; + when(mMockedDefaultDisplay.getDisplayInfoLocked()).thenReturn(mockedDefaultDisplayInfo); + when(mMockedLogicalDisplayMapper.getDisplayLocked(Display.DEFAULT_DISPLAY)).thenReturn( + mMockedDefaultDisplay); } @Test @@ -293,6 +306,52 @@ public class ExternalDisplayPolicyTest { verify(mMockedLogicalDisplayMapper, never()).forEachLocked(any()); } + @Test + public void testMirroringAlwaysConfirmedByUser_flagDisabled() { + when(mMockedFlags.isWaitingConfirmationBeforeMirroringEnabled()).thenReturn(false); + assertThat(mExternalDisplayPolicy.isDisplayReadyForMirroring(EXTERNAL_DISPLAY_ID)).isTrue(); + } + + @Test + public void testMirroringConfirmed_afterBootForEnabledDisplay() { + when(mMockedFlags.isWaitingConfirmationBeforeMirroringEnabled()).thenReturn(true); + mExternalDisplayPolicy.onBootCompleted(); + assertThat(mExternalDisplayPolicy.isDisplayReadyForMirroring(EXTERNAL_DISPLAY_ID)) + .isTrue(); + } + + @Test + public void testMirroringNotConfirmed_afterBootForDisabledDisplay() { + when(mMockedFlags.isWaitingConfirmationBeforeMirroringEnabled()).thenReturn(true); + mExternalDisplayPolicy.onBootCompleted(); + when(mMockedLogicalDisplay.isEnabledLocked()).thenReturn(false); + assertThat(mExternalDisplayPolicy.isDisplayReadyForMirroring(EXTERNAL_DISPLAY_ID)) + .isFalse(); + } + + @Test + public void testMirroringNeverConfirmed_forNonExternalDisplays() { + when(mMockedFlags.isWaitingConfirmationBeforeMirroringEnabled()).thenReturn(true); + mExternalDisplayPolicy.onBootCompleted(); + assertThat(mExternalDisplayPolicy.isDisplayReadyForMirroring(Display.DEFAULT_DISPLAY)) + .isFalse(); + } + + @Test + public void testMirroringNeverConfirmed_forNonExistingDisplays() { + when(mMockedFlags.isWaitingConfirmationBeforeMirroringEnabled()).thenReturn(true); + mExternalDisplayPolicy.onBootCompleted(); + assertThat(mExternalDisplayPolicy.isDisplayReadyForMirroring(Display.INVALID_DISPLAY)) + .isFalse(); + } + + @Test + public void testMirroringNeverConfirmed_duringBoot() { + when(mMockedFlags.isWaitingConfirmationBeforeMirroringEnabled()).thenReturn(true); + assertThat(mExternalDisplayPolicy.isDisplayReadyForMirroring(EXTERNAL_DISPLAY_ID)) + .isFalse(); + } + private void setTemperature(final IThermalEventListener thermalEventListener, final List<Temperature> temperature) throws RemoteException { for (var t : temperature) { diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java index 1729ad5ff19f..d831cf8a3643 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java @@ -121,6 +121,8 @@ public class LogicalDisplayMapperTest { Set.of(DeviceState.PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE), Collections.emptySet()); private static final DeviceState DEVICE_STATE_OPEN = createDeviceState(2, "Two", Set.of(DeviceState.PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE), Collections.emptySet()); + private static final DeviceState DEVICE_STATE_EMULATED = createDeviceState(3, "Three", + Set.of(DeviceState.PROPERTY_EMULATED_ONLY), Collections.emptySet()); private static final int FLAG_GO_TO_SLEEP_ON_FOLD = 0; private static final int FLAG_GO_TO_SLEEP_FLAG_SOFT_SLEEP = 2; private static int sNextNonDefaultDisplayId = DEFAULT_DISPLAY + 1; @@ -686,6 +688,14 @@ public class LogicalDisplayMapperTest { } @Test + public void testDeviceShouldNotBeWokenWhenExitingEmulatedState() { + assertFalse(mLogicalDisplayMapper.shouldDeviceBeWoken(DEVICE_STATE_OPEN, + DEVICE_STATE_EMULATED, + /* isInteractive= */false, + /* isBootCompleted= */true)); + } + + @Test public void testDeviceShouldBePutToSleep() { assertTrue(mLogicalDisplayMapper.shouldDeviceBePutToSleep(DEVICE_STATE_CLOSED, DEVICE_STATE_OPEN, diff --git a/services/tests/displayservicetests/src/com/android/server/display/PersistentDataStoreTest.java b/services/tests/displayservicetests/src/com/android/server/display/PersistentDataStoreTest.java index 5676a388acff..6d14065b6248 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/PersistentDataStoreTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/PersistentDataStoreTest.java @@ -459,7 +459,6 @@ public class PersistentDataStoreTest { ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); newInjector.setReadStream(bais); newDataStore.loadIfNeeded(); - assertNotNull(newDataStore.getUserPreferredRefreshRate(testDisplayDevice)); assertEquals(85.3f, mDataStore.getUserPreferredRefreshRate(testDisplayDevice), 01.f); assertEquals(85.3f, newDataStore.getUserPreferredRefreshRate(testDisplayDevice), 0.1f); } diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java index 584fd6270c69..40b9c61a0597 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java @@ -25,6 +25,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSess import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; import static com.android.server.job.Flags.FLAG_COUNT_QUOTA_FIX; +import static com.android.server.job.Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_FGS_JOBS; import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX; import static com.android.server.job.JobSchedulerService.EXEMPTED_INDEX; import static com.android.server.job.JobSchedulerService.FREQUENT_INDEX; @@ -303,6 +304,12 @@ public class QuotaControllerTest { } } + private int getProcessStateQuotaFreeThreshold() { + synchronized (mQuotaController.mLock) { + return mQuotaController.getProcessStateQuotaFreeThreshold(); + } + } + private void setProcessState(int procState) { setProcessState(procState, mSourceUid); } @@ -315,7 +322,7 @@ public class QuotaControllerTest { final boolean contained = foregroundUids.get(uid); mUidObserver.onUidStateChanged(uid, procState, 0, ActivityManager.PROCESS_CAPABILITY_NONE); - if (procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) { + if (procState <= getProcessStateQuotaFreeThreshold()) { if (!contained) { verify(foregroundUids, timeout(2 * SECOND_IN_MILLIS).times(1)) .put(eq(uid), eq(true)); @@ -1371,7 +1378,7 @@ public class QuotaControllerTest { } setDischarging(); - setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); + setProcessState(getProcessStateQuotaFreeThreshold()); synchronized (mQuotaController.mLock) { assertEquals(timeUntilQuotaConsumedMs, mQuotaController.getMaxJobExecutionTimeMsLocked((job))); @@ -1473,7 +1480,7 @@ public class QuotaControllerTest { } setDischarging(); - setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); + setProcessState(getProcessStateQuotaFreeThreshold()); synchronized (mQuotaController.mLock) { assertEquals(mQcConstants.EJ_LIMIT_WORKING_MS / 2, mQuotaController.getMaxJobExecutionTimeMsLocked(job)); @@ -1505,7 +1512,7 @@ public class QuotaControllerTest { createTimingSession(sElapsedRealtimeClock.millis() - mQcConstants.EJ_WINDOW_SIZE_MS, timeUsedMs, 5), true); - setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); + setProcessState(getProcessStateQuotaFreeThreshold()); synchronized (mQuotaController.mLock) { assertEquals(mQcConstants.EJ_LIMIT_WORKING_MS / 2, mQuotaController.getMaxJobExecutionTimeMsLocked(job)); @@ -4126,7 +4133,7 @@ public class QuotaControllerTest { } advanceElapsedClock(5 * SECOND_IN_MILLIS); // Change to a state that should still be considered foreground. - setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); + setProcessState(getProcessStateQuotaFreeThreshold()); advanceElapsedClock(5 * SECOND_IN_MILLIS); synchronized (mQuotaController.mLock) { mQuotaController.maybeStopTrackingJobLocked(jobStatus, null); @@ -4134,6 +4141,36 @@ public class QuotaControllerTest { assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); } + /** Tests that Timers count FOREGROUND_SERVICE jobs. */ + @Test + @RequiresFlagsEnabled(FLAG_ENFORCE_QUOTA_POLICY_TO_FGS_JOBS) + public void testTimerTracking_Fgs() { + setDischarging(); + + JobStatus jobStatus = createJobStatus("testTimerTracking_Fgs", 1); + setProcessState(ActivityManager.PROCESS_STATE_BOUND_TOP); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStartTrackingJobLocked(jobStatus, null); + } + + assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); + + synchronized (mQuotaController.mLock) { + mQuotaController.prepareForExecutionLocked(jobStatus); + } + advanceElapsedClock(5 * SECOND_IN_MILLIS); + // Change to FOREGROUND_SERVICE state that should count. + setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); + long start = JobSchedulerService.sElapsedRealtimeClock.millis(); + advanceElapsedClock(5 * SECOND_IN_MILLIS); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStopTrackingJobLocked(jobStatus, null); + } + List<TimingSession> expected = new ArrayList<>(); + expected.add(createTimingSession(start, 5 * SECOND_IN_MILLIS, 1)); + assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)); + } + /** * Tests that Timers properly track sessions when switching between foreground and background * states. @@ -4180,7 +4217,7 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1)); - setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); + setProcessState(getProcessStateQuotaFreeThreshold()); synchronized (mQuotaController.mLock) { mQuotaController.prepareForExecutionLocked(jobFg3); } @@ -4213,7 +4250,7 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1)); - setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); + setProcessState(getProcessStateQuotaFreeThreshold()); synchronized (mQuotaController.mLock) { mQuotaController.prepareForExecutionLocked(jobFg3); } @@ -4262,7 +4299,7 @@ public class QuotaControllerTest { } assertEquals(0, stats.jobCountInRateLimitingWindow); - setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); + setProcessState(getProcessStateQuotaFreeThreshold()); synchronized (mQuotaController.mLock) { mQuotaController.prepareForExecutionLocked(jobFg1); } @@ -4412,7 +4449,7 @@ public class QuotaControllerTest { mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1); } advanceElapsedClock(5 * SECOND_IN_MILLIS); - setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); + setProcessState(getProcessStateQuotaFreeThreshold()); synchronized (mQuotaController.mLock) { mQuotaController.prepareForExecutionLocked(jobFg1); } @@ -4625,7 +4662,7 @@ public class QuotaControllerTest { // App still in foreground so everything should be in quota. advanceElapsedClock(20 * SECOND_IN_MILLIS); - setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); + setProcessState(getProcessStateQuotaFreeThreshold()); assertTrue(jobTop.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); assertTrue(jobFg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); assertTrue(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); @@ -5901,7 +5938,7 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1)); - setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); + setProcessState(getProcessStateQuotaFreeThreshold()); synchronized (mQuotaController.mLock) { mQuotaController.prepareForExecutionLocked(jobFg3); } @@ -5935,7 +5972,7 @@ public class QuotaControllerTest { } advanceElapsedClock(10 * SECOND_IN_MILLIS); expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1)); - setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); + setProcessState(getProcessStateQuotaFreeThreshold()); synchronized (mQuotaController.mLock) { mQuotaController.prepareForExecutionLocked(jobFg3); } @@ -6056,7 +6093,7 @@ public class QuotaControllerTest { mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1); } advanceElapsedClock(5 * SECOND_IN_MILLIS); - setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); + setProcessState(getProcessStateQuotaFreeThreshold()); synchronized (mQuotaController.mLock) { mQuotaController.prepareForExecutionLocked(jobFg1); } @@ -6534,7 +6571,7 @@ public class QuotaControllerTest { // App still in foreground so everything should be in quota. advanceElapsedClock(20 * SECOND_IN_MILLIS); - setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); + setProcessState(getProcessStateQuotaFreeThreshold()); assertTrue(jobTop2.isExpeditedQuotaApproved()); assertTrue(jobFg.isExpeditedQuotaApproved()); assertTrue(jobBg.isExpeditedQuotaApproved()); diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index dad45b3048b9..e09933a55fb9 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -245,6 +245,8 @@ public class VirtualDeviceManagerServiceTest { @Mock private IDisplayManager mIDisplayManager; @Mock + private WindowManager mWindowManager; + @Mock private VirtualDeviceImpl.PendingTrampolineCallback mPendingTrampolineCallback; @Mock private DevicePolicyManager mDevicePolicyManagerMock; @@ -383,8 +385,7 @@ public class VirtualDeviceManagerServiceTest { // Allow virtual devices to be created on the looper thread for testing. final InputController.DeviceCreationThreadVerifier threadVerifier = () -> true; mInputController = new InputController(mNativeWrapperMock, - new Handler(TestableLooper.get(this).getLooper()), - mContext.getSystemService(WindowManager.class), + new Handler(TestableLooper.get(this).getLooper()), mWindowManager, AttributionSource.myAttributionSource(), threadVerifier); mCameraAccessController = new CameraAccessController(mContext, mLocalService, mCameraAccessBlockedCallback); @@ -535,7 +536,7 @@ public class VirtualDeviceManagerServiceTest { .build(); mDeviceImpl.close(); mDeviceImpl = createVirtualDevice(VIRTUAL_DEVICE_ID_1, DEVICE_OWNER_UID_1, params); - addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); + addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED); GenericWindowPolicyController gwpc = mDeviceImpl.getDisplayWindowPolicyControllerForTest(DISPLAY_ID_1); @@ -543,6 +544,21 @@ public class VirtualDeviceManagerServiceTest { } @Test + public void getDevicePolicy_customRecentsPolicy_untrustedDisplaygwpcShowsRecentsOnHostDevice() { + VirtualDeviceParams params = new VirtualDeviceParams + .Builder() + .setDevicePolicy(POLICY_TYPE_RECENTS, DEVICE_POLICY_CUSTOM) + .build(); + mDeviceImpl.close(); + mDeviceImpl = createVirtualDevice(VIRTUAL_DEVICE_ID_1, DEVICE_OWNER_UID_1, params); + addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); + + GenericWindowPolicyController gwpc = + mDeviceImpl.getDisplayWindowPolicyControllerForTest(DISPLAY_ID_1); + assertThat(gwpc.canShowTasksInHostDeviceRecents()).isTrue(); + } + + @Test public void getDeviceOwnerUid_oneDevice_returnsCorrectId() { int ownerUid = mLocalService.getDeviceOwnerUid(mDeviceImpl.getDeviceId()); assertThat(ownerUid).isEqualTo(mDeviceImpl.getOwnerUid()); @@ -692,7 +708,7 @@ public class VirtualDeviceManagerServiceTest { @Test public void getPreferredLocaleListForApp_keyboardAttached_returnLocaleHints() { - addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); + addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED); mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER); mVdms.notifyRunningAppsChanged(mDeviceImpl.getDeviceId(), Sets.newArraySet(UID_1)); @@ -732,8 +748,8 @@ public class VirtualDeviceManagerServiceTest { .setLanguageTag("fr-FR") .build(); - addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); - addVirtualDisplay(secondDevice, DISPLAY_ID_2); + addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED); + addVirtualDisplay(secondDevice, DISPLAY_ID_2, Display.FLAG_TRUSTED); mDeviceImpl.createVirtualKeyboard(firstKeyboardConfig, BINDER); secondDevice.createVirtualKeyboard(secondKeyboardConfig, secondBinder); @@ -910,11 +926,24 @@ public class VirtualDeviceManagerServiceTest { } @Test - public void onVirtualDisplayCreatedLocked_wakeLockIsAcquired() throws RemoteException { + public void onVirtualDisplayCreatedLocked_notTrustedDisplay_noWakeLockIsAcquired() + throws RemoteException { verify(mIPowerManagerMock, never()).acquireWakeLock(any(Binder.class), anyInt(), nullable(String.class), nullable(String.class), nullable(WorkSource.class), nullable(String.class), anyInt(), eq(null)); addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); + TestableLooper.get(this).processAllMessages(); + verify(mIPowerManagerMock, never()).acquireWakeLock(any(Binder.class), anyInt(), + nullable(String.class), nullable(String.class), nullable(WorkSource.class), + nullable(String.class), anyInt(), eq(null)); + } + + @Test + public void onVirtualDisplayCreatedLocked_wakeLockIsAcquired() throws RemoteException { + verify(mIPowerManagerMock, never()).acquireWakeLock(any(Binder.class), anyInt(), + nullable(String.class), nullable(String.class), nullable(WorkSource.class), + nullable(String.class), anyInt(), eq(null)); + addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED); verify(mIPowerManagerMock).acquireWakeLock(any(Binder.class), anyInt(), nullable(String.class), nullable(String.class), nullable(WorkSource.class), nullable(String.class), eq(DISPLAY_ID_1), eq(null)); @@ -923,7 +952,7 @@ public class VirtualDeviceManagerServiceTest { @Test public void onVirtualDisplayCreatedLocked_duplicateCalls_onlyOneWakeLockIsAcquired() throws RemoteException { - addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); + addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED); assertThrows(IllegalStateException.class, () -> addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1)); TestableLooper.get(this).processAllMessages(); @@ -934,7 +963,7 @@ public class VirtualDeviceManagerServiceTest { @Test public void onVirtualDisplayRemovedLocked_wakeLockIsReleased() throws RemoteException { - addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); + addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED); ArgumentCaptor<IBinder> wakeLockCaptor = ArgumentCaptor.forClass(IBinder.class); TestableLooper.get(this).processAllMessages(); verify(mIPowerManagerMock).acquireWakeLock(wakeLockCaptor.capture(), @@ -949,7 +978,7 @@ public class VirtualDeviceManagerServiceTest { @Test public void addVirtualDisplay_displayNotReleased_wakeLockIsReleased() throws RemoteException { - addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); + addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED); ArgumentCaptor<IBinder> wakeLockCaptor = ArgumentCaptor.forClass(IBinder.class); TestableLooper.get(this).processAllMessages(); verify(mIPowerManagerMock).acquireWakeLock(wakeLockCaptor.capture(), @@ -970,24 +999,52 @@ public class VirtualDeviceManagerServiceTest { } @Test + public void createVirtualDpad_untrustedDisplay_failsSecurityException() { + addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); + assertThrows(SecurityException.class, + () -> mDeviceImpl.createVirtualDpad(DPAD_CONFIG, BINDER)); + } + + @Test public void createVirtualKeyboard_noDisplay_failsSecurityException() { assertThrows(SecurityException.class, () -> mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER)); } @Test + public void createVirtualKeyboard_untrustedDisplay_failsSecurityException() { + addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); + assertThrows(SecurityException.class, + () -> mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER)); + } + + @Test public void createVirtualMouse_noDisplay_failsSecurityException() { assertThrows(SecurityException.class, () -> mDeviceImpl.createVirtualMouse(MOUSE_CONFIG, BINDER)); } @Test + public void createVirtualMouse_untrustedDisplay_failsSecurityException() { + addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); + assertThrows(SecurityException.class, + () -> mDeviceImpl.createVirtualMouse(MOUSE_CONFIG, BINDER)); + } + + @Test public void createVirtualTouchscreen_noDisplay_failsSecurityException() { assertThrows(SecurityException.class, () -> mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER)); } @Test + public void createVirtualTouchscreen_untrustedDisplay_failsSecurityException() { + addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); + assertThrows(SecurityException.class, + () -> mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER)); + } + + @Test public void createVirtualTouchscreen_zeroDisplayDimension_failsIllegalArgumentException() { assertThrows(IllegalArgumentException.class, () -> new VirtualTouchscreenConfig.Builder( @@ -1003,7 +1060,7 @@ public class VirtualDeviceManagerServiceTest { @Test public void createVirtualTouchscreen_positiveDisplayDimension_successful() { - addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); + addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED); VirtualTouchscreenConfig positiveConfig = new VirtualTouchscreenConfig.Builder( /* touchscrenWidth= */ 600, /* touchscreenHeight= */ 800) @@ -1026,6 +1083,14 @@ public class VirtualDeviceManagerServiceTest { } @Test + public void createVirtualNavigationTouchpad_untrustedDisplay_failsSecurityException() { + addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); + assertThrows(SecurityException.class, + () -> mDeviceImpl.createVirtualNavigationTouchpad(NAVIGATION_TOUCHPAD_CONFIG, + BINDER)); + } + + @Test public void createVirtualNavigationTouchpad_zeroDisplayDimension_failsWithException() { assertThrows(IllegalArgumentException.class, () -> new VirtualNavigationTouchpadConfig.Builder( @@ -1041,7 +1106,7 @@ public class VirtualDeviceManagerServiceTest { @Test public void createVirtualNavigationTouchpad_positiveDisplayDimension_successful() { - addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); + addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED); VirtualNavigationTouchpadConfig positiveConfig = new VirtualNavigationTouchpadConfig.Builder( /* touchpadHeight= */ 50, /* touchpadWidth= */ 50) @@ -1130,7 +1195,7 @@ public class VirtualDeviceManagerServiceTest { @Test public void createVirtualDpad_hasDisplay_obtainFileDescriptor() { - addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); + addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED); mDeviceImpl.createVirtualDpad(DPAD_CONFIG, BINDER); assertWithMessage("Virtual dpad should register fd when the display matches").that( mInputController.getInputDeviceDescriptors()).isNotEmpty(); @@ -1140,7 +1205,7 @@ public class VirtualDeviceManagerServiceTest { @Test public void createVirtualKeyboard_hasDisplay_obtainFileDescriptor() { - addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); + addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED); mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER); assertWithMessage("Virtual keyboard should register fd when the display matches").that( mInputController.getInputDeviceDescriptors()).isNotEmpty(); @@ -1150,7 +1215,7 @@ public class VirtualDeviceManagerServiceTest { @Test public void createVirtualKeyboard_keyboardCreated_localeUpdated() { - addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); + addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED); mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER); assertWithMessage("Virtual keyboard should register fd when the display matches") .that(mInputController.getInputDeviceDescriptors()) @@ -1171,7 +1236,7 @@ public class VirtualDeviceManagerServiceTest { .setAssociatedDisplayId(DISPLAY_ID_1) .build(); - addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); + addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED); mDeviceImpl.createVirtualKeyboard(configWithoutExplicitLayoutInfo, BINDER); assertWithMessage("Virtual keyboard should register fd when the display matches") .that(mInputController.getInputDeviceDescriptors()) @@ -1192,7 +1257,7 @@ public class VirtualDeviceManagerServiceTest { @Test public void createVirtualMouse_hasDisplay_obtainFileDescriptor() { - addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); + addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED); mDeviceImpl.createVirtualMouse(MOUSE_CONFIG, BINDER); assertWithMessage("Virtual mouse should register fd when the display matches").that( mInputController.getInputDeviceDescriptors()).isNotEmpty(); @@ -1202,7 +1267,7 @@ public class VirtualDeviceManagerServiceTest { @Test public void createVirtualTouchscreen_hasDisplay_obtainFileDescriptor() { - addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); + addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED); mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER); assertWithMessage("Virtual touchscreen should register fd when the display matches").that( mInputController.getInputDeviceDescriptors()).isNotEmpty(); @@ -1212,7 +1277,7 @@ public class VirtualDeviceManagerServiceTest { @Test public void createVirtualNavigationTouchpad_hasDisplay_obtainFileDescriptor() { - addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); + addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED); mDeviceImpl.createVirtualNavigationTouchpad(NAVIGATION_TOUCHPAD_CONFIG, BINDER); assertWithMessage("Virtual navigation touchpad should register fd when the display matches") .that( @@ -1472,9 +1537,9 @@ public class VirtualDeviceManagerServiceTest { @Test public void setShowPointerIcon_setsValueForAllDisplays() { - addVirtualDisplay(mDeviceImpl, 1); - addVirtualDisplay(mDeviceImpl, 2); - addVirtualDisplay(mDeviceImpl, 3); + addVirtualDisplay(mDeviceImpl, 1, Display.FLAG_TRUSTED); + addVirtualDisplay(mDeviceImpl, 2, Display.FLAG_TRUSTED); + addVirtualDisplay(mDeviceImpl, 3, Display.FLAG_TRUSTED); VirtualMouseConfig config1 = new VirtualMouseConfig.Builder() .setAssociatedDisplayId(1) .setInputDeviceName(DEVICE_NAME_1) @@ -1507,6 +1572,14 @@ public class VirtualDeviceManagerServiceTest { } @Test + public void setShowPointerIcon_untrustedDisplay_pointerIconIsAlwaysShown() { + addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); + clearInvocations(mInputManagerInternalMock); + mDeviceImpl.setShowPointerIcon(false); + verify(mInputManagerInternalMock, times(0)).setPointerIconVisible(eq(false), anyInt()); + } + + @Test public void openNonBlockedAppOnVirtualDisplay_doesNotStartBlockedAlertActivity() { addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); GenericWindowPolicyController gwpc = mDeviceImpl.getDisplayWindowPolicyControllerForTest( @@ -1968,15 +2041,20 @@ public class VirtualDeviceManagerServiceTest { } private void addVirtualDisplay(VirtualDeviceImpl virtualDevice, int displayId) { + addVirtualDisplay(virtualDevice, displayId, /* flags= */ 0); + } + + private void addVirtualDisplay(VirtualDeviceImpl virtualDevice, int displayId, int flags) { when(mDisplayManagerInternalMock.createVirtualDisplay(any(), eq(mVirtualDisplayCallback), eq(virtualDevice), any(), any())).thenReturn(displayId); - virtualDevice.createVirtualDisplay(VIRTUAL_DISPLAY_CONFIG, mVirtualDisplayCallback); final String uniqueId = UNIQUE_ID + displayId; doAnswer(inv -> { final DisplayInfo displayInfo = new DisplayInfo(); displayInfo.uniqueId = uniqueId; + displayInfo.flags = flags; return displayInfo; }).when(mDisplayManagerInternalMock).getDisplayInfo(eq(displayId)); + virtualDevice.createVirtualDisplay(VIRTUAL_DISPLAY_CONFIG, mVirtualDisplayCallback); mInputManagerMockHelper.addDisplayIdMapping(uniqueId, displayId); } diff --git a/services/tests/servicestests/src/com/android/server/compat/PlatformCompatTest.java b/services/tests/servicestests/src/com/android/server/compat/PlatformCompatTest.java index 9df7a3612a92..1d075401832d 100644 --- a/services/tests/servicestests/src/com/android/server/compat/PlatformCompatTest.java +++ b/services/tests/servicestests/src/com/android/server/compat/PlatformCompatTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyLong; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; @@ -33,6 +34,11 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.os.Build; +import android.os.Process; + +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import androidx.test.runner.AndroidJUnit4; @@ -43,6 +49,7 @@ import com.android.internal.compat.CompatibilityChangeInfo; import com.android.server.LocalServices; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -55,6 +62,8 @@ import java.util.Set; public class PlatformCompatTest { private static final String PACKAGE_NAME = "my.package"; + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Mock private Context mContext; @Mock @@ -441,4 +450,79 @@ public class PlatformCompatTest { assertThat(mPlatformCompat.isChangeEnabled(3L, systemAppInfo)).isTrue(); verify(mChangeReporter).reportChange(123, 3L, ChangeReporter.STATE_ENABLED, true, false); } + + @DisableFlags(Flags.FLAG_SYSTEM_UID_TARGET_SYSTEM_SDK) + @Test + public void testSharedSystemUidFlagOff() throws Exception { + testSharedSystemUid(false); + } + + @EnableFlags(Flags.FLAG_SYSTEM_UID_TARGET_SYSTEM_SDK) + @Test + public void testSharedSystemUidFlagOn() throws Exception { + testSharedSystemUid(true); + } + + private void testSharedSystemUid(Boolean expectSystemUidTargetSystemSdk) throws Exception { + final String systemUidPackageNameTargetsR = "systemuid.package1"; + final String systemUidPackageNameTargetsQ = "systemuid.package2"; + final String nonSystemUidPackageNameTargetsR = "nonsystemuid.package1"; + final String nonSystemUidPackageNameTargetsQ = "nonsystemuid.package2"; + final int nonSystemUid = 123; + + mCompatConfig = + CompatConfigBuilder.create(mBuildClassifier, mContext) + .addEnableSinceSdkChangeWithId(Build.VERSION_CODES.R, 1L) + .build(); + mCompatConfig.forceNonDebuggableFinalForTest(true); + mPlatformCompat = + new PlatformCompat(mContext, mCompatConfig, mBuildClassifier, mChangeReporter); + + ApplicationInfo systemUidAppInfo1 = ApplicationInfoBuilder.create() + .withPackageName(systemUidPackageNameTargetsR) + .withUid(Process.SYSTEM_UID) + .withTargetSdk(Build.VERSION_CODES.R) + .build(); + when(mPackageManagerInternal.getApplicationInfo( + eq(systemUidPackageNameTargetsR), anyLong(), anyInt(), anyInt())) + .thenReturn(systemUidAppInfo1); + + ApplicationInfo systemUidAppInfo2 = ApplicationInfoBuilder.create() + .withPackageName(systemUidPackageNameTargetsQ) + .withUid(Process.SYSTEM_UID) + .withTargetSdk(Build.VERSION_CODES.Q) + .build(); + when(mPackageManagerInternal.getApplicationInfo( + eq(systemUidPackageNameTargetsQ), anyLong(), anyInt(), anyInt())) + .thenReturn(systemUidAppInfo2); + + ApplicationInfo nonSystemUidAppInfo1 = ApplicationInfoBuilder.create() + .withPackageName(nonSystemUidPackageNameTargetsR) + .withUid(nonSystemUid) + .withTargetSdk(Build.VERSION_CODES.R) + .build(); + when(mPackageManagerInternal.getApplicationInfo( + eq(nonSystemUidPackageNameTargetsR), anyLong(), anyInt(), anyInt())) + .thenReturn(nonSystemUidAppInfo1); + + ApplicationInfo nonSystemUidAppInfo2 = ApplicationInfoBuilder.create() + .withPackageName(nonSystemUidPackageNameTargetsQ) + .withUid(nonSystemUid) + .withTargetSdk(Build.VERSION_CODES.Q) + .build(); + when(mPackageManagerInternal.getApplicationInfo( + eq(nonSystemUidPackageNameTargetsQ), anyLong(), anyInt(), anyInt())) + .thenReturn(nonSystemUidAppInfo2); + + when(mPackageManager.getPackagesForUid(eq(Process.SYSTEM_UID))) + .thenReturn(new String[] {systemUidPackageNameTargetsR, systemUidPackageNameTargetsQ}); + when(mPackageManager.getPackagesForUid(eq(nonSystemUid))) + .thenReturn(new String[] { + nonSystemUidPackageNameTargetsR, nonSystemUidPackageNameTargetsQ + }); + + assertThat(mPlatformCompat.isChangeEnabledByUid(1L, Process.SYSTEM_UID)) + .isEqualTo(expectSystemUidTargetSystemSdk); + assertThat(mPlatformCompat.isChangeEnabledByUid(1L, nonSystemUid)).isFalse(); + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java b/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java index e4436966ae03..c51261f40ed5 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java @@ -52,6 +52,7 @@ import android.graphics.Rect; import android.os.IBinder; import android.platform.test.annotations.Presubmit; import android.view.ContentRecordingSession; +import android.view.Display; import android.view.DisplayInfo; import android.view.Gravity; import android.view.SurfaceControl; @@ -93,9 +94,11 @@ public class ContentRecorderTests extends WindowTestsBase { private boolean mHandleAnisotropicDisplayMirroring = false; @Before public void setUp() { + mDisplayInfo.type = Display.TYPE_VIRTUAL; MockitoAnnotations.initMocks(this); doReturn(INVALID_DISPLAY).when(mWm.mDisplayManagerInternal).getDisplayIdToMirror(anyInt()); + doReturn(false).when(mWm.mDisplayManagerInternal).isDisplayReadyForMirroring(anyInt()); // Skip unnecessary operations of relayout. spyOn(mWm.mWindowPlacerLocked); @@ -163,6 +166,25 @@ public class ContentRecorderTests extends WindowTestsBase { } @Test + public void testUpdateRecording_externalDisplayWithoutUserConfirmation() { + mDisplayInfo.type = Display.TYPE_EXTERNAL; + defaultInit(); + mContentRecorder.setContentRecordingSession(mDisplaySession); + mContentRecorder.updateRecording(); + assertThat(mContentRecorder.isCurrentlyRecording()).isFalse(); + } + + @Test + public void testUpdateRecording_externalDisplayWithUserConfirmation() { + doReturn(true).when(mWm.mDisplayManagerInternal).isDisplayReadyForMirroring(anyInt()); + mDisplayInfo.type = Display.TYPE_EXTERNAL; + defaultInit(); + mContentRecorder.setContentRecordingSession(mDisplaySession); + mContentRecorder.updateRecording(); + assertThat(mContentRecorder.isCurrentlyRecording()).isTrue(); + } + + @Test public void testUpdateRecording_display_invalidDisplayIdToMirror() { defaultInit(); ContentRecordingSession session = ContentRecordingSession.createDisplaySession( diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java index 5e8f347c0c6e..c8fc4822259e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java @@ -26,7 +26,6 @@ import static android.content.res.Configuration.ORIENTATION_UNDEFINED; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; import static org.junit.Assert.assertFalse; @@ -73,7 +72,6 @@ public class DisplayRotationImmersiveAppCompatPolicyTests extends WindowTestsBas when(mMockWindowState.getRequestedVisibleTypes()).thenReturn(0); when(mMockActivityRecord.findMainWindow()).thenReturn(mMockWindowState); - spy(mDisplayContent); doReturn(mMockActivityRecord).when(mDisplayContent).topRunningActivity(); when(mDisplayContent.getIgnoreOrientationRequest()).thenReturn(true); diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index 49ca6f34d2d9..44de65a009ff 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -1965,13 +1965,14 @@ public final class SatelliteManager { } /** - * Inform whether the device is aligned with the satellite for demo mode. + * Inform whether the device is aligned with the satellite in both real and demo mode. * - * Framework can send datagram to modem only when device is aligned with the satellite. - * This method helps framework to simulate the experience of sending datagram over satellite. + * In demo mode, framework will send datagram to modem only when device is aligned with + * the satellite. This method helps framework to simulate the experience of sending datagram + * over satellite. * - * @param isAligned {@true} Device is aligned with the satellite for demo mode - * {@false} Device is not aligned with the satellite for demo mode + * @param isAligned {code @true} Device is aligned with the satellite + * {code @false} Device is not aligned with the satellite * * @throws SecurityException if the caller doesn't have required permission. * @throws IllegalStateException if the Telephony process is not currently available. diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index 61f01461232f..231c8f551389 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -2977,10 +2977,10 @@ interface ITelephony { void requestTimeForNextSatelliteVisibility(in ResultReceiver receiver); /** - * Inform whether the device is aligned with the satellite within in margin for demo mode. + * Inform whether the device is aligned with the satellite in both real and demo mode. * - * @param isAligned {@true} Device is aligned with the satellite for demo mode - * {@false} Device is not aligned with the satellite for demo mode + * @param isAligned {@true} Device is aligned with the satellite. + * {@false} Device is not aligned with the satellite. */ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(" + "android.Manifest.permission.SATELLITE_COMMUNICATION)") |