summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AconfigFlags.bp17
-rw-r--r--Android.bp1
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/JobInfo.java6
-rw-r--r--boot/boot-image-profile-extra.txt7
-rw-r--r--boot/preloaded-classes1
-rw-r--r--cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java2
-rw-r--r--cmds/svc/src/com/android/commands/svc/UsbCommand.java5
-rw-r--r--config/preloaded-classes1
-rw-r--r--core/api/current.txt2
-rw-r--r--core/api/module-lib-current.txt1
-rw-r--r--core/api/system-current.txt1
-rw-r--r--core/api/test-current.txt3
-rw-r--r--core/java/android/animation/AnimationHandler.java6
-rw-r--r--core/java/android/animation/Animator.java33
-rw-r--r--core/java/android/animation/AnimatorSet.java7
-rw-r--r--core/java/android/animation/ValueAnimator.java1
-rw-r--r--core/java/android/app/ActivityManager.java13
-rw-r--r--core/java/android/app/ActivityManagerInternal.java8
-rw-r--r--core/java/android/app/AppCompatTaskInfo.java23
-rw-r--r--core/java/android/app/AppOpsManager.java11
-rw-r--r--core/java/android/app/IActivityManager.aidl4
-rw-r--r--core/java/android/app/Notification.java48
-rw-r--r--core/java/android/app/PropertyInvalidatedCache.java37
-rw-r--r--core/java/android/app/TaskInfo.java6
-rw-r--r--core/java/android/app/admin/DevicePolicyManager.java1
-rw-r--r--core/java/android/app/admin/flags/flags.aconfig10
-rw-r--r--core/java/android/app/backup/BackupManagerMonitor.java3
-rw-r--r--core/java/android/app/notification.aconfig10
-rw-r--r--core/java/android/content/Context.java11
-rw-r--r--core/java/android/content/pm/RegisteredServicesCache.java33
-rw-r--r--core/java/android/content/pm/UserInfo.java5
-rw-r--r--core/java/android/content/pm/multiuser.aconfig9
-rw-r--r--core/java/android/database/sqlite/SQLiteConnection.java5
-rw-r--r--core/java/android/hardware/camera2/CameraManager.java8
-rw-r--r--core/java/android/hardware/input/IKeyGestureHandler.aidl11
-rw-r--r--core/java/android/hardware/input/InputManager.java7
-rw-r--r--core/java/android/hardware/input/InputManagerGlobal.java17
-rw-r--r--core/java/android/hardware/input/KeyGestureEvent.java6
-rw-r--r--core/java/android/os/CombinedMessageQueue/MessageQueue.java13
-rw-r--r--core/java/android/os/IpcDataCache.java5
-rw-r--r--core/java/android/os/UserManager.java29
-rw-r--r--core/java/android/os/flags.aconfig8
-rw-r--r--core/java/android/os/health/SystemHealthManager.java10
-rw-r--r--core/java/android/os/vibrator/flags.aconfig11
-rw-r--r--core/java/android/permission/flags.aconfig22
-rw-r--r--core/java/android/provider/Settings.java127
-rw-r--r--core/java/android/service/chooser/flags.aconfig10
-rw-r--r--core/java/android/service/quickaccesswallet/QuickAccessWalletServiceInfo.java4
-rw-r--r--core/java/android/service/voice/HotwordDetectionService.java4
-rw-r--r--core/java/android/service/voice/ISandboxedDetectionService.aidl6
-rw-r--r--core/java/android/service/voice/VisualQueryDetectionService.java4
-rw-r--r--core/java/android/text/Layout.java9
-rw-r--r--core/java/android/util/MapCollections.java37
-rw-r--r--core/java/android/view/Choreographer.java6
-rw-r--r--core/java/android/view/LayoutInflater.java5
-rw-r--r--core/java/android/view/ListenerWrapper.java56
-rw-r--r--core/java/android/view/WindowManagerGlobal.java33
-rw-r--r--core/java/android/view/flags/view_flags.aconfig8
-rw-r--r--core/java/android/view/inspector/WindowInspector.java24
-rw-r--r--core/java/android/view/translation/ListenerGroup.java87
-rw-r--r--core/java/android/window/DesktopExperienceFlags.java17
-rw-r--r--core/java/android/window/DesktopModeFlags.java6
-rw-r--r--core/java/android/window/WindowContainerTransaction.java32
-rw-r--r--core/java/android/window/flags/lse_desktop_experience.aconfig29
-rw-r--r--core/java/android/window/flags/windowing_frontend.aconfig19
-rw-r--r--core/java/com/android/internal/app/ChooserActivity.java76
-rw-r--r--core/java/com/android/internal/app/ChooserListAdapter.java20
-rw-r--r--core/java/com/android/internal/app/ResolverListAdapter.java6
-rw-r--r--core/java/com/android/internal/os/BatteryStatsHistory.java190
-rw-r--r--core/java/com/android/internal/os/BatteryStatsHistoryIterator.java74
-rw-r--r--core/java/com/android/internal/policy/DecorView.java130
-rw-r--r--core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java20
-rw-r--r--core/java/com/android/internal/widget/ActionBarContextView.java12
-rw-r--r--core/java/com/android/internal/widget/ActionBarOverlayLayout.java85
-rw-r--r--core/java/com/android/internal/widget/NotificationExpandButton.java10
-rw-r--r--core/jni/android_database_SQLiteConnection.cpp8
-rw-r--r--core/jni/com_android_internal_content_FileSystemUtils.cpp21
-rw-r--r--core/jni/com_android_internal_content_FileSystemUtils.h14
-rw-r--r--core/jni/com_android_internal_content_NativeLibraryHelper.cpp12
-rw-r--r--core/proto/android/providers/settings/secure.proto3
-rw-r--r--core/res/AndroidManifest.xml3
-rw-r--r--core/res/res/drawable/accessibility_autoclick_scroll_down.xml25
-rw-r--r--core/res/res/drawable/accessibility_autoclick_scroll_exit.xml25
-rw-r--r--core/res/res/drawable/accessibility_autoclick_scroll_left.xml25
-rw-r--r--core/res/res/drawable/accessibility_autoclick_scroll_right.xml25
-rw-r--r--core/res/res/drawable/accessibility_autoclick_scroll_up.xml25
-rw-r--r--core/res/res/layout/accessibility_autoclick_scroll_panel.xml92
-rw-r--r--core/res/res/layout/notification_2025_expand_button.xml1
-rw-r--r--core/res/res/layout/notification_2025_right_icon.xml31
-rw-r--r--core/res/res/layout/notification_2025_template_collapsed_base.xml24
-rw-r--r--core/res/res/layout/notification_2025_template_collapsed_call.xml15
-rw-r--r--core/res/res/layout/notification_2025_template_collapsed_conversation.xml11
-rw-r--r--core/res/res/layout/notification_2025_template_collapsed_media.xml24
-rw-r--r--core/res/res/layout/notification_2025_template_collapsed_messaging.xml11
-rw-r--r--core/res/res/layout/notification_2025_template_expanded_base.xml2
-rw-r--r--core/res/res/layout/notification_2025_template_expanded_big_picture.xml2
-rw-r--r--core/res/res/layout/notification_2025_template_expanded_big_text.xml2
-rw-r--r--core/res/res/layout/notification_2025_template_expanded_call.xml2
-rw-r--r--core/res/res/layout/notification_2025_template_expanded_conversation.xml2
-rw-r--r--core/res/res/layout/notification_2025_template_expanded_inbox.xml2
-rw-r--r--core/res/res/layout/notification_2025_template_expanded_media.xml2
-rw-r--r--core/res/res/layout/notification_2025_template_expanded_messaging.xml2
-rw-r--r--core/res/res/layout/notification_2025_template_expanded_progress.xml2
-rw-r--r--core/res/res/layout/notification_2025_text.xml8
-rw-r--r--core/res/res/layout/notification_template_notification_progress_bar.xml1
-rw-r--r--core/res/res/values/config.xml19
-rw-r--r--core/res/res/values/dimens.xml28
-rw-r--r--core/res/res/values/strings.xml14
-rw-r--r--core/res/res/values/styles.xml13
-rw-r--r--core/res/res/values/symbols.xml36
-rw-r--r--core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java38
-rw-r--r--core/tests/coretests/src/android/animation/ValueAnimatorTests.java33
-rw-r--r--core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java6
-rw-r--r--core/tests/coretests/src/android/text/LayoutTest.java79
-rw-r--r--core/tests/coretests/src/android/util/ArrayMapTest.java51
-rw-r--r--core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java12
-rw-r--r--core/tests/coretests/src/com/android/internal/app/ChooserActivityWorkProfileTest.java3
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java21
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java103
-rw-r--r--libs/WindowManager/Shell/aconfig/multitasking.aconfig10
-rw-r--r--libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryScreenshotTest.kt1
-rw-r--r--libs/WindowManager/Shell/multivalentTests/Android.bp2
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleExpandedViewTest.kt73
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt6
-rw-r--r--libs/WindowManager/Shell/res/values/strings.xml8
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellSharedConstants.java7
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TypefaceUtils.kt86
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DeviceConfig.kt3
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt6
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetView.kt14
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/multiinstance/ManageWindowsViewContainer.kt10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java23
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/ComponentUtils.kt3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/HomeIntentProvider.kt65
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java51
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java37
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIHandler.kt5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIRequest.kt34
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIRequests.kt33
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/crashhandling/OWNERS2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/crashhandling/ShellCrashHandler.kt75
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java74
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt28
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt36
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt21
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt14
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandler.kt81
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt61
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt39
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt305
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt17
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt156
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt22
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt84
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt32
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializer.kt16
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt174
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java45
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java23
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java23
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt29
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt40
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt16
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt6
-rw-r--r--libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/UnminimizeAppFromTaskbar.kt16
-rw-r--r--libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml2
-rw-r--r--libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml2
-rw-r--r--libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java6
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsStateTest.java27
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java17
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/crashhandling/ShellCrashHandlerTest.kt119
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt72
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt258
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt32
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandlerTest.kt85
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt19
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt11
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt915
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt87
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt19
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt6
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt143
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt414
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt123
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java18
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java26
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTaskListenerTest.java35
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt39
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt28
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java41
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java46
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt17
-rw-r--r--libs/androidfw/ResourceTypes.cpp94
-rw-r--r--libs/androidfw/include/androidfw/ResourceTypes.h15
-rw-r--r--libs/hostgraphics/HostBufferQueue.cpp1
-rw-r--r--libs/hwui/jni/Graphics.cpp13
-rw-r--r--location/java/android/location/flags/location.aconfig7
-rw-r--r--media/java/Android.bp1
-rw-r--r--media/java/android/media/AudioManager.java21
-rw-r--r--media/java/android/media/IMediaRouter2.aidl3
-rw-r--r--media/java/android/media/IMediaRouter2Manager.aidl3
-rw-r--r--media/java/android/media/IMediaRouterService.aidl9
-rw-r--r--media/java/android/media/MediaCodecInfo.java145
-rw-r--r--media/java/android/media/MediaRouter2.java226
-rw-r--r--media/java/android/media/MediaRouter2Manager.java8
-rw-r--r--media/java/android/media/SuggestedDeviceInfo.aidl19
-rw-r--r--media/java/android/media/SuggestedDeviceInfo.java235
-rw-r--r--media/java/android/media/flags/media_better_together.aconfig8
-rw-r--r--media/java/android/media/quality/Android.bp39
-rw-r--r--media/java/android/media/quality/MediaQualityContract.java2
-rw-r--r--media/java/android/media/quality/MediaQualityManager.java86
-rw-r--r--media/java/android/media/quality/SoundProfileHandle.java72
-rw-r--r--media/java/android/media/quality/aidl/android/media/quality/ActiveProcessingPicture.aidl (renamed from media/java/android/media/quality/ActiveProcessingPicture.aidl)0
-rw-r--r--media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightEvent.aidl (renamed from media/java/android/media/quality/AmbientBacklightEvent.aidl)0
-rw-r--r--media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightMetadata.aidl (renamed from media/java/android/media/quality/AmbientBacklightMetadata.aidl)0
-rw-r--r--media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightSettings.aidl (renamed from media/java/android/media/quality/AmbientBacklightSettings.aidl)0
-rw-r--r--media/java/android/media/quality/aidl/android/media/quality/IAmbientBacklightCallback.aidl (renamed from media/java/android/media/quality/IAmbientBacklightCallback.aidl)0
-rw-r--r--media/java/android/media/quality/aidl/android/media/quality/IMediaQualityManager.aidl (renamed from media/java/android/media/quality/IMediaQualityManager.aidl)70
-rw-r--r--media/java/android/media/quality/aidl/android/media/quality/IPictureProfileCallback.aidl (renamed from media/java/android/media/quality/IPictureProfileCallback.aidl)0
-rw-r--r--media/java/android/media/quality/aidl/android/media/quality/ISoundProfileCallback.aidl (renamed from media/java/android/media/quality/ISoundProfileCallback.aidl)0
-rw-r--r--media/java/android/media/quality/aidl/android/media/quality/ParameterCapability.aidl (renamed from media/java/android/media/quality/ParameterCapability.aidl)0
-rw-r--r--media/java/android/media/quality/aidl/android/media/quality/PictureProfile.aidl (renamed from media/java/android/media/quality/PictureProfile.aidl)0
-rw-r--r--media/java/android/media/quality/aidl/android/media/quality/PictureProfileHandle.aidl (renamed from media/java/android/media/quality/PictureProfileHandle.aidl)0
-rw-r--r--media/java/android/media/quality/aidl/android/media/quality/SoundProfile.aidl (renamed from media/java/android/media/quality/SoundProfile.aidl)0
-rw-r--r--media/java/android/media/quality/aidl/android/media/quality/SoundProfileHandle.aidl (renamed from media/java/android/media/quality/SoundProfileHandle.aidl)5
-rw-r--r--media/java/android/media/tv/extension/scan/IHDPlusInfo.aidl2
-rw-r--r--media/java/android/media/tv/extension/scan/IScanListener.aidl2
-rw-r--r--media/java/android/media/tv/extension/scan/IScanSession.aidl2
-rw-r--r--media/java/android/media/tv/extension/servicedb/IServiceListEdit.aidl7
-rw-r--r--media/java/android/media/tv/extension/servicedb/IServiceListImportListener.aidl4
-rw-r--r--native/android/performance_hint.cpp2
-rw-r--r--packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java16
-rw-r--r--packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java4
-rw-r--r--packages/SettingsLib/RestrictedLockUtils/res/values/strings.xml4
-rw-r--r--packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java14
-rw-r--r--packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedMode.kt4
-rw-r--r--packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSystemInteger.kt60
-rw-r--r--packages/SettingsLib/SpaPrivileged/tests/robotests/Android.bp59
-rw-r--r--packages/SettingsLib/SpaPrivileged/tests/robotests/AndroidManifest.xml25
-rw-r--r--packages/SettingsLib/SpaPrivileged/tests/robotests/config/robolectric.properties16
-rw-r--r--packages/SettingsLib/SpaPrivileged/tests/robotests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSystemIntegerTest.kt136
-rw-r--r--packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedModeTest.kt12
-rw-r--r--packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPageTest.kt8
-rw-r--r--packages/SettingsLib/res/values/strings.xml2
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java13
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java15
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedPreferenceHelperTest.java11
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java21
-rw-r--r--packages/SettingsProvider/res/values/defaults.xml3
-rw-r--r--packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java5
-rw-r--r--packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java9
-rw-r--r--packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java4
-rw-r--r--packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java19
-rw-r--r--packages/SystemUI/aconfig/predictive_back.aconfig5
-rw-r--r--packages/SystemUI/aconfig/systemui.aconfig38
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt52
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/animation/GSFAxes.kt1
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt1
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt32
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt3
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt22
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt34
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt15
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt108
-rw-r--r--packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragFullyClose.json54
-rw-r--r--packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragHalfClose.json56
-rw-r--r--packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragOpen.json32
-rw-r--r--packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingClose.json54
-rw-r--r--packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingOpen.json40
-rw-r--r--packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_magneticDetachAndReattach.json70
-rw-r--r--packages/SystemUI/compose/scene/tests/goldens/verticalReveal_triggeredRevealCloseTransition.json54
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt16
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/test/subjects/DpOffsetSubject.kt4
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt27
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/CanvasUtil.kt2
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt10
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt9
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt3
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt4
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt10
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ViewUtils.kt5
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt29
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt147
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java119
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/ExpandHelperTest.java2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java11
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt20
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt8
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandlerTest.kt240
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt30
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt13
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java10
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyEventRepositoryTest.kt62
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractorTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt147
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModelTest.kt25
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/log/LogWtfHandlerRuleTest.kt155
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/media/NotificationMediaManagerTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationMediaManagerTest.kt)2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java43
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModelTest.kt29
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt62
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/IconProviderTest.kt74
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiStateTest.kt43
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractorTest.kt11
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt32
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java20
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicyTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt30
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt47
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java12
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationGroupingUtilTest.kt63
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java124
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt114
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt10
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt209
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationCloseButtonTest.kt160
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt27
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinatorTest.kt13
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt15
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt8
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt6
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java9
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt41
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosTest.kt1046
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosTest.kt1137
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelKairosTest.kt198
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairosTest.kt1077
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairosTest.kt385
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosTest.kt142
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/TopLevelWindowEffectsTest.kt113
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryTest.kt95
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/domain/interactor/SqueezeEffectInteractorTest.kt63
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/ClientTrackingWakeLockTest.kt9
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/WakeLockTest.java6
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt3
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt12
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt20
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VPoint.kt (renamed from packages/SystemUI/customization/src/com/android/systemui/shared/clocks/VPoint.kt)14
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VRect.kt188
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/WeatherData.kt70
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt12
-rw-r--r--packages/SystemUI/res/drawable/notification_2025_smart_reply_button_background.xml35
-rw-r--r--packages/SystemUI/res/layout/clipboard_overlay.xml3
-rw-r--r--packages/SystemUI/res/layout/media_output_dialog.xml27
-rw-r--r--packages/SystemUI/res/layout/notif_half_shelf.xml15
-rw-r--r--packages/SystemUI/res/layout/notif_half_shelf_row.xml15
-rw-r--r--packages/SystemUI/res/layout/notification_2025_smart_action_button.xml35
-rw-r--r--packages/SystemUI/res/layout/notification_2025_smart_reply_button.xml36
-rw-r--r--packages/SystemUI/res/raw/fingerprint_dialogue_unlocked_to_checkmark_success_lottie.json2
-rw-r--r--packages/SystemUI/res/values/dimens.xml6
-rw-r--r--packages/SystemUI/res/values/strings.xml4
-rw-r--r--packages/SystemUI/src/com/android/keyguard/ClockEventController.kt6
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java15
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java70
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java7
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java7
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/Dependency.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java30
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt17
-rw-r--r--packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt25
-rw-r--r--packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingView.kt55
-rw-r--r--packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandler.kt148
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricAuthInteractor.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt28
-rw-r--r--packages/SystemUI/src/com/android/systemui/display/data/repository/FakePerDisplayRepository.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyevent/data/repository/KeyEventRepository.kt20
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractor.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt54
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewSmartspaceViewBinder.kt32
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardTouchViewBinder.kt20
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt170
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java12
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/NotificationMediaManager.java (renamed from packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java)6
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java27
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt91
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java116
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java52
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java15
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt16
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputColorSchemeLegacy.kt126
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java13
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt21
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java174
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java55
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/domain/interactor/MediaInteractor.kt38
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaActionModel.kt27
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaOutputDeviceModel.kt21
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaSessionModel.kt77
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaActionState.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaCardActionButtonLayout.kt24
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaColorScheme.kt21
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/DismissibleHorizontalPager.kt159
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt492
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCardViewModel.kt19
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCarouselVisibility.kt31
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaNavigationViewModel.kt (renamed from packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSeekBarViewModel.kt)25
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaPlayPauseActionViewModel.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSecondaryActionViewModel.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaViewModel.kt253
-rw-r--r--packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt62
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/external/ui/dialog/TileRequestDialogComposeDelegate.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModel.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt255
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt18
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt21
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt34
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt16
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt39
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt41
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractor.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java29
-rw-r--r--packages/SystemUI/src/com/android/systemui/settings/brightness/ComposeDialogComposableProvider.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt29
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java20
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt29
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java17
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java91
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsReturnAnimations.kt31
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt166
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt171
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt25
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java11
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapterFactoryImpl.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java34
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinator.kt20
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java15
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifBundler.kt35
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManager.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java11
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java167
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManager.kt46
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java14
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt56
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt21
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairos.kt337
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapter.kt78
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairos.kt395
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosAdapter.kt172
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModelKairos.kt113
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt328
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt152
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairos.kt96
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt33
-rw-r--r--packages/SystemUI/src/com/android/systemui/topwindoweffects/TopLevelWindowEffects.kt102
-rw-r--r--packages/SystemUI/src/com/android/systemui/topwindoweffects/dagger/SqueezeEffectRepositoryModule.kt31
-rw-r--r--packages/SystemUI/src/com/android/systemui/topwindoweffects/dagger/TopLevelWindowEffectsModule.kt33
-rw-r--r--packages/SystemUI/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepository.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryImpl.kt62
-rw-r--r--packages/SystemUI/src/com/android/systemui/topwindoweffects/domain/interactor/SqueezeEffectInteractor.kt28
-rw-r--r--packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/EffectsWindowRoot.kt50
-rw-r--r--packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/SqueezeEffect.kt139
-rw-r--r--packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/viewmodel/SqueezeEffectViewModel.kt53
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt24
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java1
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt17
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt27
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java7
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java19
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt26
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/ringtone/RingtonePlayerTest.java72
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java21
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java37
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java11
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java29
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt3
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyevent/data/repository/FakeKeyEventRepository.kt7
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt6
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModelKosmos.kt1
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt1
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/util/KeyguardTransitionRepositorySpySubject.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt114
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/log/LogWtfHandlerRule.kt102
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt10
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModelKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandlerSubject.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/QSTileStateSubject.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/TileSubject.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/EntryAdapterFactoryKosmos.kt19
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt3
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManagerKosmos.kt23
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt7
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileDomainInteractorKairosKosmos.kt41
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosKosmos.kt22
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/FakeSqueezeEffectRepository.kt23
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryKosmos.kt21
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/truth/correspondence/FakeUiEvent.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/truth/correspondence/LogMaker.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/util/CarrierConfigTrackerKosmos.kt22
-rw-r--r--ravenwood/Android.bp18
-rw-r--r--ravenwood/Framework.bp12
-rw-r--r--ravenwood/TEST_MAPPING8
-rw-r--r--ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java22
-rwxr-xr-xravenwood/scripts/extract-last-soong-commands.py2
-rw-r--r--ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodJdkPatchTest.java63
-rw-r--r--ravenwood/texts/ravenizer-standard-options.txt13
-rw-r--r--ravenwood/texts/ravenwood-standard-annotations.txt37
-rw-r--r--ravenwood/texts/ravenwood-standard-options.txt38
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/Exceptions.kt11
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessor.kt75
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessorOptions.kt10
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt2
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/DelegatingFilter.kt27
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/ImplicitOutputFilter.kt38
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/InMemoryOutputFilter.kt85
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/OutputFilter.kt10
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt75
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyMethodReplaceFilter.kt60
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyRemapperFilter.kt44
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/utils/OptionUtils.kt79
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/BaseAdapter.kt52
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt2
-rw-r--r--ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt7
-rw-r--r--ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaOptions.kt6
-rw-r--r--ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaProcessor.kt4
-rw-r--r--ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/sourcemap/MapOptions.kt6
-rw-r--r--ravenwood/tools/ravenizer/Android.bp5
-rw-r--r--ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt110
-rw-r--r--ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt13
-rw-r--r--ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt14
-rw-r--r--ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt4
-rw-r--r--ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt6
-rw-r--r--services/accessibility/accessibility.aconfig10
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java9
-rw-r--r--services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java54
-rw-r--r--services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java105
-rw-r--r--services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java8
-rw-r--r--services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java23
-rw-r--r--services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java100
-rw-r--r--services/autofill/java/com/android/server/autofill/Session.java42
-rw-r--r--services/backup/java/com/android/server/backup/BackupAgentConnectionManager.java31
-rw-r--r--services/backup/java/com/android/server/backup/BackupRestoreTask.java31
-rw-r--r--services/backup/java/com/android/server/backup/UserBackupManagerService.java47
-rw-r--r--services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java7
-rw-r--r--services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java2
-rw-r--r--services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java44
-rw-r--r--services/backup/java/com/android/server/backup/internal/BackupHandler.java5
-rw-r--r--services/backup/java/com/android/server/backup/internal/LifecycleOperationStorage.java21
-rw-r--r--services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java10
-rw-r--r--services/backup/java/com/android/server/backup/restore/AdbRestoreFinishedLatch.java2
-rw-r--r--services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java5
-rw-r--r--services/backup/java/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtils.java2
-rw-r--r--services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java10
-rw-r--r--services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java35
-rw-r--r--services/core/Android.bp9
-rw-r--r--services/core/java/com/android/server/Watchdog.java1
-rw-r--r--services/core/java/com/android/server/adb/AdbDebuggingManager.java48
-rw-r--r--services/core/java/com/android/server/adb/AdbService.java38
-rw-r--r--services/core/java/com/android/server/am/ActivityManagerService.java18
-rw-r--r--services/core/java/com/android/server/am/BroadcastProcessQueue.java4
-rw-r--r--services/core/java/com/android/server/am/BroadcastQueueImpl.java18
-rw-r--r--services/core/java/com/android/server/am/ConnectionRecord.java7
-rw-r--r--services/core/java/com/android/server/am/OomAdjuster.java15
-rw-r--r--services/core/java/com/android/server/am/OomAdjusterModernImpl.java9
-rw-r--r--services/core/java/com/android/server/am/UserController.java56
-rw-r--r--services/core/java/com/android/server/am/UserSwitchingDialog.java27
-rw-r--r--services/core/java/com/android/server/appop/AttributedOp.java11
-rw-r--r--services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java8
-rw-r--r--services/core/java/com/android/server/appop/DiscreteOpsMigrationHelper.java11
-rw-r--r--services/core/java/com/android/server/appop/DiscreteOpsRegistry.java154
-rw-r--r--services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java18
-rw-r--r--services/core/java/com/android/server/appop/DiscreteOpsTestingShim.java8
-rw-r--r--services/core/java/com/android/server/appop/DiscreteOpsXmlRegistry.java11
-rw-r--r--services/core/java/com/android/server/appop/HistoricalRegistry.java9
-rw-r--r--services/core/java/com/android/server/audio/AudioService.java2
-rw-r--r--services/core/java/com/android/server/display/DisplayControl.java13
-rw-r--r--services/core/java/com/android/server/display/DisplayManagerService.java19
-rw-r--r--services/core/java/com/android/server/display/DisplayTopologyCoordinator.java16
-rw-r--r--services/core/java/com/android/server/display/VirtualDisplayAdapter.java34
-rw-r--r--services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java4
-rw-r--r--services/core/java/com/android/server/hdmi/HdmiControlService.java11
-rw-r--r--services/core/java/com/android/server/input/InputManagerService.java24
-rw-r--r--services/core/java/com/android/server/input/KeyGestureController.java109
-rw-r--r--services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java24
-rw-r--r--services/core/java/com/android/server/inputmethod/ImeProtoLogGroup.java6
-rw-r--r--services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java38
-rw-r--r--services/core/java/com/android/server/locksettings/LockSettingsService.java6
-rw-r--r--services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java250
-rw-r--r--services/core/java/com/android/server/media/MediaRouterService.java33
-rw-r--r--services/core/java/com/android/server/media/quality/MediaQualityService.java117
-rw-r--r--services/core/java/com/android/server/media/quality/MediaQualityUtils.java7
-rw-r--r--services/core/java/com/android/server/notification/NotificationManagerService.java2
-rw-r--r--services/core/java/com/android/server/om/OverlayManagerServiceImpl.java4
-rw-r--r--services/core/java/com/android/server/os/instrumentation/OWNERS1
-rw-r--r--services/core/java/com/android/server/pm/InstallRequest.java14
-rw-r--r--services/core/java/com/android/server/pm/UserManagerService.java12
-rw-r--r--services/core/java/com/android/server/pm/UserTypeFactory.java31
-rw-r--r--services/core/java/com/android/server/policy/PhoneWindowManager.java81
-rw-r--r--services/core/java/com/android/server/power/ScreenUndimDetector.java44
-rw-r--r--services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java42
-rw-r--r--services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java22
-rw-r--r--services/core/java/com/android/server/selinux/SelinuxAuditLogsService.java109
-rw-r--r--services/core/java/com/android/server/selinux/flags.aconfig9
-rw-r--r--services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java61
-rw-r--r--services/core/java/com/android/server/tv/TvInputManagerService.java124
-rw-r--r--services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java141
-rw-r--r--services/core/java/com/android/server/updates/CertPinInstallReceiver.java20
-rw-r--r--services/core/java/com/android/server/vr/EnabledComponentsObserver.java62
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperCropper.java81
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperDataParser.java8
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperDefaultDisplayInfo.java207
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java102
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperManagerService.java16
-rw-r--r--services/core/java/com/android/server/wm/ActivityMetricsLogger.java11
-rw-r--r--services/core/java/com/android/server/wm/ActivityRecord.java24
-rw-r--r--services/core/java/com/android/server/wm/ActivitySnapshotController.java29
-rw-r--r--services/core/java/com/android/server/wm/ActivityStarter.java5
-rw-r--r--services/core/java/com/android/server/wm/ActivityTaskSupervisor.java15
-rw-r--r--services/core/java/com/android/server/wm/AppCompatController.java8
-rw-r--r--services/core/java/com/android/server/wm/AppCompatDisplayCompatModePolicy.java39
-rw-r--r--services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java5
-rw-r--r--services/core/java/com/android/server/wm/AppCompatUtils.java3
-rw-r--r--services/core/java/com/android/server/wm/BackNavigationController.java40
-rw-r--r--services/core/java/com/android/server/wm/BackgroundActivityStartController.java11
-rw-r--r--services/core/java/com/android/server/wm/BaseAppSnapshotPersister.java84
-rw-r--r--services/core/java/com/android/server/wm/DeferredDisplayUpdater.java26
-rw-r--r--services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java54
-rw-r--r--services/core/java/com/android/server/wm/DisplayWindowListenerController.java6
-rw-r--r--services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java31
-rw-r--r--services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java4
-rw-r--r--services/core/java/com/android/server/wm/InputManagerCallback.java7
-rw-r--r--services/core/java/com/android/server/wm/RootWindowContainer.java26
-rw-r--r--services/core/java/com/android/server/wm/Task.java41
-rw-r--r--services/core/java/com/android/server/wm/TaskDisplayArea.java56
-rw-r--r--services/core/java/com/android/server/wm/TaskFragment.java187
-rw-r--r--services/core/java/com/android/server/wm/Transition.java3
-rw-r--r--services/core/java/com/android/server/wm/WallpaperController.java22
-rw-r--r--services/core/java/com/android/server/wm/WindowContainer.java7
-rw-r--r--services/core/java/com/android/server/wm/WindowManagerInternal.java7
-rw-r--r--services/core/java/com/android/server/wm/WindowManagerService.java45
-rw-r--r--services/core/java/com/android/server/wm/WindowOrganizerController.java34
-rw-r--r--services/core/jni/Android.bp4
-rw-r--r--services/core/jni/com_android_server_display_DisplayControl.cpp7
-rw-r--r--services/core/jni/com_android_server_vibrator_VibratorController.cpp28
-rw-r--r--services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java73
-rw-r--r--services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java6
-rw-r--r--services/java/com/android/server/SystemServer.java9
-rw-r--r--services/java/com/android/server/flags.aconfig8
-rw-r--r--services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java9
-rw-r--r--services/tests/DynamicInstrumentationManagerServiceTests/OWNERS1
-rw-r--r--services/tests/InputMethodSystemServerTests/Android.bp5
-rw-r--r--services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/util/IgnoreableExpect.kt2
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java3
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt14
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java45
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java30
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/backup/BackupAgentConnectionManagerTest.java10
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/power/ScreenUndimDetectorTest.java10
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java101
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperDefaultDisplayInfoTest.java340
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java72
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java161
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java92
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java32
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchStateTest.java77
-rw-r--r--services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpXmlPersistenceTest.java6
-rw-r--r--services/tests/servicestests/src/com/android/server/appop/DiscreteOpsMigrationAndRollbackTest.java4
-rw-r--r--services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java42
-rw-r--r--services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java10
-rw-r--r--services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java3
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java32
-rw-r--r--services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java42
-rw-r--r--services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java47
-rw-r--r--services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java47
-rw-r--r--services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java46
-rw-r--r--services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java5
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java14
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java2
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java5
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java5
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java13
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java9
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java873
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java37
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DisplayCompatTests.java71
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java2
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java5
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java6
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java12
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java7
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java22
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java28
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterLoaderTest.java32
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java27
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TaskTests.java3
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java1
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java15
-rw-r--r--services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java22
-rw-r--r--telecomm/java/android/telecom/Connection.java4
-rw-r--r--telephony/java/android/telephony/TelephonyManager.java11
-rw-r--r--tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml2
-rw-r--r--tests/FlickerTests/AppClose/AndroidTestTemplate.xml2
-rw-r--r--tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml2
-rw-r--r--tests/FlickerTests/FlickerService/AndroidTestTemplate.xml2
-rw-r--r--tests/FlickerTests/IME/AndroidTestTemplate.xml2
-rw-r--r--tests/FlickerTests/Notification/AndroidTestTemplate.xml2
-rw-r--r--tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml2
-rw-r--r--tests/FlickerTests/Rotation/AndroidTestTemplate.xml2
-rw-r--r--tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt4
-rw-r--r--tests/Input/src/com/android/server/input/BatteryControllerTests.kt4
-rw-r--r--tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt110
-rw-r--r--tests/Input/src/com/android/server/input/debug/TouchpadDebugViewControllerTests.java8
-rw-r--r--tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java10
-rw-r--r--tests/utils/testutils/java/android/os/test/TestLooper.java11
-rw-r--r--tools/aapt2/Resource.h2
-rw-r--r--tools/aapt2/ResourceTable.cpp9
-rw-r--r--tools/aapt2/ResourceTable.h5
-rw-r--r--tools/aapt2/cmd/Link.cpp12
-rw-r--r--tools/aapt2/format/binary/BinaryResourceParser.cpp2
-rw-r--r--tools/aapt2/format/binary/ResEntryWriter.cpp4
-rw-r--r--tools/aapt2/format/binary/ResEntryWriter.h2
-rw-r--r--tools/aapt2/format/binary/TableFlattener.cpp3
-rw-r--r--tools/aapt2/format/binary/TableFlattener_test.cpp19
-rw-r--r--tools/aapt2/link/FlaggedResources_test.cpp48
-rw-r--r--tools/aapt2/link/FlaggedXmlVersioner.cpp27
-rw-r--r--tools/processors/intdef_mappings/src/android/processor/IntDefProcessor.kt18
821 files changed, 23595 insertions, 8194 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 4e05cbc23dc3..2dd16de3e188 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -73,6 +73,7 @@ aconfig_declarations_group {
"android.service.dreams.flags-aconfig-java",
"android.service.notification.flags-aconfig-java",
"android.service.quickaccesswallet.flags-aconfig-java",
+ "android.service.selinux.flags-aconfig-java",
"android.service.voice.flags-aconfig-java",
"android.speech.flags-aconfig-java",
"android.systemserver.flags-aconfig-java",
@@ -1943,3 +1944,19 @@ java_aconfig_library {
aconfig_declarations: "android.service.quickaccesswallet.flags-aconfig",
defaults: ["framework-minus-apex-aconfig-java-defaults"],
}
+
+// SELinux log collector
+aconfig_declarations {
+ name: "android.service.selinux.flags-aconfig",
+ package: "com.android.server.selinux.flags",
+ container: "system",
+ srcs: [
+ "services/core/java/com/android/server/selinux/*.aconfig",
+ ],
+}
+
+java_aconfig_library {
+ name: "android.service.selinux.flags-aconfig-java",
+ aconfig_declarations: "android.service.selinux.flags-aconfig",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
diff --git a/Android.bp b/Android.bp
index 444725eb2c79..127556f8e075 100644
--- a/Android.bp
+++ b/Android.bp
@@ -415,6 +415,7 @@ java_defaults {
"mimemap",
"av-types-aidl-java",
"tv_tuner_resource_manager_aidl_interface-java",
+ "media_quality_aidl_interface-java",
"soundtrigger_middleware-aidl-java",
"modules-utils-binary-xml",
"modules-utils-build",
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
index cc2d104813e4..d48af2cccd59 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
@@ -810,7 +810,7 @@ public class JobInfo implements Parcelable {
/**
* <p class="caution"><strong>Note:</strong> Beginning with
- * {@link android.os.Build.VERSION_CODES#B}, this flag will be ignored and no longer
+ * {@link android.os.Build.VERSION_CODES#BAKLAVA}, this flag will be ignored and no longer
* function effectively, regardless of the calling app's target SDK version.
* Calling this method will always return {@code false}.
*
@@ -2137,9 +2137,9 @@ public class JobInfo implements Parcelable {
* Jobs marked as important-while-foreground are given {@link #PRIORITY_HIGH} by default.
*
* <p class="caution"><strong>Note:</strong> Beginning with
- * {@link android.os.Build.VERSION_CODES#B}, this flag will be ignored and no longer
+ * {@link android.os.Build.VERSION_CODES#BAKLAVA}, this flag will be ignored and no longer
* function effectively, regardless of the calling app's target SDK version.
- * {link #isImportantWhileForeground()} will always return {@code false}.
+ * {@link #isImportantWhileForeground()} will always return {@code false}.
* Apps should use {link #setExpedited(boolean)} with {@code true} to indicate
* that this job is important and needs to run as soon as possible.
*
diff --git a/boot/boot-image-profile-extra.txt b/boot/boot-image-profile-extra.txt
index ce99bfed1ce3..cc02c8ae36e6 100644
--- a/boot/boot-image-profile-extra.txt
+++ b/boot/boot-image-profile-extra.txt
@@ -70,3 +70,10 @@ HSPLandroid/os/PerfettoTrackEventExtra$FieldString;->*
HSPLandroid/os/PerfettoTrackEventExtra$FieldNested;->*
HSPLandroid/os/PerfettoTrackEventExtra$Pool;->*
HSPLandroid/os/PerfettoTrackEventExtra$RingBuffer;->*
+
+# While the SystemFeaturesMetadata static cache isn't heavyweight, ensure it's
+# pre-initialized in the boot image to avoid redundant per-process overhead.
+# TODO(b/326623529): Consider removing this after the feature has fully ramped
+# and is captured with the boot image profiling pipeline.
+HSPLcom/android/internal/pm/SystemFeaturesMetadata;->*
+Lcom/android/internal/pm/SystemFeaturesMetadata;
diff --git a/boot/preloaded-classes b/boot/preloaded-classes
index f87828ec8d9a..7f4b3244c164 100644
--- a/boot/preloaded-classes
+++ b/boot/preloaded-classes
@@ -11885,6 +11885,7 @@ com.android.internal.os.ZygoteServer$UsapPoolRefillAction
com.android.internal.os.ZygoteServer
com.android.internal.os.logging.MetricsLoggerWrapper
com.android.internal.pm.RoSystemFeatures
+com.android.internal.pm.SystemFeaturesMetadata
com.android.internal.pm.parsing.PackageParser2$Callback
com.android.internal.pm.parsing.PackageParserException
com.android.internal.pm.pkg.component.flags.FeatureFlags
diff --git a/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java b/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java
index 696bc82a9ffc..ae150aece432 100644
--- a/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java
+++ b/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java
@@ -1309,6 +1309,8 @@ public class Bmgr {
return "AGENT_FAILURE_DURING_RESTORE";
case BackupManagerMonitor.LOG_EVENT_ID_FAILED_TO_READ_DATA_FROM_TRANSPORT:
return "FAILED_TO_READ_DATA_FROM_TRANSPORT";
+ case BackupManagerMonitor.LOG_EVENT_ID_FULL_BACKUP_AGENT_PIPE_BROKEN:
+ return "LOG_EVENT_ID_FULL_BACKUP_AGENT_PIPE_BROKEN";
default:
return "UNKNOWN_ID";
}
diff --git a/cmds/svc/src/com/android/commands/svc/UsbCommand.java b/cmds/svc/src/com/android/commands/svc/UsbCommand.java
index 26e20f601c7a..6542d08d6384 100644
--- a/cmds/svc/src/com/android/commands/svc/UsbCommand.java
+++ b/cmds/svc/src/com/android/commands/svc/UsbCommand.java
@@ -89,6 +89,11 @@ public class UsbCommand extends Svc.Command {
IUsbManager usbMgr = IUsbManager.Stub.asInterface(ServiceManager.getService(
Context.USB_SERVICE));
+ if (usbMgr == null) {
+ System.err.println("Could not obtain USB service. Try again later.");
+ return;
+ }
+
Executor executor = context.getMainExecutor();
Consumer<Integer> consumer = new Consumer<Integer>(){
public void accept(Integer status){
diff --git a/config/preloaded-classes b/config/preloaded-classes
index 4147fd7c4ae6..707acb00b102 100644
--- a/config/preloaded-classes
+++ b/config/preloaded-classes
@@ -11921,6 +11921,7 @@ com.android.internal.os.ZygoteServer$UsapPoolRefillAction
com.android.internal.os.ZygoteServer
com.android.internal.os.logging.MetricsLoggerWrapper
com.android.internal.pm.RoSystemFeatures
+com.android.internal.pm.SystemFeaturesMetadata
com.android.internal.pm.parsing.PackageParser2$Callback
com.android.internal.pm.parsing.PackageParserException
com.android.internal.pm.pkg.component.flags.FeatureFlags
diff --git a/core/api/current.txt b/core/api/current.txt
index 7bc0fb220e1a..1630d80346ce 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -58287,7 +58287,9 @@ package android.view.inspector {
}
public final class WindowInspector {
+ method @FlaggedApi("android.view.flags.root_view_changed_listener") public static void addGlobalWindowViewsListener(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.util.List<android.view.View>>);
method @NonNull public static java.util.List<android.view.View> getGlobalWindowViews();
+ method @FlaggedApi("android.view.flags.root_view_changed_listener") public static void removeGlobalWindowViewsListener(@NonNull java.util.function.Consumer<java.util.List<android.view.View>>);
}
}
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index 526a213a6003..132c65cc26ee 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -410,6 +410,7 @@ package android.os {
method public void invalidateCache();
method public static void invalidateCache(@NonNull String, @NonNull String);
method @Nullable public Result query(@NonNull Query);
+ method @FlaggedApi("android.os.ipc_data_cache_test_apis") public static void setTestMode(boolean);
field public static final String MODULE_BLUETOOTH = "bluetooth";
}
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 984bc680c685..32b170a6286b 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -11725,6 +11725,7 @@ package android.os {
field public static final String USER_TYPE_FULL_GUEST = "android.os.usertype.full.GUEST";
field public static final String USER_TYPE_FULL_SECONDARY = "android.os.usertype.full.SECONDARY";
field public static final String USER_TYPE_FULL_SYSTEM = "android.os.usertype.full.SYSTEM";
+ field @FlaggedApi("android.multiuser.allow_supervising_profile") public static final String USER_TYPE_PROFILE_SUPERVISING = "android.os.usertype.profile.SUPERVISING";
field public static final String USER_TYPE_SYSTEM_HEADLESS = "android.os.usertype.system.HEADLESS";
}
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index d651010b641a..bb023f25094e 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1199,6 +1199,7 @@ package android.content.pm {
method public boolean isProfile();
method public boolean isQuietModeEnabled();
method public boolean isRestricted();
+ method @FlaggedApi("android.multiuser.allow_supervising_profile") public boolean isSupervisingProfile();
method public boolean supportsSwitchTo();
method @Deprecated public boolean supportsSwitchToByUser();
method public void writeToParcel(android.os.Parcel, int);
@@ -2459,7 +2460,7 @@ package android.os {
method public static void invalidateCache(@NonNull String, @NonNull String);
method public final boolean isDisabled();
method @Nullable public Result query(@NonNull Query);
- method public static void setTestMode(boolean);
+ method @FlaggedApi("android.os.ipc_data_cache_test_apis") public static void setTestMode(boolean);
field public static final String MODULE_BLUETOOTH = "bluetooth";
field public static final String MODULE_SYSTEM = "system_server";
field public static final String MODULE_TEST = "test";
diff --git a/core/java/android/animation/AnimationHandler.java b/core/java/android/animation/AnimationHandler.java
index d84a4c12a2cd..d5b2f980e1a6 100644
--- a/core/java/android/animation/AnimationHandler.java
+++ b/core/java/android/animation/AnimationHandler.java
@@ -384,6 +384,12 @@ public class AnimationHandler {
});
}
+ void removePendingEndAnimationCallback(Runnable notifyEndAnimation) {
+ if (mPendingEndAnimationListeners != null) {
+ mPendingEndAnimationListeners.remove(notifyEndAnimation);
+ }
+ }
+
private void doAnimationFrame(long frameTime) {
long currentTime = SystemClock.uptimeMillis();
final int size = mAnimationCallbacks.size();
diff --git a/core/java/android/animation/Animator.java b/core/java/android/animation/Animator.java
index 4bf87f91cb2f..e62cd556af9a 100644
--- a/core/java/android/animation/Animator.java
+++ b/core/java/android/animation/Animator.java
@@ -82,6 +82,12 @@ public abstract class Animator implements Cloneable {
static boolean sPostNotifyEndListenerEnabled;
/**
+ * If {@link #sPostNotifyEndListenerEnabled} is enabled, it will be set when the end callback
+ * is scheduled. It is cleared when it runs or finishes immediately, e.g. cancel.
+ */
+ private Runnable mPendingEndCallback;
+
+ /**
* A cache of the values in a list. Used so that when calling the list, we have a copy
* of it in case the list is modified while iterating. The array can be reused to avoid
* allocation on every notification.
@@ -660,10 +666,33 @@ public abstract class Animator implements Cloneable {
}
}
+ /**
+ * This is called when the animator needs to finish immediately. This is usually no-op unless
+ * {@link #sPostNotifyEndListenerEnabled} is enabled and a finish request calls around the last
+ * animation frame.
+ *
+ * @param notifyListeners Whether to invoke {@link AnimatorListener#onAnimationEnd}.
+ * @return {@code true} if the pending listeners are removed.
+ */
+ boolean consumePendingEndListeners(boolean notifyListeners) {
+ if (mPendingEndCallback == null) {
+ return false;
+ }
+ AnimationHandler.getInstance().removePendingEndAnimationCallback(mPendingEndCallback);
+ mPendingEndCallback = null;
+ if (notifyListeners) {
+ notifyEndListeners(false /* isReversing */);
+ }
+ return true;
+ }
+
void notifyEndListenersFromEndAnimation(boolean isReversing, boolean postNotifyEndListener) {
if (postNotifyEndListener) {
- AnimationHandler.getInstance().postEndAnimationCallback(
- () -> completeEndAnimation(isReversing, "postNotifyAnimEnd"));
+ mPendingEndCallback = () -> {
+ completeEndAnimation(isReversing, "postNotifyAnimEnd");
+ mPendingEndCallback = null;
+ };
+ AnimationHandler.getInstance().postEndAnimationCallback(mPendingEndCallback);
} else {
completeEndAnimation(isReversing, "notifyAnimEnd");
}
diff --git a/core/java/android/animation/AnimatorSet.java b/core/java/android/animation/AnimatorSet.java
index 78566d2fe98d..4a07de0410ae 100644
--- a/core/java/android/animation/AnimatorSet.java
+++ b/core/java/android/animation/AnimatorSet.java
@@ -423,6 +423,13 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim
notifyListeners(AnimatorCaller.ON_CANCEL, false);
callOnPlayingSet(Animator::cancel);
mPlayingSet.clear();
+ // If the end callback is pending, invoke the end callbacks of the animator nodes before
+ // ending this set. Pass notifyListeners=false because this endAnimation will do that.
+ if (consumePendingEndListeners(false /* notifyListeners */)) {
+ for (int i = mNodeMap.size() - 1; i >= 0; i--) {
+ mNodeMap.keyAt(i).consumePendingEndListeners(true /* notifyListeners */);
+ }
+ }
endAnimation();
}
}
diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java
index 492c2ffc561f..fbcc73ea59e7 100644
--- a/core/java/android/animation/ValueAnimator.java
+++ b/core/java/android/animation/ValueAnimator.java
@@ -1182,6 +1182,7 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio
// If end has already been requested, through a previous end() or cancel() call, no-op
// until animation starts again.
if (mAnimationEndRequested) {
+ consumePendingEndListeners(true /* notifyListeners */);
return;
}
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index b38f5da6b638..54ab3b8f185b 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -55,9 +55,7 @@ import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Point;
-import android.graphics.Rect;
import android.graphics.drawable.Icon;
-import android.hardware.HardwareBuffer;
import android.os.BatteryStats;
import android.os.Binder;
import android.os.Build;
@@ -86,7 +84,6 @@ import android.util.Log;
import android.util.Singleton;
import android.util.Size;
import android.view.WindowInsetsController.Appearance;
-import android.window.TaskSnapshot;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.LocalePicker;
@@ -5420,10 +5417,11 @@ public class ActivityManager {
*
* @hide
*/
+ @Nullable
@RequiresPermission(Manifest.permission.MANAGE_USERS)
- public @Nullable String getSwitchingFromUserMessage() {
+ public String getSwitchingFromUserMessage(@UserIdInt int userId) {
try {
- return getService().getSwitchingFromUserMessage();
+ return getService().getSwitchingFromUserMessage(userId);
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
@@ -5434,10 +5432,11 @@ public class ActivityManager {
*
* @hide
*/
+ @Nullable
@RequiresPermission(Manifest.permission.MANAGE_USERS)
- public @Nullable String getSwitchingToUserMessage() {
+ public String getSwitchingToUserMessage(@UserIdInt int userId) {
try {
- return getService().getSwitchingToUserMessage();
+ return getService().getSwitchingToUserMessage(userId);
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index a12c0674998e..e5f7889859c1 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -292,14 +292,14 @@ public abstract class ActivityManagerInternal {
public abstract boolean canStartMoreUsers();
/**
- * Sets the user switcher message for switching from {@link android.os.UserHandle#SYSTEM}.
+ * Sets the user switcher message for switching from a user.
*/
- public abstract void setSwitchingFromSystemUserMessage(String switchingFromSystemUserMessage);
+ public abstract void setSwitchingFromUserMessage(@UserIdInt int user, @Nullable String message);
/**
- * Sets the user switcher message for switching to {@link android.os.UserHandle#SYSTEM}.
+ * Sets the user switcher message for switching to a user.
*/
- public abstract void setSwitchingToSystemUserMessage(String switchingToSystemUserMessage);
+ public abstract void setSwitchingToUserMessage(@UserIdInt int user, @Nullable String message);
/**
* Returns maximum number of users that can run simultaneously.
diff --git a/core/java/android/app/AppCompatTaskInfo.java b/core/java/android/app/AppCompatTaskInfo.java
index 599f1a8be233..ea4646aa9eb9 100644
--- a/core/java/android/app/AppCompatTaskInfo.java
+++ b/core/java/android/app/AppCompatTaskInfo.java
@@ -102,6 +102,8 @@ public class AppCompatTaskInfo implements Parcelable {
private static final int FLAG_FULLSCREEN_OVERRIDE_USER = FLAG_BASE << 8;
/** Top activity flag for whether min aspect ratio of the activity has been overridden.*/
public static final int FLAG_HAS_MIN_ASPECT_RATIO_OVERRIDE = FLAG_BASE << 9;
+ /** Top activity flag for whether restart menu is shown due to display move. */
+ private static final int FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE = FLAG_BASE << 10;
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, value = {
@@ -115,7 +117,8 @@ public class AppCompatTaskInfo implements Parcelable {
FLAG_ELIGIBLE_FOR_USER_ASPECT_RATIO_BUTTON,
FLAG_FULLSCREEN_OVERRIDE_SYSTEM,
FLAG_FULLSCREEN_OVERRIDE_USER,
- FLAG_HAS_MIN_ASPECT_RATIO_OVERRIDE
+ FLAG_HAS_MIN_ASPECT_RATIO_OVERRIDE,
+ FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE
})
public @interface TopActivityFlag {}
@@ -133,7 +136,8 @@ public class AppCompatTaskInfo implements Parcelable {
@TopActivityFlag
private static final int FLAGS_COMPAT_UI_INTERESTED = FLAGS_ORGANIZER_INTERESTED
- | FLAG_IN_SIZE_COMPAT | FLAG_ELIGIBLE_FOR_LETTERBOX_EDU | FLAG_LETTERBOX_EDU_ENABLED;
+ | FLAG_IN_SIZE_COMPAT | FLAG_ELIGIBLE_FOR_LETTERBOX_EDU | FLAG_LETTERBOX_EDU_ENABLED
+ | FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE;
private AppCompatTaskInfo() {
// Do nothing
@@ -300,6 +304,21 @@ public class AppCompatTaskInfo implements Parcelable {
}
/**
+ * @return {@code true} if the restart menu is enabled for the top activity due to display move.
+ */
+ public boolean isRestartMenuEnabledForDisplayMove() {
+ return isTopActivityFlagEnabled(FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE);
+ }
+
+ /**
+ * Sets the top activity flag for whether the restart menu is enabled for the top activity due
+ * to display move.
+ */
+ public void setRestartMenuEnabledForDisplayMove(boolean enable) {
+ setTopActivityFlag(FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE, enable);
+ }
+
+ /**
* @return {@code true} if the top activity bounds are letterboxed.
*/
public boolean isTopActivityLetterboxed() {
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 248f191cb8b8..1864d4a55f2e 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -18,7 +18,6 @@ package android.app;
import static android.location.flags.Flags.FLAG_LOCATION_BYPASS;
-import static android.media.audio.Flags.roForegroundAudioControl;
import static android.permission.flags.Flags.FLAG_OP_ENABLE_MOBILE_DATA_BY_USER;
import static android.service.notification.Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS;
import static android.view.contentprotection.flags.Flags.FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED;
@@ -3481,6 +3480,16 @@ public class AppOpsManager {
}
/**
+ * Whether an app op is backed by a runtime permission or not.
+ * @hide
+ */
+ public static boolean opIsRuntimePermission(int op) {
+ if (op == OP_NONE) return false;
+
+ return ArrayUtils.contains(RUNTIME_PERMISSION_OPS, op);
+ }
+
+ /**
* Retrieve the user restriction associated with an operation, or null if there is not one.
* @hide
*/
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index ad01ad57b2d8..6cdfb97520ae 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -403,8 +403,8 @@ interface IActivityManager {
void setPackageScreenCompatMode(in String packageName, int mode);
@UnsupportedAppUsage
boolean switchUser(int userid);
- String getSwitchingFromUserMessage();
- String getSwitchingToUserMessage();
+ String getSwitchingFromUserMessage(int userId);
+ String getSwitchingToUserMessage(int userId);
@UnsupportedAppUsage
void setStopUserOnSwitch(int value);
boolean removeTask(int taskId);
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 8d4925d8182d..127a08b04e87 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -6143,6 +6143,20 @@ public class Notification implements Parcelable
result.mTitleMarginSet.applyToView(contentView, p.mTextViewId);
contentView.setInt(p.mTextViewId, "setNumIndentLines", p.hasTitle() ? 0 : 1);
}
+ // The expand button uses paddings rather than margins, so we'll adjust it
+ // separately.
+ adjustExpandButtonPadding(contentView, result.mRightIconVisible);
+ }
+
+ private void adjustExpandButtonPadding(RemoteViews contentView, boolean rightIconVisible) {
+ if (notificationsRedesignTemplates()) {
+ final Resources res = mContext.getResources();
+ int normalPadding = res.getDimensionPixelSize(R.dimen.notification_2025_margin);
+ int iconSpacing = res.getDimensionPixelSize(
+ R.dimen.notification_2025_expand_button_right_icon_spacing);
+ contentView.setInt(R.id.expand_button, "setStartPadding",
+ rightIconVisible ? iconSpacing : normalPadding);
+ }
}
// This code is executed on behalf of other apps' notifications, sometimes even by 3p apps,
@@ -6154,12 +6168,21 @@ public class Notification implements Parcelable
@NonNull TemplateBindResult result) {
final Resources resources = mContext.getResources();
final float density = resources.getDisplayMetrics().density;
- final float iconMarginDp = resources.getDimension(
- R.dimen.notification_right_icon_content_margin) / density;
+ int iconMarginId = notificationsRedesignTemplates()
+ ? R.dimen.notification_2025_right_icon_content_margin
+ : R.dimen.notification_right_icon_content_margin;
+ final float iconMarginDp = resources.getDimension(iconMarginId) / density;
final float contentMarginDp = resources.getDimension(
R.dimen.notification_content_margin_end) / density;
- final float expanderSizeDp = resources.getDimension(
- R.dimen.notification_header_expand_icon_size) / density - contentMarginDp;
+ float spaceForExpanderDp;
+ if (notificationsRedesignTemplates()) {
+ spaceForExpanderDp = resources.getDimension(
+ R.dimen.notification_2025_right_icon_expanded_margin_end) / density
+ - contentMarginDp;
+ } else {
+ spaceForExpanderDp = resources.getDimension(
+ R.dimen.notification_header_expand_icon_size) / density - contentMarginDp;
+ }
final float viewHeightDp = resources.getDimension(
R.dimen.notification_right_icon_size) / density;
float viewWidthDp = viewHeightDp; // icons are 1:1 by default
@@ -6176,9 +6199,10 @@ public class Notification implements Parcelable
}
}
}
+ // Margin needed for the header to accommodate the icon when shown
final float extraMarginEndDpIfVisible = viewWidthDp + iconMarginDp;
result.setRightIconState(rightIcon != null /* visible */, viewWidthDp,
- viewHeightDp, extraMarginEndDpIfVisible, expanderSizeDp);
+ viewHeightDp, extraMarginEndDpIfVisible, spaceForExpanderDp);
}
/**
@@ -14658,13 +14682,19 @@ public class Notification implements Parcelable
public final MarginSet mTitleMarginSet = new MarginSet();
public void setRightIconState(boolean visible, float widthDp, float heightDp,
- float marginEndDpIfVisible, float expanderSizeDp) {
+ float marginEndDpIfVisible, float spaceForExpanderDp) {
mRightIconVisible = visible;
mRightIconWidthDp = widthDp;
mRightIconHeightDp = heightDp;
- mHeadingExtraMarginSet.setValues(0, marginEndDpIfVisible);
- mHeadingFullMarginSet.setValues(expanderSizeDp, marginEndDpIfVisible + expanderSizeDp);
- mTitleMarginSet.setValues(0, marginEndDpIfVisible + expanderSizeDp);
+ mHeadingExtraMarginSet.setValues(
+ /* valueIfGone = */ 0,
+ /* valueIfVisible = */ marginEndDpIfVisible);
+ mHeadingFullMarginSet.setValues(
+ /* valueIfGone = */ spaceForExpanderDp,
+ /* valueIfVisible = */ marginEndDpIfVisible + spaceForExpanderDp);
+ mTitleMarginSet.setValues(
+ /* valueIfGone = */ 0,
+ /* valueIfVisible = */ marginEndDpIfVisible + spaceForExpanderDp);
}
/**
diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java
index 38141cf20ce3..6e495768bfd4 100644
--- a/core/java/android/app/PropertyInvalidatedCache.java
+++ b/core/java/android/app/PropertyInvalidatedCache.java
@@ -1417,7 +1417,36 @@ public class PropertyInvalidatedCache<Query, Result> {
}
/**
- * Enable or disable testing. The protocol requires that the mode toggle: for instance, it is
+ * Throw if the current process is not allowed to use test APIs.
+ */
+ @android.ravenwood.annotation.RavenwoodReplace
+ private static void throwIfNotTest() {
+ final ActivityThread activityThread = ActivityThread.currentActivityThread();
+ if (activityThread == null) {
+ // Only tests can reach here.
+ return;
+ }
+ final Instrumentation instrumentation = activityThread.getInstrumentation();
+ if (instrumentation == null) {
+ // Only tests can reach here.
+ return;
+ }
+ if (instrumentation.isInstrumenting()) {
+ return;
+ }
+ if (Flags.enforcePicTestmodeProtocol()) {
+ throw new IllegalStateException("Test-only API called not from a test.");
+ }
+ }
+
+ /**
+ * Do not throw if running under ravenwood.
+ */
+ private static void throwIfNotTest$ravenwood() {
+ }
+
+ /**
+ * Enable or disable test mode. The protocol requires that the mode toggle: for instance, it is
* illegal to clear the test mode if the test mode is already off. Enabling test mode puts
* all caches in the process into test mode; all nonces are initialized to UNSET and
* subsequent reads and writes are to process memory. This has the effect of disabling all
@@ -1425,10 +1454,12 @@ public class PropertyInvalidatedCache<Query, Result> {
* operation.
* @param mode The desired test mode.
* @throws IllegalStateException if the supplied mode is already set.
+ * @throws IllegalStateException if the process is not running an instrumentation test.
* @hide
*/
@VisibleForTesting
public static void setTestMode(boolean mode) {
+ throwIfNotTest();
synchronized (sGlobalLock) {
if (sTestMode == mode) {
final String msg = "cannot set test mode redundantly: mode=" + mode;
@@ -1464,9 +1495,11 @@ public class PropertyInvalidatedCache<Query, Result> {
* for which it would not otherwise have permission. Caches in test mode do NOT write their
* values to the system properties. The effect is local to the current process. Test mode
* must be true when this method is called.
+ * @throws IllegalStateException if the process is not running an instrumentation test.
* @hide
*/
public void testPropertyName() {
+ throwIfNotTest();
synchronized (sGlobalLock) {
if (sTestMode == false) {
throw new IllegalStateException("cannot test property name with test mode off");
@@ -1777,10 +1810,12 @@ public class PropertyInvalidatedCache<Query, Result> {
* When multiple caches share a single property value, using an instance method on one of
* the cache objects to invalidate all of the cache objects becomes confusing and you should
* just use the static version of this function.
+ * @throws IllegalStateException if the process is not running an instrumentation test.
* @hide
*/
@VisibleForTesting
public void disableSystemWide() {
+ throwIfNotTest();
disableSystemWide(mPropertyName);
}
diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java
index 6936ddc5eeac..59247934ba46 100644
--- a/core/java/android/app/TaskInfo.java
+++ b/core/java/android/app/TaskInfo.java
@@ -655,8 +655,10 @@ public class TaskInfo {
+ " effectiveUid=" + effectiveUid
+ " displayId=" + displayId
+ " isRunning=" + isRunning
- + " baseIntent=" + baseIntent + " baseActivity=" + baseActivity
- + " topActivity=" + topActivity + " origActivity=" + origActivity
+ + " baseIntent=" + baseIntent
+ + " baseActivity=" + baseActivity
+ + " topActivity=" + topActivity
+ + " origActivity=" + origActivity
+ " realActivity=" + realActivity
+ " numActivities=" + numActivities
+ " lastActiveTime=" + lastActiveTime
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 5359ba44a3d2..73de1b67dc66 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -8269,6 +8269,7 @@ public class DevicePolicyManager {
*
* @throws SecurityException if the caller is not a device owner, a profile owner or
* delegated certificate chooser.
+ * @throws IllegalArgumentException if {@code alias} does not correspond to an existing key
* @see #grantKeyPairToWifiAuth
*/
public boolean isKeyPairGrantedToWifiAuth(@NonNull String alias) {
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index 0ecd2754b1f0..572bffe6c6a4 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -402,3 +402,13 @@ flag {
description: "Add new API for secondary lockscreen"
bug: "336297680"
}
+
+flag {
+ name: "remove_managed_esim_on_work_profile_deletion"
+ namespace: "enterprise"
+ description: "Remove managed eSIM when work profile is deleted"
+ bug: "347925470"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/android/app/backup/BackupManagerMonitor.java b/core/java/android/app/backup/BackupManagerMonitor.java
index e741bc2bf608..19c24cd8a14a 100644
--- a/core/java/android/app/backup/BackupManagerMonitor.java
+++ b/core/java/android/app/backup/BackupManagerMonitor.java
@@ -297,6 +297,9 @@ public class BackupManagerMonitor {
@hide */
public static final int LOG_EVENT_ID_FAILED_TO_READ_DATA_FROM_TRANSPORT = 81;
+ /** The pipe between the BackupAgent and the framework was broken during full backup. @hide */
+ public static final int LOG_EVENT_ID_FULL_BACKUP_AGENT_PIPE_BROKEN = 82;
+
/**
* This method will be called each time something important happens on BackupManager.
*
diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig
index 5c267c9f6475..4afe75f7814c 100644
--- a/core/java/android/app/notification.aconfig
+++ b/core/java/android/app/notification.aconfig
@@ -265,6 +265,16 @@ flag {
}
flag {
+ name: "expanding_public_view"
+ namespace: "systemui"
+ description: "enables user expanding the public view of a notification"
+ bug: "398853084"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "api_rich_ongoing"
is_exported: true
namespace: "systemui"
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 55d78f9b8c48..cc288b1f5601 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -744,15 +744,22 @@ public abstract class Context {
*/
public static final long BIND_MATCH_QUARANTINED_COMPONENTS = 0x2_0000_0000L;
+ /**
+ * Flag for {@link #bindService} that allows the bound app to be frozen if it is eligible.
+ *
+ * @hide
+ */
+ public static final long BIND_ALLOW_FREEZE = 0x4_0000_0000L;
/**
* These bind flags reduce the strength of the binding such that we shouldn't
* consider it as pulling the process up to the level of the one that is bound to it.
* @hide
*/
- public static final int BIND_REDUCTION_FLAGS =
+ public static final long BIND_REDUCTION_FLAGS =
Context.BIND_ALLOW_OOM_MANAGEMENT | Context.BIND_WAIVE_PRIORITY
- | Context.BIND_NOT_PERCEPTIBLE | Context.BIND_NOT_VISIBLE;
+ | Context.BIND_NOT_PERCEPTIBLE | Context.BIND_NOT_VISIBLE
+ | Context.BIND_ALLOW_FREEZE;
/** @hide */
@IntDef(flag = true, prefix = { "RECEIVER_VISIBLE" }, value = {
diff --git a/core/java/android/content/pm/RegisteredServicesCache.java b/core/java/android/content/pm/RegisteredServicesCache.java
index 74da62c85ed2..10d3051cff6f 100644
--- a/core/java/android/content/pm/RegisteredServicesCache.java
+++ b/core/java/android/content/pm/RegisteredServicesCache.java
@@ -166,7 +166,15 @@ public abstract class RegisteredServicesCache<V> {
@UnsupportedAppUsage
public RegisteredServicesCache(Context context, String interfaceName, String metaDataName,
String attributeName, XmlSerializerAndParser<V> serializerAndParser) {
- mContext = context;
+ this(new Injector<V>(context), interfaceName, metaDataName, attributeName,
+ serializerAndParser);
+ }
+
+ /** Provides the basic functionality for unit tests. */
+ @VisibleForTesting
+ public RegisteredServicesCache(Injector<V> injector, String interfaceName, String metaDataName,
+ String attributeName, XmlSerializerAndParser<V> serializerAndParser) {
+ mContext = injector.getContext();
mInterfaceName = interfaceName;
mMetaDataName = metaDataName;
mAttributesName = attributeName;
@@ -184,7 +192,7 @@ public abstract class RegisteredServicesCache<V> {
if (isCore) {
intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
}
- mBackgroundHandler = BackgroundThread.getHandler();
+ mBackgroundHandler = injector.getBackgroundHandler();
mContext.registerReceiverAsUser(
mPackageReceiver, UserHandle.ALL, intentFilter, null, mBackgroundHandler);
@@ -918,4 +926,25 @@ public abstract class RegisteredServicesCache<V> {
return null;
}
}
+
+ /**
+ * Point of injection for test dependencies.
+ * @param <V> The type of the value.
+ */
+ @VisibleForTesting
+ public static class Injector<V> {
+ private final Context mContext;
+
+ public Injector(Context context) {
+ mContext = context;
+ }
+
+ public Context getContext() {
+ return mContext;
+ }
+
+ public Handler getBackgroundHandler() {
+ return BackgroundThread.getHandler();
+ }
+ }
}
diff --git a/core/java/android/content/pm/UserInfo.java b/core/java/android/content/pm/UserInfo.java
index 582a1a9442ce..53203eba4020 100644
--- a/core/java/android/content/pm/UserInfo.java
+++ b/core/java/android/content/pm/UserInfo.java
@@ -410,6 +410,11 @@ public class UserInfo implements Parcelable {
return UserManager.isUserTypePrivateProfile(userType);
}
+ @FlaggedApi(android.multiuser.Flags.FLAG_ALLOW_SUPERVISING_PROFILE)
+ public boolean isSupervisingProfile() {
+ return UserManager.isUserTypeSupervisingProfile(userType);
+ }
+
/** See {@link #FLAG_DISABLED}*/
@UnsupportedAppUsage
public boolean isEnabled() {
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index 5c904c15e706..7f57f5dbf0ab 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -617,8 +617,8 @@ flag {
}
flag {
- namespace: "multi_user"
name: "logout_user_api"
+ namespace: "multiuser"
description: "Add API to logout user"
bug: "350045389"
}
@@ -629,3 +629,10 @@ flag {
description: "Enable moving content into the Private Space"
bug: "360066001"
}
+
+flag {
+ name: "allow_supervising_profile"
+ namespace: "supervision"
+ description: "Enables support for new supervising user type"
+ bug: "389712089"
+}
diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java
index e43a5fce6cb7..040dcfdeb998 100644
--- a/core/java/android/database/sqlite/SQLiteConnection.java
+++ b/core/java/android/database/sqlite/SQLiteConnection.java
@@ -183,11 +183,6 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen
private static native long nativeChanges(long connectionPtr);
private static native long nativeTotalChanges(long connectionPtr);
- // This method is deprecated and should be removed when it is no longer needed by the
- // robolectric tests. It should not be called from any frameworks java code.
- @Deprecated
- private static native void nativeClose(long connectionPtr);
-
private SQLiteConnection(SQLiteConnectionPool pool,
SQLiteDatabaseConfiguration configuration,
int connectionId, boolean primaryConnection) {
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index bcb7ebfb286f..6e9dcf5a83a1 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -1714,7 +1714,7 @@ public final class CameraManager {
final TaskInfo taskInfo = appTask.getTaskInfo();
final int freeformCameraCompatMode = taskInfo.appCompatTaskInfo
.cameraCompatTaskInfo.freeformCameraCompatMode;
- if (freeformCameraCompatMode != 0
+ if (isInCameraCompatMode(freeformCameraCompatMode)
&& taskInfo.topActivity != null
&& taskInfo.topActivity.getPackageName().equals(packageName)) {
// WindowManager has requested rotation override.
@@ -1741,6 +1741,12 @@ public final class CameraManager {
: ICameraService.ROTATION_OVERRIDE_NONE;
}
+ private static boolean isInCameraCompatMode(@CameraCompatTaskInfo.FreeformCameraCompatMode int
+ freeformCameraCompatMode) {
+ return (freeformCameraCompatMode != CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_UNSPECIFIED)
+ && (freeformCameraCompatMode != CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE);
+ }
+
private static int getRotationOverrideForCompatFreeform(
@CameraCompatTaskInfo.FreeformCameraCompatMode int freeformCameraCompatMode) {
// Only rotate-and-crop if the app and device orientations do not match.
diff --git a/core/java/android/hardware/input/IKeyGestureHandler.aidl b/core/java/android/hardware/input/IKeyGestureHandler.aidl
index 509b9482154e..4da991ee85b1 100644
--- a/core/java/android/hardware/input/IKeyGestureHandler.aidl
+++ b/core/java/android/hardware/input/IKeyGestureHandler.aidl
@@ -28,15 +28,4 @@ interface IKeyGestureHandler {
* to that gesture.
*/
boolean handleKeyGesture(in AidlKeyGestureEvent event, in IBinder focusedToken);
-
- /**
- * Called to know if a particular gesture type is supported by the handler.
- *
- * TODO(b/358569822): Remove this call to reduce the binder calls to single call for
- * handleKeyGesture. For this we need to remove dependency of multi-key gestures to identify if
- * a key gesture is supported on first relevant key down.
- * Also, for now we prioritize handlers in the system server process above external handlers to
- * reduce IPC binder calls.
- */
- boolean isKeyGestureSupported(int gestureType);
}
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index 49db54d81e65..d6419afb2a5a 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -1758,13 +1758,6 @@ public final class InputManager {
*/
boolean handleKeyGestureEvent(@NonNull KeyGestureEvent event,
@Nullable IBinder focusedToken);
-
- /**
- * Called to identify if a particular gesture is of interest to a handler.
- *
- * NOTE: If no active handler supports certain gestures, the gestures will not be captured.
- */
- boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType);
}
/** @hide */
diff --git a/core/java/android/hardware/input/InputManagerGlobal.java b/core/java/android/hardware/input/InputManagerGlobal.java
index a9a45ae45ec3..c4b4831ba76e 100644
--- a/core/java/android/hardware/input/InputManagerGlobal.java
+++ b/core/java/android/hardware/input/InputManagerGlobal.java
@@ -1193,23 +1193,6 @@ public final class InputManagerGlobal {
}
return false;
}
-
- @Override
- public boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType) {
- synchronized (mKeyGestureEventHandlerLock) {
- if (mKeyGestureEventHandlers == null) {
- return false;
- }
- final int numHandlers = mKeyGestureEventHandlers.size();
- for (int i = 0; i < numHandlers; i++) {
- KeyGestureEventHandler handler = mKeyGestureEventHandlers.get(i);
- if (handler.isKeyGestureSupported(gestureType)) {
- return true;
- }
- }
- }
- return false;
- }
}
/**
diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java
index 1a712d2b3f31..9dd1fed4a85a 100644
--- a/core/java/android/hardware/input/KeyGestureEvent.java
+++ b/core/java/android/hardware/input/KeyGestureEvent.java
@@ -108,7 +108,8 @@ public final class KeyGestureEvent {
public static final int KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD = 55;
public static final int KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD = 56;
public static final int KEY_GESTURE_TYPE_GLOBAL_ACTIONS = 57;
- public static final int KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD = 58;
+ @Deprecated
+ public static final int DEPRECATED_KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD = 58;
public static final int KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT = 59;
public static final int KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT = 60;
public static final int KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS = 61;
@@ -200,7 +201,6 @@ public final class KeyGestureEvent {
KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD,
KEY_GESTURE_TYPE_GLOBAL_ACTIONS,
- KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD,
KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT,
KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT,
KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS,
@@ -777,8 +777,6 @@ public final class KeyGestureEvent {
return "KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD";
case KEY_GESTURE_TYPE_GLOBAL_ACTIONS:
return "KEY_GESTURE_TYPE_GLOBAL_ACTIONS";
- case KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD:
- return "KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD";
case KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT:
return "KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT";
case KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT:
diff --git a/core/java/android/os/CombinedMessageQueue/MessageQueue.java b/core/java/android/os/CombinedMessageQueue/MessageQueue.java
index 0964cde5a1f4..c3ec96d17437 100644
--- a/core/java/android/os/CombinedMessageQueue/MessageQueue.java
+++ b/core/java/android/os/CombinedMessageQueue/MessageQueue.java
@@ -145,8 +145,17 @@ public final class MessageQueue {
}
if (Flags.forceConcurrentMessageQueue()) {
- sIsProcessAllowedToUseConcurrent = true;
- return;
+ // b/379472827: Robolectric tests use reflection to access MessageQueue.mMessages.
+ // This is a hack to allow Robolectric tests to use the legacy implementation.
+ try {
+ Class.forName("org.robolectric.Robolectric");
+ } catch (ClassNotFoundException e) {
+ // This is not a Robolectric test.
+ sIsProcessAllowedToUseConcurrent = true;
+ return;
+ }
+ // This is a Robolectric test.
+ // Continue to the following checks.
}
final String processName = Process.myProcessName();
diff --git a/core/java/android/os/IpcDataCache.java b/core/java/android/os/IpcDataCache.java
index 2e7c3be53d90..e888f520b842 100644
--- a/core/java/android/os/IpcDataCache.java
+++ b/core/java/android/os/IpcDataCache.java
@@ -718,7 +718,7 @@ public class IpcDataCache<Query, Result> extends PropertyInvalidatedCache<Query,
}
/**
- * Enable or disable testing. The protocol requires that the mode toggle: for instance, it is
+ * Enable or disable test mode. The protocol requires that the mode toggle: for instance, it is
* illegal to clear the test mode if the test mode is already off. Enabling test mode puts
* all caches in the process into test mode; all nonces are initialized to UNSET and
* subsequent reads and writes are to process memory. This has the effect of disabling all
@@ -726,8 +726,11 @@ public class IpcDataCache<Query, Result> extends PropertyInvalidatedCache<Query,
* operation.
* @param mode The desired test mode.
* @throws IllegalStateException if the supplied mode is already set.
+ * @throws IllegalStateException if the process is not running an instrumentation test.
* @hide
*/
+ @FlaggedApi(android.os.Flags.FLAG_IPC_DATA_CACHE_TEST_APIS)
+ @SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
@TestApi
public static void setTestMode(boolean mode) {
PropertyInvalidatedCache.setTestMode(mode);
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 767019d97758..c01c3cdc7ace 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -209,6 +209,23 @@ public class UserManager {
public static final String USER_TYPE_PROFILE_COMMUNAL = "android.os.usertype.profile.COMMUNAL";
/**
+ * User type representing a user who manages supervision on the device.
+ * When any full user on the device is supervised, the credentials for this profile will be
+ * required in order to perform certain actions for that user (i.e. those controlled by
+ * {@link android.app.supervision.SupervisionManager} or the
+ * {@link android.app.role.RoleManager#ROLE_SYSTEM_SUPERVISION supervision role holder}).
+ * There can only be one supervising profile per device, and the credentials set for that
+ * profile will be used to authorize actions for any supervised user on the device. This is
+ * distinct from a managed profile in that it functions only to authorize certain supervised
+ * actions; it does not represent the user to which restriction or management is applied.
+ * @hide
+ */
+ @FlaggedApi(android.multiuser.Flags.FLAG_ALLOW_SUPERVISING_PROFILE)
+ @SystemApi
+ public static final String USER_TYPE_PROFILE_SUPERVISING =
+ "android.os.usertype.profile.SUPERVISING";
+
+ /**
* User type representing a {@link UserHandle#USER_SYSTEM system} user that is <b>not</b> a
* human user.
* This type of user cannot be created; it can only pre-exist on first boot.
@@ -3226,6 +3243,18 @@ public class UserManager {
}
/**
+ * Returns whether the user type is a
+ * {@link UserManager#USER_TYPE_PROFILE_SUPERVISING supervising profile}.
+ *
+ * @hide
+ */
+ @FlaggedApi(android.multiuser.Flags.FLAG_ALLOW_SUPERVISING_PROFILE)
+ @android.ravenwood.annotation.RavenwoodKeep
+ public static boolean isUserTypeSupervisingProfile(@Nullable String userType) {
+ return USER_TYPE_PROFILE_SUPERVISING.equals(userType);
+ }
+
+ /**
* @hide
* @deprecated Use {@link #isRestrictedProfile()}
*/
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index 86acb2b21cfa..5d80119410e1 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -227,6 +227,14 @@ flag {
}
flag {
+ name: "ipc_data_cache_test_apis"
+ namespace: "system_performance"
+ description: "Expose IpcDataCache test apis to mainline modules."
+ bug: "396173886"
+ is_exported: true
+}
+
+flag {
name: "mainline_vcn_platform_api"
namespace: "vcn"
description: "Expose platform APIs to mainline VCN"
diff --git a/core/java/android/os/health/SystemHealthManager.java b/core/java/android/os/health/SystemHealthManager.java
index a8a22f675e08..b82f278ef7d5 100644
--- a/core/java/android/os/health/SystemHealthManager.java
+++ b/core/java/android/os/health/SystemHealthManager.java
@@ -216,7 +216,7 @@ public class SystemHealthManager {
/**
* Gets the maximum number of TIDs this device supports for getting CPU headroom.
* <p>
- * See {@link CpuHeadroomParams#setTids(int...)}.
+ * See {@link CpuHeadroomParams.Builder#setTids(int...)}.
*
* @return the maximum size of TIDs supported
* @throws UnsupportedOperationException if the CPU headroom API is unsupported.
@@ -288,9 +288,7 @@ public class SystemHealthManager {
/**
* Gets the range of the calculation window size for CPU headroom.
* <p>
- * In API version 36, the range will be a superset of [50, 10000].
- * <p>
- * See {@link CpuHeadroomParams#setCalculationWindowMillis(int)}.
+ * See {@link CpuHeadroomParams.Builder#setCalculationWindowMillis(int)}.
*
* @return the range of the calculation window size supported in milliseconds.
* @throws UnsupportedOperationException if the CPU headroom API is unsupported.
@@ -310,9 +308,7 @@ public class SystemHealthManager {
/**
* Gets the range of the calculation window size for GPU headroom.
* <p>
- * In API version 36, the range will be a superset of [50, 10000].
- * <p>
- * See {@link GpuHeadroomParams#setCalculationWindowMillis(int)}.
+ * See {@link GpuHeadroomParams.Builder#setCalculationWindowMillis(int)}.
*
* @return the range of the calculation window size supported in milliseconds.
* @throws UnsupportedOperationException if the GPU headroom API is unsupported.
diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig
index 414f27498414..176c0c8ab966 100644
--- a/core/java/android/os/vibrator/flags.aconfig
+++ b/core/java/android/os/vibrator/flags.aconfig
@@ -165,3 +165,14 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ namespace: "haptics"
+ name: "remove_hidl_support"
+ description: "Remove framework code to support HIDL vibrator HALs."
+ bug: "308452413"
+ is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_FEATURE
+ }
+}
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index 0476f62ec263..34272b17cf54 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -71,6 +71,19 @@ flag {
}
flag {
+ name: "unknown_call_setting_blocked_logging_enabled"
+ is_exported: true
+ is_fixed_read_only: true
+ namespace: "permissions"
+ description: "enable the metrics when blocking certain app installs during an unknown call"
+ bug: "364535720"
+
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "op_enable_mobile_data_by_user"
is_exported: true
namespace: "permissions"
@@ -372,6 +385,15 @@ flag {
}
flag {
+ name: "record_all_runtime_appops_sqlite"
+ is_fixed_read_only: true
+ is_exported: true
+ namespace: "permissions"
+ description: "Enables recording of all runtime app ops into SQlite"
+ bug: "377584611"
+}
+
+flag {
name: "ranging_permission_enabled"
is_fixed_read_only: true
is_exported: true
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index b97c9b5e83f2..da4709b4b8b1 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -8651,6 +8651,34 @@ public final class Settings {
public static final String DOCKED_CLOCK_FACE = "docked_clock_face";
/**
+ * Setting to indicate that content filters should be enabled on web browsers.
+ *
+ * <ul>
+ * <li>0 = Allow all sites
+ * <li>1 = Try to block explicit sites
+ * </ul>
+ *
+ * @hide
+ */
+ @Readable
+ public static final String BROWSER_CONTENT_FILTERS_ENABLED =
+ "browser_content_filters_enabled";
+
+ /**
+ * Setting to indicate that content filters should be enabled in web search engines.
+ *
+ * <ul>
+ * <li>0 = Off
+ * <li>1 = Filter
+ * </ul>
+ *
+ * @hide
+ */
+ @Readable
+ public static final String SEARCH_CONTENT_FILTERS_ENABLED =
+ "search_content_filters_enabled";
+
+ /**
* Set by the system to track if the user needs to see the call to action for
* the lockscreen notification policy.
* @hide
@@ -10589,6 +10617,57 @@ public final class Settings {
public static final String GLANCEABLE_HUB_ENABLED = "glanceable_hub_enabled";
/**
+ * Indicates that glanceable hub should never be started automatically.
+ *
+ * @hide
+ */
+ public static final int GLANCEABLE_HUB_START_NEVER = 0;
+
+ /**
+ * Indicates that glanceable hub should be started when charging.
+ *
+ * @hide
+ */
+ public static final int GLANCEABLE_HUB_START_CHARGING = 1;
+
+ /**
+ * Indicates that glanceable hub should be started when charging and upright.
+ *
+ * @hide
+ */
+ public static final int GLANCEABLE_HUB_START_CHARGING_UPRIGHT = 2;
+
+ /**
+ * Indicates that glanceable hub should be started when docked.
+ *
+ * @hide
+ */
+ public static final int GLANCEABLE_HUB_START_DOCKED = 3;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ GLANCEABLE_HUB_START_NEVER,
+ GLANCEABLE_HUB_START_CHARGING,
+ GLANCEABLE_HUB_START_CHARGING_UPRIGHT,
+ GLANCEABLE_HUB_START_DOCKED,
+ })
+ public @interface WhenToStartGlanceableHub {
+ }
+
+ /**
+ * Indicates when to start glanceable hub. Possible values are:
+ * 0: Never
+ * 1: While charging always
+ * 2: While upright and charging
+ * 3: While docked
+ *
+ * @hide
+ */
+ public static final String WHEN_TO_START_GLANCEABLE_HUB =
+ "when_to_start_glanceable_hub";
+
+ /**
* Whether home controls are enabled to be shown over the screensaver by the user.
*
* @hide
@@ -11132,6 +11211,12 @@ public final class Settings {
public static final String DOUBLE_TAP_TO_WAKE = "double_tap_to_wake";
/**
+ * Controls whether double tap to sleep is enabled.
+ * @hide
+ */
+ public static final String DOUBLE_TAP_TO_SLEEP = "double_tap_to_sleep";
+
+ /**
* The current assistant component. It could be a voice interaction service,
* or an activity that handles ACTION_ASSIST, or empty which means using the default
* handling.
@@ -12499,6 +12584,48 @@ public final class Settings {
"accessibility_magnification_always_on_enabled";
/**
+ * Controls how the magnification follows the cursor.
+ *
+ * @hide
+ */
+ public static final String ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE =
+ "accessibility_magnification_cursor_following_mode";
+
+ /**
+ * Magnification cursor following mode value for the continuous mode.
+ *
+ * @hide
+ */
+ public static final int ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CONTINUOUS = 0;
+
+ /**
+ * Magnification cursor following mode value for the center mode.
+ *
+ * @hide
+ */
+ public static final int ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CENTER = 1;
+
+ /**
+ * Magnification cursor following mode value for the edge mode.
+ *
+ * @hide
+ */
+ public static final int ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_EDGE = 2;
+
+ /**
+ * Different cursor following settings that can be used as values with
+ * {@link #ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE}.
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_" },
+ value = {
+ ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CONTINUOUS,
+ ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CENTER,
+ ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_EDGE})
+ public @interface AccessibilityMagnificationCursorFollowingMode {}
+
+ /**
* Whether the following typing focus feature for magnification is enabled.
* @hide
*/
diff --git a/core/java/android/service/chooser/flags.aconfig b/core/java/android/service/chooser/flags.aconfig
index ae0b56e6f009..45a21beabd89 100644
--- a/core/java/android/service/chooser/flags.aconfig
+++ b/core/java/android/service/chooser/flags.aconfig
@@ -44,6 +44,16 @@ flag {
}
flag {
+ name: "notify_single_item_change_on_icon_load"
+ namespace: "intentresolver"
+ description: "ChooserGridAdapter to notify specific items change when the target icon is loaded (instead of all-item change)."
+ bug: "298193161"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "fix_resolver_memory_leak"
is_exported: true
namespace: "intentresolver"
diff --git a/core/java/android/service/quickaccesswallet/QuickAccessWalletServiceInfo.java b/core/java/android/service/quickaccesswallet/QuickAccessWalletServiceInfo.java
index e1dc6f6d642b..e9ddfc3559bf 100644
--- a/core/java/android/service/quickaccesswallet/QuickAccessWalletServiceInfo.java
+++ b/core/java/android/service/quickaccesswallet/QuickAccessWalletServiceInfo.java
@@ -96,6 +96,10 @@ class QuickAccessWalletServiceInfo {
defaultAppPackageName = defaultPaymentApp.getPackageName();
}
+ if (defaultAppPackageName == null || defaultAppUser < 0) {
+ return null;
+ }
+
ServiceInfo serviceInfo = getWalletServiceInfo(context, defaultAppPackageName,
defaultAppUser);
if (serviceInfo == null) {
diff --git a/core/java/android/service/voice/HotwordDetectionService.java b/core/java/android/service/voice/HotwordDetectionService.java
index 937aecc8d718..0ace80875325 100644
--- a/core/java/android/service/voice/HotwordDetectionService.java
+++ b/core/java/android/service/voice/HotwordDetectionService.java
@@ -230,8 +230,8 @@ public abstract class HotwordDetectionService extends Service
}
@Override
- public void ping(IRemoteCallback callback) throws RemoteException {
- callback.sendResult(null);
+ public void ping(IPingMe callback) throws RemoteException {
+ callback.onPing();
}
@Override
diff --git a/core/java/android/service/voice/ISandboxedDetectionService.aidl b/core/java/android/service/voice/ISandboxedDetectionService.aidl
index c76ac28eb36c..5c4503cb359c 100644
--- a/core/java/android/service/voice/ISandboxedDetectionService.aidl
+++ b/core/java/android/service/voice/ISandboxedDetectionService.aidl
@@ -65,11 +65,15 @@ oneway interface ISandboxedDetectionService {
void updateRecognitionServiceManager(
in IRecognitionServiceManager recognitionServiceManager);
+ interface IPingMe {
+ void onPing();
+ }
+
/**
* Simply requests the service to trigger the callback, so that the system can check its
* identity.
*/
- void ping(in IRemoteCallback callback);
+ void ping(in IPingMe callback);
void stopDetection();
diff --git a/core/java/android/service/voice/VisualQueryDetectionService.java b/core/java/android/service/voice/VisualQueryDetectionService.java
index 8c9731d12df3..5dcaa3e976eb 100644
--- a/core/java/android/service/voice/VisualQueryDetectionService.java
+++ b/core/java/android/service/voice/VisualQueryDetectionService.java
@@ -122,8 +122,8 @@ public abstract class VisualQueryDetectionService extends Service
}
@Override
- public void ping(IRemoteCallback callback) throws RemoteException {
- callback.sendResult(null);
+ public void ping(IPingMe callback) throws RemoteException {
+ callback.onPing();
}
@Override
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index 44c3f9a8244e..0152c52a6753 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -75,9 +75,12 @@ public abstract class Layout {
// These should match the constants in framework/base/libs/hwui/hwui/DrawTextFunctor.h
private static final float HIGH_CONTRAST_TEXT_BORDER_WIDTH_MIN_PX = 0f;
private static final float HIGH_CONTRAST_TEXT_BORDER_WIDTH_FACTOR = 0f;
- private static final float HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_DP = 5f;
// since we're not using soft light yet, this needs to be much lower than the spec'd 0.8
private static final float HIGH_CONTRAST_TEXT_BACKGROUND_ALPHA_PERCENTAGE = 0.7f;
+ @VisibleForTesting
+ static final float HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_MIN_DP = 5f;
+ @VisibleForTesting
+ static final float HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_FACTOR = 0.5f;
/** @hide */
@IntDef(prefix = { "BREAK_STRATEGY_" }, value = {
@@ -1030,7 +1033,9 @@ public abstract class Layout {
var padding = Math.max(HIGH_CONTRAST_TEXT_BORDER_WIDTH_MIN_PX,
mPaint.getTextSize() * HIGH_CONTRAST_TEXT_BORDER_WIDTH_FACTOR);
- var cornerRadius = mPaint.density * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_DP;
+ var cornerRadius = Math.max(
+ mPaint.density * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_MIN_DP,
+ mPaint.getTextSize() * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_FACTOR);
// We set the alpha on the color itself instead of Paint.setAlpha(), because that function
// actually mutates the color in... *ehem* very strange ways. Also the color might get reset
diff --git a/core/java/android/util/MapCollections.java b/core/java/android/util/MapCollections.java
index cce3a0e3eaa9..e7ceaae964ef 100644
--- a/core/java/android/util/MapCollections.java
+++ b/core/java/android/util/MapCollections.java
@@ -355,7 +355,12 @@ abstract class MapCollections<K, V> {
}
return result;
}
- };
+
+ @Override
+ public String toString() {
+ return toStringHelper(0, this, "KeySet");
+ }
+ }
final class ValuesCollection implements Collection<V> {
@@ -456,7 +461,12 @@ abstract class MapCollections<K, V> {
public <T> T[] toArray(T[] array) {
return toArrayHelper(array, 1);
}
- };
+
+ @Override
+ public String toString() {
+ return toStringHelper(1, this, "ValuesCollection");
+ }
+ }
public static <K, V> boolean containsAllHelper(Map<K, V> map, Collection<?> collection) {
Iterator<?> it = collection.iterator();
@@ -513,6 +523,29 @@ abstract class MapCollections<K, V> {
return array;
}
+ private String toStringHelper(int offset, Object thing, String thingName) {
+ int size = colGetSize();
+ if (size == 0) {
+ return "[]";
+ }
+
+ StringBuilder buffer = new StringBuilder(size * 14);
+ buffer.append('[');
+ for (int i = 0; i < size; i++) {
+ if (i > 0) {
+ buffer.append(", ");
+ }
+ Object entry = colGetEntry(i, offset);
+ if (entry != thing) {
+ buffer.append(entry);
+ } else {
+ buffer.append("(this ").append(thingName).append(")");
+ }
+ }
+ buffer.append(']');
+ return buffer.toString();
+ }
+
public static <T> boolean equalsSetHelper(Set<T> set, Object object) {
if (set == object) {
return true;
diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java
index 7c1e4976b9d3..3a2ec91b3b20 100644
--- a/core/java/android/view/Choreographer.java
+++ b/core/java/android/view/Choreographer.java
@@ -967,8 +967,11 @@ public final class Choreographer {
DisplayEventReceiver.VsyncEventData vsyncEventData) {
final long startNanos;
final long frameIntervalNanos = vsyncEventData.frameInterval;
- boolean resynced = false;
+ // Original intended vsync time that is not adjusted by jitter
+ // or buffer stuffing recovery. Reported for jank tracking.
+ final long intendedFrameTimeNanos = frameTimeNanos;
long offsetFrameTimeNanos = frameTimeNanos;
+ boolean resynced = false;
// Evaluate if buffer stuffing recovery needs to start or end, and
// what actions need to be taken for recovery.
@@ -1012,7 +1015,6 @@ public final class Choreographer {
+ ((offsetFrameTimeNanos - mLastFrameTimeNanos) * 0.000001f) + " ms");
}
- long intendedFrameTimeNanos = offsetFrameTimeNanos;
startNanos = System.nanoTime();
// Calculating jitter involves using the original frame time without
// adjustments from buffer stuffing
diff --git a/core/java/android/view/LayoutInflater.java b/core/java/android/view/LayoutInflater.java
index aad3bf2679d9..901fc02f0040 100644
--- a/core/java/android/view/LayoutInflater.java
+++ b/core/java/android/view/LayoutInflater.java
@@ -516,8 +516,9 @@ public abstract class LayoutInflater {
mConstructorArgs[0] = inflaterContext;
View result = root;
- if (root != null && root.getViewRootImpl() != null) {
- root.getViewRootImpl().notifyRendererOfExpensiveFrame();
+ ViewRootImpl viewRootImpl = root != null ? root.getViewRootImpl() : null;
+ if (viewRootImpl != null) {
+ viewRootImpl.notifyRendererOfExpensiveFrame();
}
try {
diff --git a/core/java/android/view/ListenerWrapper.java b/core/java/android/view/ListenerWrapper.java
new file mode 100644
index 000000000000..fcf3fdb68112
--- /dev/null
+++ b/core/java/android/view/ListenerWrapper.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * A utilty class to bundle a {@link Consumer} and an {@link Executor}
+ * @param <T> the type of value to be reported.
+ * @hide
+ */
+public class ListenerWrapper<T> {
+
+ @NonNull
+ private final Consumer<T> mConsumer;
+ @NonNull
+ private final Executor mExecutor;
+
+ public ListenerWrapper(@NonNull Executor executor, @NonNull Consumer<T> consumer) {
+ mExecutor = Objects.requireNonNull(executor);
+ mConsumer = Objects.requireNonNull(consumer);
+ }
+
+ /**
+ * Relays the new value to the {@link Consumer} using the {@link Executor}
+ */
+ public void accept(@NonNull T value) {
+ mExecutor.execute(() -> mConsumer.accept(value));
+ }
+
+ /**
+ * Returns {@code true} if the consumer matches the one provided in the constructor,
+ * {@code false} otherwise.
+ */
+ public boolean isConsumerSame(@NonNull Consumer<T> consumer) {
+ return mConsumer.equals(consumer);
+ }
+}
diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java
index a5da0c3ce5b1..624216776f42 100644
--- a/core/java/android/view/WindowManagerGlobal.java
+++ b/core/java/android/view/WindowManagerGlobal.java
@@ -44,6 +44,7 @@ import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
import android.view.inputmethod.InputMethodManager;
+import android.view.translation.ListenerGroup;
import android.window.ITrustedPresentationListener;
import android.window.InputTransferToken;
import android.window.TrustedPresentationThresholds;
@@ -58,6 +59,7 @@ import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
+import java.util.List;
import java.util.WeakHashMap;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
@@ -147,6 +149,12 @@ public final class WindowManagerGlobal {
@UnsupportedAppUsage
private final ArrayList<View> mViews = new ArrayList<View>();
+ /**
+ * The {@link ListenerGroup} that is associated to {@link #mViews}.
+ * @hide
+ */
+ @GuardedBy("mLock")
+ private final ListenerGroup<List<View>> mWindowViewsListenerGroup = new ListenerGroup<>();
@UnsupportedAppUsage
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
@UnsupportedAppUsage
@@ -319,6 +327,29 @@ public final class WindowManagerGlobal {
}
}
+ /**
+ * Adds a listener that will be notified whenever {@link #getWindowViews()} changes. The
+ * current value is provided immediately. If it was registered previously then this is ano op.
+ */
+ public void addWindowViewsListener(@NonNull Executor executor,
+ @NonNull Consumer<List<View>> consumer) {
+ synchronized (mLock) {
+ mWindowViewsListenerGroup.addListener(executor, consumer);
+ mWindowViewsListenerGroup.accept(getWindowViews());
+ }
+ }
+
+ /**
+ * Removes a listener that was registered in
+ * {@link #addWindowViewsListener(Executor, Consumer)}. If it was not registered previously,
+ * then this is a no op.
+ */
+ public void removeWindowViewsListener(@NonNull Consumer<List<View>> consumer) {
+ synchronized (mLock) {
+ mWindowViewsListenerGroup.removeListener(consumer);
+ }
+ }
+
public View getWindowView(IBinder windowToken) {
synchronized (mLock) {
final int numViews = mViews.size();
@@ -454,6 +485,7 @@ public final class WindowManagerGlobal {
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView, userId);
+ mWindowViewsListenerGroup.accept(getWindowViews());
} catch (RuntimeException e) {
Log.e(TAG, "Couldn't add view: " + view, e);
final int viewIndex = (index >= 0) ? index : (mViews.size() - 1);
@@ -575,6 +607,7 @@ public final class WindowManagerGlobal {
mDyingViews.remove(view);
}
allViewsRemoved = mRoots.isEmpty();
+ mWindowViewsListenerGroup.accept(getWindowViews());
}
// If we don't have any views anymore in our process, we no longer need the
diff --git a/core/java/android/view/flags/view_flags.aconfig b/core/java/android/view/flags/view_flags.aconfig
index d06f885638b6..d97310494d34 100644
--- a/core/java/android/view/flags/view_flags.aconfig
+++ b/core/java/android/view/flags/view_flags.aconfig
@@ -159,3 +159,11 @@ flag {
bug: "364653005"
is_fixed_read_only: true
}
+
+flag {
+ name: "root_view_changed_listener"
+ namespace: "windowing_sdk"
+ description: "Implement listener pattern for WindowInspector#getGlobalWindowViews."
+ bug: "394397033"
+ is_fixed_read_only: false
+}
diff --git a/core/java/android/view/inspector/WindowInspector.java b/core/java/android/view/inspector/WindowInspector.java
index 69d004e860fd..3ebca3c9d9b6 100644
--- a/core/java/android/view/inspector/WindowInspector.java
+++ b/core/java/android/view/inspector/WindowInspector.java
@@ -16,11 +16,14 @@
package android.view.inspector;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.view.View;
import android.view.WindowManagerGlobal;
import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
/**
* Provides access to window inspection information.
@@ -37,4 +40,25 @@ public final class WindowInspector {
public static List<View> getGlobalWindowViews() {
return WindowManagerGlobal.getInstance().getWindowViews();
}
+
+ /**
+ * Adds a listener that is notified whenever the list of global window views changes. If a
+ * {@link Consumer} is already registered this method is a no op.
+ * @see #getGlobalWindowViews()
+ */
+ @FlaggedApi(android.view.flags.Flags.FLAG_ROOT_VIEW_CHANGED_LISTENER)
+ public static void addGlobalWindowViewsListener(@NonNull Executor executor,
+ @NonNull Consumer<List<View>> consumer) {
+ WindowManagerGlobal.getInstance().addWindowViewsListener(executor, consumer);
+ }
+
+ /**
+ * Removes a listener from getting notifications of global window views changes. If the
+ * {@link Consumer} is not registered this method is a no op.
+ * @see #addGlobalWindowViewsListener(Executor, Consumer)
+ */
+ @FlaggedApi(android.view.flags.Flags.FLAG_ROOT_VIEW_CHANGED_LISTENER)
+ public static void removeGlobalWindowViewsListener(@NonNull Consumer<List<View>> consumer) {
+ WindowManagerGlobal.getInstance().removeWindowViewsListener(consumer);
+ }
}
diff --git a/core/java/android/view/translation/ListenerGroup.java b/core/java/android/view/translation/ListenerGroup.java
new file mode 100644
index 000000000000..bf506815f841
--- /dev/null
+++ b/core/java/android/view/translation/ListenerGroup.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.translation;
+
+import android.annotation.NonNull;
+import android.view.ListenerWrapper;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * A utility class to manage a list of {@link ListenerWrapper}. This class is not thread safe.
+ * @param <T> the type of the value to be reported.
+ * @hide
+ */
+public class ListenerGroup<T> {
+ private final List<ListenerWrapper<T>> mListeners = new ArrayList<>();
+
+ /**
+ * Relays the value to all the registered {@link java.util.function.Consumer}
+ */
+ public void accept(@NonNull T value) {
+ Objects.requireNonNull(value);
+ for (int i = 0; i < mListeners.size(); i++) {
+ mListeners.get(i).accept(value);
+ }
+ }
+
+ /**
+ * Adds a {@link Consumer} to the group. If the {@link Consumer} is already present then this
+ * is a no op.
+ */
+ public void addListener(@NonNull Executor executor, @NonNull Consumer<T> consumer) {
+ if (isContained(consumer)) {
+ return;
+ }
+ mListeners.add(new ListenerWrapper<>(executor, consumer));
+ }
+
+ /**
+ * Removes a {@link Consumer} from the group. If the {@link Consumer} was not present then this
+ * is a no op.
+ */
+ public void removeListener(@NonNull Consumer<T> consumer) {
+ final int index = computeIndex(consumer);
+ if (index > -1) {
+ mListeners.remove(index);
+ }
+ }
+
+ /**
+ * Returns {@code true} if the {@link Consumer} is present in the list, {@code false}
+ * otherwise.
+ */
+ private boolean isContained(Consumer<T> consumer) {
+ return computeIndex(consumer) > -1;
+ }
+
+ /**
+ * Returns the index of the matching {@link ListenerWrapper} if present, {@code -1} otherwise.
+ */
+ private int computeIndex(Consumer<T> consumer) {
+ for (int i = 0; i < mListeners.size(); i++) {
+ if (mListeners.get(i).isConsumerSame(consumer)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+}
diff --git a/core/java/android/window/DesktopExperienceFlags.java b/core/java/android/window/DesktopExperienceFlags.java
index b4ff8e70ec36..866c16cb566d 100644
--- a/core/java/android/window/DesktopExperienceFlags.java
+++ b/core/java/android/window/DesktopExperienceFlags.java
@@ -47,17 +47,19 @@ public enum DesktopExperienceFlags {
com.android.server.display.feature.flags.Flags::baseDensityForExternalDisplays, true),
CONNECTED_DISPLAYS_CURSOR(com.android.input.flags.Flags::connectedDisplaysCursor, true),
DISPLAY_TOPOLOGY(com.android.server.display.feature.flags.Flags::displayTopology, true),
- ENABLE_BUG_FIXES_FOR_SECONDARY_DISPLAY(Flags::enableBugFixesForSecondaryDisplay, false),
+ ENABLE_BUG_FIXES_FOR_SECONDARY_DISPLAY(Flags::enableBugFixesForSecondaryDisplay, true),
ENABLE_CONNECTED_DISPLAYS_DND(Flags::enableConnectedDisplaysDnd, false),
ENABLE_CONNECTED_DISPLAYS_PIP(Flags::enableConnectedDisplaysPip, false),
- ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG(Flags::enableConnectedDisplaysWindowDrag, false),
+ ENABLE_CONNECTED_DISPLAYS_WALLPAPER(
+ android.app.Flags::enableConnectedDisplaysWallpaper, false),
+ ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG(Flags::enableConnectedDisplaysWindowDrag, true),
ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT(
com.android.server.display.feature.flags.Flags::enableDisplayContentModeManagement,
true),
- ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS(Flags::enableDisplayFocusInShellTransitions, false),
- ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING(Flags::enableDisplayWindowingModeSwitching, false),
+ ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS(Flags::enableDisplayFocusInShellTransitions, true),
+ ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING(Flags::enableDisplayWindowingModeSwitching, true),
ENABLE_DRAG_TO_MAXIMIZE(Flags::enableDragToMaximize, true),
- ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT(Flags::enableMoveToNextDisplayShortcut, false),
+ ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT(Flags::enableMoveToNextDisplayShortcut, true),
ENABLE_MULTIPLE_DESKTOPS_BACKEND(Flags::enableMultipleDesktopsBackend, false),
ENABLE_MULTIPLE_DESKTOPS_FRONTEND(Flags::enableMultipleDesktopsFrontend, false),
ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS(
@@ -67,9 +69,10 @@ public enum DesktopExperienceFlags {
ENABLE_PER_DISPLAY_PACKAGE_CONTEXT_CACHE_IN_STATUSBAR_NOTIF(
Flags::enablePerDisplayPackageContextCacheInStatusbarNotif, false),
ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE(Flags::enableProjectedDisplayDesktopMode, false),
- ENABLE_TASKBAR_CONNECTED_DISPLAYS(Flags::enableTaskbarConnectedDisplays, false),
+ ENABLE_TASKBAR_CONNECTED_DISPLAYS(Flags::enableTaskbarConnectedDisplays, true),
ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAYS(Flags::enterDesktopByDefaultOnFreeformDisplays,
- false),
+ true),
+ FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH(Flags::formFactorBasedDesktopFirstSwitch, false),
REPARENT_WINDOW_TOKEN_API(Flags::reparentWindowTokenApi, true)
// go/keep-sorted end
;
diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java
index 78769248c013..17165cdcf7b1 100644
--- a/core/java/android/window/DesktopModeFlags.java
+++ b/core/java/android/window/DesktopModeFlags.java
@@ -138,10 +138,14 @@ public enum DesktopModeFlags {
ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS(
Flags::enableWindowingTransitionHandlersObservers, false),
EXCLUDE_CAPTION_FROM_APP_BOUNDS(Flags::excludeCaptionFromAppBounds, false),
+ FORCE_CLOSE_TOP_TRANSPARENT_FULLSCREEN_TASK(
+ Flags::forceCloseTopTransparentFullscreenTask, false),
IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES(
Flags::ignoreAspectRatioRestrictionsForResizeableFreeformActivities, true),
INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC(
- Flags::includeTopTransparentFullscreenTaskInDesktopHeuristic, true)
+ Flags::includeTopTransparentFullscreenTaskInDesktopHeuristic, true),
+ INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES(
+ Flags::inheritTaskBoundsForTrampolineTaskLaunches, false),
// go/keep-sorted end
;
diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java
index 1156503cf8e8..ea345a5ef46a 100644
--- a/core/java/android/window/WindowContainerTransaction.java
+++ b/core/java/android/window/WindowContainerTransaction.java
@@ -688,12 +688,6 @@ public final class WindowContainerTransaction implements Parcelable {
@NonNull
public WindowContainerTransaction setAdjacentRoots(
@NonNull WindowContainerToken root1, @NonNull WindowContainerToken root2) {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- mHierarchyOps.add(HierarchyOp.createForAdjacentRoots(
- root1.asBinder(),
- root2.asBinder()));
- return this;
- }
return setAdjacentRootSet(root1, root2);
}
@@ -714,10 +708,6 @@ public final class WindowContainerTransaction implements Parcelable {
*/
@NonNull
public WindowContainerTransaction setAdjacentRootSet(@NonNull WindowContainerToken... roots) {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- throw new IllegalArgumentException("allowMultipleAdjacentTaskFragments is not enabled."
- + " Use #setAdjacentRoots instead.");
- }
if (roots.length < 2) {
throw new IllegalArgumentException("setAdjacentRootSet must have size >= 2");
}
@@ -1973,13 +1963,6 @@ public final class WindowContainerTransaction implements Parcelable {
return mContainers;
}
- /** @deprecated b/373709676 replace with {@link #getContainers()}. */
- @Deprecated
- @NonNull
- public IBinder getAdjacentRoot() {
- return mReparent;
- }
-
public boolean getToTop() {
return mToTop;
}
@@ -2127,17 +2110,12 @@ public final class WindowContainerTransaction implements Parcelable {
sb.append(mContainer).append(" to ").append(mToTop ? "top" : "bottom");
break;
case HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS:
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- for (IBinder container : mContainers) {
- if (container == mContainers[0]) {
- sb.append("adjacentRoots=").append(container);
- } else {
- sb.append(", ").append(container);
- }
+ for (IBinder container : mContainers) {
+ if (container == mContainers[0]) {
+ sb.append("adjacentRoots=").append(container);
+ } else {
+ sb.append(", ").append(container);
}
- } else {
- sb.append("container=").append(mContainer)
- .append(" adjacentRoot=").append(mReparent);
}
break;
case HIERARCHY_OP_TYPE_LAUNCH_TASK:
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index 1f710c1cc8c0..e706af999117 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -27,6 +27,17 @@ flag {
}
flag {
+ name: "inherit_task_bounds_for_trampoline_task_launches"
+ namespace: "lse_desktop_experience"
+ description: "Forces trampoline task launches to inherit the bounds of the previous instance /n"
+ "before is closes to prevent each task from cascading."
+ bug: "392815318"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "include_top_transparent_fullscreen_task_in_desktop_heuristic"
namespace: "lse_desktop_experience"
description: "Whether to include any top transparent fullscreen task launched in desktop /n"
@@ -50,6 +61,17 @@ flag {
}
flag {
+ name: "force_close_top_transparent_fullscreen_task"
+ namespace: "lse_desktop_experience"
+ description: "If a top transparent fullscreen task is on top of desktop mode, force it to /n"
+ "close if another task is opened or brought to front."
+ bug: "395041610"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "enable_windowing_dynamic_initial_bounds"
namespace: "lse_desktop_experience"
description: "Enables new initial bounds for desktop windowing which adjust depending on app constraints"
@@ -916,3 +938,10 @@ flag {
description: "Enables the desktop-first mode switching logic based on its form factor."
bug: "394736817"
}
+
+flag {
+ name: "enable_restart_menu_for_connected_displays"
+ namespace: "lse_desktop_experience"
+ description: "Enable restart menu UI, which is shown when an app moves between displays."
+ bug: "397804287"
+}
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index b4fec416bd5f..3927c713e500 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -86,6 +86,14 @@ flag {
}
flag {
+ name: "action_mode_edge_to_edge"
+ namespace: "windowing_frontend"
+ description: "Make contextual action bar edge-to-edge"
+ bug: "379783298"
+ is_fixed_read_only: true
+}
+
+flag {
name: "keyguard_going_away_timeout"
namespace: "windowing_frontend"
description: "Allow a maximum of 10 seconds with keyguardGoingAway=true before force-resetting"
@@ -501,6 +509,17 @@ flag {
description: "Sets Launch powermode for activity launches earlier"
bug: "399380676"
is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
+ name: "scramble_snapshot_file_name"
+ namespace: "windowing_frontend"
+ description: "Scramble the file name of task snapshot."
+ bug: "293139053"
+ is_fixed_read_only: true
metadata {
purpose: PURPOSE_BUGFIX
}
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index c009fc3b7e63..9bc6671bbc31 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -23,6 +23,7 @@ import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE;
import static android.content.ContentProvider.getUriWithoutUserId;
import static android.content.ContentProvider.getUserIdFromUri;
+import static android.service.chooser.Flags.notifySingleItemChangeOnIconLoad;
import static android.stats.devicepolicy.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL;
import static android.stats.devicepolicy.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK;
@@ -163,9 +164,11 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@@ -3212,6 +3215,8 @@ public class ChooserActivity extends ResolverActivity implements
private static final int NUM_EXPANSIONS_TO_HIDE_AZ_LABEL = 20;
+ private final Set<ViewHolderBase> mBoundViewHolders = new HashSet<>();
+
ChooserGridAdapter(ChooserListAdapter wrappedAdapter) {
super();
mChooserListAdapter = wrappedAdapter;
@@ -3232,6 +3237,31 @@ public class ChooserActivity extends ResolverActivity implements
notifyDataSetChanged();
}
});
+ if (notifySingleItemChangeOnIconLoad()) {
+ wrappedAdapter.setOnIconLoadedListener(this::onTargetIconLoaded);
+ }
+ }
+
+ private void onTargetIconLoaded(DisplayResolveInfo info) {
+ for (ViewHolderBase holder : mBoundViewHolders) {
+ switch (holder.getViewType()) {
+ case VIEW_TYPE_NORMAL:
+ TargetInfo itemInfo =
+ mChooserListAdapter.getItem(
+ ((ItemViewHolder) holder).mListPosition);
+ if (info == itemInfo) {
+ notifyItemChanged(holder.getAdapterPosition());
+ }
+ break;
+ case VIEW_TYPE_CALLER_AND_RANK:
+ ItemGroupViewHolder groupHolder = (ItemGroupViewHolder) holder;
+ if (suggestedAppsGroupContainsTarget(groupHolder, info)) {
+ notifyItemChanged(holder.getAdapterPosition());
+ }
+ break;
+ }
+
+ }
}
public void setFooterHeight(int height) {
@@ -3382,6 +3412,9 @@ public class ChooserActivity extends ResolverActivity implements
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+ if (notifySingleItemChangeOnIconLoad()) {
+ mBoundViewHolders.add((ViewHolderBase) holder);
+ }
int viewType = ((ViewHolderBase) holder).getViewType();
switch (viewType) {
case VIEW_TYPE_DIRECT_SHARE:
@@ -3396,6 +3429,22 @@ public class ChooserActivity extends ResolverActivity implements
}
@Override
+ public void onViewRecycled(RecyclerView.ViewHolder holder) {
+ if (notifySingleItemChangeOnIconLoad()) {
+ mBoundViewHolders.remove((ViewHolderBase) holder);
+ }
+ super.onViewRecycled(holder);
+ }
+
+ @Override
+ public boolean onFailedToRecycleView(RecyclerView.ViewHolder holder) {
+ if (notifySingleItemChangeOnIconLoad()) {
+ mBoundViewHolders.remove((ViewHolderBase) holder);
+ }
+ return super.onFailedToRecycleView(holder);
+ }
+
+ @Override
public int getItemViewType(int position) {
int count;
@@ -3604,6 +3653,33 @@ public class ChooserActivity extends ResolverActivity implements
}
}
+ /**
+ * Checks whether the suggested apps group, {@code holder}, contains the target,
+ * {@code info}.
+ */
+ private boolean suggestedAppsGroupContainsTarget(
+ ItemGroupViewHolder holder, DisplayResolveInfo info) {
+
+ int position = holder.getAdapterPosition();
+ int start = getListPosition(position);
+ int startType = getRowType(start);
+
+ int columnCount = holder.getColumnCount();
+ int end = start + columnCount - 1;
+ while (getRowType(end) != startType && end >= start) {
+ end--;
+ }
+
+ for (int i = 0; i < columnCount; i++) {
+ if (start + i <= end) {
+ if (mChooserListAdapter.getItem(holder.getItemIndex(i)) == info) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
int getListPosition(int position) {
position -= getSystemRowCount() + getProfileRowCount();
diff --git a/core/java/com/android/internal/app/ChooserListAdapter.java b/core/java/com/android/internal/app/ChooserListAdapter.java
index d38689c7505b..1b8c36db3908 100644
--- a/core/java/com/android/internal/app/ChooserListAdapter.java
+++ b/core/java/com/android/internal/app/ChooserListAdapter.java
@@ -16,9 +16,12 @@
package com.android.internal.app;
+import static android.service.chooser.Flags.notifySingleItemChangeOnIconLoad;
+
import static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE;
import static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER;
+import android.annotation.Nullable;
import android.app.prediction.AppPredictor;
import android.content.ComponentName;
import android.content.Context;
@@ -56,6 +59,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.function.Consumer;
public class ChooserListAdapter extends ResolverListAdapter {
private static final String TAG = "ChooserListAdapter";
@@ -108,6 +112,9 @@ public class ChooserListAdapter extends ResolverListAdapter {
// Represents the UserSpace in which the Initial Intents should be resolved.
private final UserHandle mInitialIntentsUserSpace;
+ @Nullable
+ private Consumer<DisplayResolveInfo> mOnIconLoadedListener;
+
// For pinned direct share labels, if the text spans multiple lines, the TextView will consume
// the full width, even if the characters actually take up less than that. Measure the actual
// line widths and constrain the View's width based upon that so that the pin doesn't end up
@@ -218,6 +225,10 @@ public class ChooserListAdapter extends ResolverListAdapter {
true);
}
+ public void setOnIconLoadedListener(Consumer<DisplayResolveInfo> onIconLoadedListener) {
+ mOnIconLoadedListener = onIconLoadedListener;
+ }
+
AppPredictor getAppPredictor() {
return mAppPredictor;
}
@@ -329,6 +340,15 @@ public class ChooserListAdapter extends ResolverListAdapter {
}
}
+ @Override
+ protected void onIconLoaded(DisplayResolveInfo info) {
+ if (notifySingleItemChangeOnIconLoad() && mOnIconLoadedListener != null) {
+ mOnIconLoadedListener.accept(info);
+ } else {
+ notifyDataSetChanged();
+ }
+ }
+
private void loadDirectShareIcon(SelectableTargetInfo info) {
LoadDirectShareIconTask task = (LoadDirectShareIconTask) mIconLoaders.get(info);
if (task == null) {
diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java
index 54c0e61fd5cd..4d9ce86096c7 100644
--- a/core/java/com/android/internal/app/ResolverListAdapter.java
+++ b/core/java/com/android/internal/app/ResolverListAdapter.java
@@ -680,6 +680,10 @@ public class ResolverListAdapter extends BaseAdapter {
}
}
+ protected void onIconLoaded(DisplayResolveInfo info) {
+ notifyDataSetChanged();
+ }
+
private void loadLabel(DisplayResolveInfo info) {
LoadLabelTask task = mLabelLoaders.get(info);
if (task == null) {
@@ -1004,7 +1008,7 @@ public class ResolverListAdapter extends BaseAdapter {
mResolverListCommunicator.updateProfileViewButton();
} else if (!mDisplayResolveInfo.hasDisplayIcon()) {
mDisplayResolveInfo.setDisplayIcon(d);
- notifyDataSetChanged();
+ onIconLoaded(mDisplayResolveInfo);
}
}
}
diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java
index 81ca23173457..c6207f9451c2 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistory.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistory.java
@@ -45,11 +45,13 @@ import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import java.io.PrintWriter;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Queue;
import java.util.concurrent.locks.ReentrantLock;
/**
@@ -203,15 +205,6 @@ public class BatteryStatsHistory {
BatteryHistoryFragment getLatestFragment();
/**
- * Given a fragment, returns the earliest fragment that follows it whose monotonic
- * start time falls within the specified range. `startTimeMs` is inclusive, `endTimeMs`
- * is exclusive.
- */
- @Nullable
- BatteryHistoryFragment getNextFragment(BatteryHistoryFragment current, long startTimeMs,
- long endTimeMs);
-
- /**
* Acquires a lock on the entire store.
*/
void lock();
@@ -268,6 +261,60 @@ public class BatteryStatsHistory {
void reset();
}
+ class BatteryHistoryParcelContainer {
+ private boolean mParcelReadyForReading;
+ private Parcel mParcel;
+ private BatteryStatsHistory.BatteryHistoryFragment mFragment;
+ private long mMonotonicStartTime;
+
+ BatteryHistoryParcelContainer(@NonNull Parcel parcel, long monotonicStartTime) {
+ mParcel = parcel;
+ mMonotonicStartTime = monotonicStartTime;
+ mParcelReadyForReading = true;
+ }
+
+ BatteryHistoryParcelContainer(@NonNull BatteryHistoryFragment fragment) {
+ mFragment = fragment;
+ mMonotonicStartTime = fragment.monotonicTimeMs;
+ mParcelReadyForReading = false;
+ }
+
+ @Nullable
+ Parcel getParcel() {
+ if (mParcelReadyForReading) {
+ return mParcel;
+ }
+
+ Parcel parcel = Parcel.obtain();
+ if (readFragmentToParcel(parcel, mFragment)) {
+ parcel.readInt(); // skip buffer size
+ mParcel = parcel;
+ } else {
+ parcel.recycle();
+ }
+ mParcelReadyForReading = true;
+ return mParcel;
+ }
+
+ long getMonotonicStartTime() {
+ return mMonotonicStartTime;
+ }
+
+ /**
+ * Recycles the parcel (if appropriate). Should be called after the parcel has been
+ * processed by the iterator.
+ */
+ void close() {
+ if (mParcel != null && mFragment != null) {
+ mParcel.recycle();
+ }
+ // ParcelContainers are not meant to be reusable. To prevent any unintentional
+ // access to the parcel after it has been recycled, clear the references to it.
+ mParcel = null;
+ mFragment = null;
+ }
+ }
+
private final Parcel mHistoryBuffer;
private final HistoryStepDetailsCalculator mStepDetailsCalculator;
private final Clock mClock;
@@ -447,6 +494,7 @@ public class BatteryStatsHistory {
mWritableHistory = writableHistory;
if (mWritableHistory != null) {
mMutable = false;
+ mHistoryBufferStartTime = mWritableHistory.mHistoryBufferStartTime;
mHistoryMonotonicEndTime = mWritableHistory.mHistoryMonotonicEndTime;
}
@@ -689,95 +737,66 @@ public class BatteryStatsHistory {
}
/**
- * When iterating history files and history buffer, always start from the lowest numbered
- * history file, when reached the mActiveFile (highest numbered history file), do not read from
- * mActiveFile, read from history buffer instead because the buffer has more updated data.
- *
- * @return The parcel that has next record. null if finished all history files and history
- * buffer
+ * Returns all chunks of accumulated history that contain items within the range between
+ * `startTimeMs` (inclusive) and `endTimeMs` (exclusive)
*/
- @Nullable
- public Parcel getNextParcel(long startTimeMs, long endTimeMs) {
- checkImmutable();
+ Queue<BatteryHistoryParcelContainer> getParcelContainers(long startTimeMs, long endTimeMs) {
+ if (mMutable) {
+ throw new IllegalStateException("Iterating over a mutable battery history");
+ }
- // First iterate through all records in current parcel.
- if (mCurrentParcel != null) {
- if (mCurrentParcel.dataPosition() < mCurrentParcelEnd) {
- // There are more records in current parcel.
- return mCurrentParcel;
- } else if (mHistoryBuffer == mCurrentParcel) {
- // finished iterate through all history files and history buffer.
- return null;
- } else if (mHistoryParcels == null
- || !mHistoryParcels.contains(mCurrentParcel)) {
- // current parcel is from history file.
- mCurrentParcel.recycle();
- }
+ if (endTimeMs == MonotonicClock.UNDEFINED || endTimeMs == 0) {
+ endTimeMs = Long.MAX_VALUE;
}
+ Queue<BatteryHistoryParcelContainer> containers = new ArrayDeque<>();
+
if (mStore != null) {
- BatteryHistoryFragment next = mStore.getNextFragment(mCurrentFragment, startTimeMs,
- endTimeMs);
- while (next != null) {
- mCurrentParcel = null;
- mCurrentParcelEnd = 0;
- final Parcel p = Parcel.obtain();
- if (readFragmentToParcel(p, next)) {
- int bufSize = p.readInt();
- int curPos = p.dataPosition();
- mCurrentParcelEnd = curPos + bufSize;
- mCurrentParcel = p;
- if (curPos < mCurrentParcelEnd) {
- mCurrentFragment = next;
- return mCurrentParcel;
- }
- } else {
- p.recycle();
+ List<BatteryHistoryFragment> fragments = mStore.getFragments();
+ for (int i = 0; i < fragments.size(); i++) {
+ BatteryHistoryFragment fragment = fragments.get(i);
+ if (fragment.monotonicTimeMs >= endTimeMs) {
+ break;
+ }
+
+ if (fragment.monotonicTimeMs >= startTimeMs && fragment != mActiveFragment) {
+ containers.add(new BatteryHistoryParcelContainer(fragment));
}
- next = mStore.getNextFragment(next, startTimeMs, endTimeMs);
}
}
- // mHistoryParcels is created when BatteryStatsImpl object is created from deserialization
- // of a parcel, such as Settings app or checkin file.
if (mHistoryParcels != null) {
- while (mParcelIndex < mHistoryParcels.size()) {
- final Parcel p = mHistoryParcels.get(mParcelIndex++);
+ for (int i = 0; i < mHistoryParcels.size(); i++) {
+ final Parcel p = mHistoryParcels.get(i);
if (!verifyVersion(p)) {
continue;
}
- // skip monotonic time field.
- p.readLong();
- // skip monotonic end time field
- p.readLong();
+
+ long monotonicStartTime = p.readLong();
+ if (monotonicStartTime >= endTimeMs) {
+ continue;
+ }
+
+ long monotonicEndTime = p.readLong();
+ if (monotonicEndTime < startTimeMs) {
+ continue;
+ }
+
// skip monotonic size field
p.readLong();
+ // skip buffer size field
+ p.readInt();
- final int bufSize = p.readInt();
- final int curPos = p.dataPosition();
- mCurrentParcelEnd = curPos + bufSize;
- mCurrentParcel = p;
- if (curPos < mCurrentParcelEnd) {
- return mCurrentParcel;
- }
+ containers.add(new BatteryHistoryParcelContainer(p, monotonicStartTime));
}
}
- // finished iterator through history files (except the last one), now history buffer.
- if (mHistoryBuffer.dataSize() <= 0) {
- // buffer is empty.
- return null;
- }
- mHistoryBuffer.setDataPosition(0);
- mCurrentParcel = mHistoryBuffer;
- mCurrentParcelEnd = mCurrentParcel.dataSize();
- return mCurrentParcel;
- }
-
- private void checkImmutable() {
- if (mMutable) {
- throw new IllegalStateException("Iterating over a mutable battery history");
+ if (mHistoryBufferStartTime < endTimeMs) {
+ mHistoryBuffer.setDataPosition(0);
+ containers.add(
+ new BatteryHistoryParcelContainer(mHistoryBuffer, mHistoryBufferStartTime));
}
+ return containers;
}
/**
@@ -818,21 +837,6 @@ public class BatteryStatsHistory {
}
/**
- * Extracts the monotonic time, as per {@link MonotonicClock}, from the supplied battery history
- * buffer.
- */
- public long getHistoryBufferStartTime(Parcel p) {
- int pos = p.dataPosition();
- p.setDataPosition(0);
- p.readInt(); // Skip the version field
- long monotonicTime = p.readLong();
- p.readLong(); // Skip monotonic end time field
- p.readLong(); // Skip monotonic size field
- p.setDataPosition(pos);
- return monotonicTime;
- }
-
- /**
* Writes the battery history contents for persistence.
*/
public void writeSummaryToParcel(Parcel out, boolean inclHistory) {
diff --git a/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java b/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java
index 38398b4bf6f0..09f9b0bf8c7e 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java
@@ -24,6 +24,7 @@ import android.util.Slog;
import android.util.SparseArray;
import java.util.Iterator;
+import java.util.Queue;
/**
* An iterator for {@link BatteryStats.HistoryItem}'s.
@@ -48,7 +49,10 @@ public class BatteryStatsHistoryIterator implements Iterator<BatteryStats.Histor
private long mBaseMonotonicTime;
private long mBaseTimeUtc;
private int mItemIndex = 0;
- private int mMaxHistoryItems;
+ private final int mMaxHistoryItems;
+ private int mParcelDataPosition;
+
+ private Queue<BatteryStatsHistory.BatteryHistoryParcelContainer> mParcelContainers;
public BatteryStatsHistoryIterator(@NonNull BatteryStatsHistory history, long startTimeMs,
long endTimeMs) {
@@ -62,7 +66,11 @@ public class BatteryStatsHistoryIterator implements Iterator<BatteryStats.Histor
@Override
public boolean hasNext() {
if (!mNextItemReady) {
- advance();
+ if (!advance()) {
+ mHistoryItem = null;
+ close();
+ }
+ mNextItemReady = true;
}
return mHistoryItem != null;
@@ -75,35 +83,48 @@ public class BatteryStatsHistoryIterator implements Iterator<BatteryStats.Histor
@Override
public BatteryStats.HistoryItem next() {
if (!mNextItemReady) {
- advance();
+ if (!advance()) {
+ mHistoryItem = null;
+ close();
+ }
}
mNextItemReady = false;
return mHistoryItem;
}
- private void advance() {
- while (true) {
- if (mItemIndex > mMaxHistoryItems) {
- Slog.wtfStack(TAG, "Number of battery history items is too large: " + mItemIndex);
- break;
- }
+ private boolean advance() {
+ if (mParcelContainers == null) {
+ mParcelContainers = mBatteryStatsHistory.getParcelContainers(mStartTimeMs, mEndTimeMs);
+ }
- Parcel p = mBatteryStatsHistory.getNextParcel(mStartTimeMs, mEndTimeMs);
- if (p == null) {
- break;
+ BatteryStatsHistory.BatteryHistoryParcelContainer container;
+ while ((container = mParcelContainers.peek()) != null) {
+ Parcel p = container.getParcel();
+ if (p == null || p.dataPosition() >= p.dataSize()) {
+ container.close();
+ mParcelContainers.remove();
+ mParcelDataPosition = 0;
+ continue;
}
if (!mTimeInitialized) {
- mBaseMonotonicTime = mBatteryStatsHistory.getHistoryBufferStartTime(p);
+ mBaseMonotonicTime = container.getMonotonicStartTime();
mHistoryItem.time = mBaseMonotonicTime;
mTimeInitialized = true;
}
try {
readHistoryDelta(p, mHistoryItem);
+ int dataPosition = p.dataPosition();
+ if (dataPosition <= mParcelDataPosition) {
+ Slog.wtf(TAG, "Corrupted battery history, parcel is not progressing: "
+ + dataPosition + " of " + p.dataSize());
+ return false;
+ }
+ mParcelDataPosition = dataPosition;
} catch (Throwable t) {
Slog.wtf(TAG, "Corrupted battery history", t);
- break;
+ return false;
}
if (mHistoryItem.cmd == BatteryStats.HistoryItem.CMD_CURRENT_TIME
@@ -111,21 +132,24 @@ public class BatteryStatsHistoryIterator implements Iterator<BatteryStats.Histor
mBaseTimeUtc = mHistoryItem.currentTime - (mHistoryItem.time - mBaseMonotonicTime);
}
- mHistoryItem.currentTime = mBaseTimeUtc + (mHistoryItem.time - mBaseMonotonicTime);
+ if (mHistoryItem.time < mStartTimeMs) {
+ continue;
+ }
- if (mEndTimeMs != 0 && mHistoryItem.time >= mEndTimeMs) {
- break;
+ if (mEndTimeMs != 0 && mEndTimeMs != MonotonicClock.UNDEFINED
+ && mHistoryItem.time >= mEndTimeMs) {
+ return false;
}
- if (mHistoryItem.time >= mStartTimeMs) {
- mItemIndex++;
- mNextItemReady = true;
- return;
+
+ if (mItemIndex++ > mMaxHistoryItems) {
+ Slog.wtfStack(TAG, "Number of battery history items is too large: " + mItemIndex);
+ return false;
}
- }
- mHistoryItem = null;
- mNextItemReady = true;
- close();
+ mHistoryItem.currentTime = mBaseTimeUtc + (mHistoryItem.time - mBaseMonotonicTime);
+ return true;
+ }
+ return false;
}
private void readHistoryDelta(Parcel src, BatteryStats.HistoryItem cur) {
diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java
index e20a52b24485..3d81e4fc7acd 100644
--- a/core/java/com/android/internal/policy/DecorView.java
+++ b/core/java/com/android/internal/policy/DecorView.java
@@ -120,6 +120,7 @@ import com.android.internal.view.menu.MenuHelper;
import com.android.internal.widget.ActionBarContextView;
import com.android.internal.widget.BackgroundFallback;
import com.android.internal.widget.floatingtoolbar.FloatingToolbar;
+import com.android.window.flags.Flags;
import java.util.List;
import java.util.concurrent.Executor;
@@ -1003,7 +1004,8 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
public void onWindowSystemUiVisibilityChanged(int visible) {
updateColorViews(null /* insets */, true /* animate */);
- if (mStatusGuard != null && mStatusGuard.getVisibility() == VISIBLE) {
+ if (!Flags.actionModeEdgeToEdge()
+ && mStatusGuard != null && mStatusGuard.getVisibility() == VISIBLE) {
updateStatusGuardColor();
}
}
@@ -1040,7 +1042,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
}
mFrameOffsets.set(insets.getSystemWindowInsetsAsRect());
insets = updateColorViews(insets, true /* animate */);
- insets = updateStatusGuard(insets);
+ insets = updateActionModeInsets(insets);
if (getForeground() != null) {
drawableChanged();
}
@@ -1592,7 +1594,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
}
}
- private WindowInsets updateStatusGuard(WindowInsets insets) {
+ private WindowInsets updateActionModeInsets(WindowInsets insets) {
boolean showStatusGuard = false;
// Show the status guard when the non-overlay contextual action bar is showing
if (mPrimaryActionModeView != null) {
@@ -1608,54 +1610,78 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
final Rect rect = mTempRect;
// Apply the insets that have not been applied by the contentParent yet.
- WindowInsets innerInsets =
+ final WindowInsets innerInsets =
mWindow.mContentParent.computeSystemWindowInsets(insets, rect);
- int newTopMargin = innerInsets.getSystemWindowInsetTop();
- int newLeftMargin = innerInsets.getSystemWindowInsetLeft();
- int newRightMargin = innerInsets.getSystemWindowInsetRight();
-
- // Must use root window insets for the guard, because the color views consume
- // the navigation bar inset if the window does not request LAYOUT_HIDE_NAV - but
- // the status guard is attached at the root.
- WindowInsets rootInsets = getRootWindowInsets();
- int newGuardLeftMargin = rootInsets.getSystemWindowInsetLeft();
- int newGuardRightMargin = rootInsets.getSystemWindowInsetRight();
-
- if (mlp.topMargin != newTopMargin || mlp.leftMargin != newLeftMargin
- || mlp.rightMargin != newRightMargin) {
- mlpChanged = true;
- mlp.topMargin = newTopMargin;
- mlp.leftMargin = newLeftMargin;
- mlp.rightMargin = newRightMargin;
- }
+ final boolean consumesSystemWindowInsetsTop;
+ if (Flags.actionModeEdgeToEdge()) {
+ final Insets newPadding = innerInsets.getSystemWindowInsets();
+ final Insets newMargin = innerInsets.getInsets(
+ WindowInsets.Type.navigationBars());
+
+ // Don't extend into navigation bar area so the width can align with status
+ // bar color view.
+ if (mlp.leftMargin != newMargin.left
+ || mlp.rightMargin != newMargin.right) {
+ mlpChanged = true;
+ mlp.leftMargin = newMargin.left;
+ mlp.rightMargin = newMargin.right;
+ }
+
+ mPrimaryActionModeView.setPadding(
+ newPadding.left - newMargin.left,
+ newPadding.top,
+ newPadding.right - newMargin.right,
+ 0);
+ consumesSystemWindowInsetsTop = newPadding.top > 0;
+ } else {
+ int newTopMargin = innerInsets.getSystemWindowInsetTop();
+ int newLeftMargin = innerInsets.getSystemWindowInsetLeft();
+ int newRightMargin = innerInsets.getSystemWindowInsetRight();
+
+ // Must use root window insets for the guard, because the color views
+ // consume the navigation bar inset if the window does not request
+ // LAYOUT_HIDE_NAV - but the status guard is attached at the root.
+ WindowInsets rootInsets = getRootWindowInsets();
+ int newGuardLeftMargin = rootInsets.getSystemWindowInsetLeft();
+ int newGuardRightMargin = rootInsets.getSystemWindowInsetRight();
+
+ if (mlp.topMargin != newTopMargin || mlp.leftMargin != newLeftMargin
+ || mlp.rightMargin != newRightMargin) {
+ mlpChanged = true;
+ mlp.topMargin = newTopMargin;
+ mlp.leftMargin = newLeftMargin;
+ mlp.rightMargin = newRightMargin;
+ }
- if (newTopMargin > 0 && mStatusGuard == null) {
- mStatusGuard = new View(mContext);
- mStatusGuard.setVisibility(GONE);
- final LayoutParams lp = new LayoutParams(MATCH_PARENT,
- mlp.topMargin, Gravity.LEFT | Gravity.TOP);
- lp.leftMargin = newGuardLeftMargin;
- lp.rightMargin = newGuardRightMargin;
- addView(mStatusGuard, indexOfChild(mStatusColorViewState.view), lp);
- } else if (mStatusGuard != null) {
- final LayoutParams lp = (LayoutParams)
- mStatusGuard.getLayoutParams();
- if (lp.height != mlp.topMargin || lp.leftMargin != newGuardLeftMargin
- || lp.rightMargin != newGuardRightMargin) {
- lp.height = mlp.topMargin;
+ if (newTopMargin > 0 && mStatusGuard == null) {
+ mStatusGuard = new View(mContext);
+ mStatusGuard.setVisibility(GONE);
+ final LayoutParams lp = new LayoutParams(MATCH_PARENT,
+ mlp.topMargin, Gravity.LEFT | Gravity.TOP);
lp.leftMargin = newGuardLeftMargin;
lp.rightMargin = newGuardRightMargin;
- mStatusGuard.setLayoutParams(lp);
+ addView(mStatusGuard, indexOfChild(mStatusColorViewState.view), lp);
+ } else if (mStatusGuard != null) {
+ final LayoutParams lp = (LayoutParams)
+ mStatusGuard.getLayoutParams();
+ if (lp.height != mlp.topMargin || lp.leftMargin != newGuardLeftMargin
+ || lp.rightMargin != newGuardRightMargin) {
+ lp.height = mlp.topMargin;
+ lp.leftMargin = newGuardLeftMargin;
+ lp.rightMargin = newGuardRightMargin;
+ mStatusGuard.setLayoutParams(lp);
+ }
}
- }
- // The action mode's theme may differ from the app, so
- // always show the status guard above it if we have one.
- showStatusGuard = mStatusGuard != null;
+ // The action mode's theme may differ from the app, so
+ // always show the status guard above it if we have one.
+ showStatusGuard = mStatusGuard != null;
- if (showStatusGuard && mStatusGuard.getVisibility() != VISIBLE) {
- // If it wasn't previously shown, the color may be stale
- updateStatusGuardColor();
+ if (showStatusGuard && mStatusGuard.getVisibility() != VISIBLE) {
+ // If it wasn't previously shown, the color may be stale
+ updateStatusGuardColor();
+ }
+ consumesSystemWindowInsetsTop = showStatusGuard;
}
// We only need to consume the insets if the action
@@ -1664,14 +1690,16 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
// screen_simple_overlay_action_mode.xml).
final boolean nonOverlay = (mWindow.getLocalFeaturesPrivate()
& (1 << Window.FEATURE_ACTION_MODE_OVERLAY)) == 0;
- if (nonOverlay && showStatusGuard) {
+ if (nonOverlay && consumesSystemWindowInsetsTop) {
insets = insets.inset(0, insets.getSystemWindowInsetTop(), 0, 0);
}
} else {
- // reset top margin
- if (mlp.topMargin != 0 || mlp.leftMargin != 0 || mlp.rightMargin != 0) {
- mlpChanged = true;
- mlp.topMargin = 0;
+ if (!Flags.actionModeEdgeToEdge()) {
+ // reset top margin
+ if (mlp.topMargin != 0 || mlp.leftMargin != 0 || mlp.rightMargin != 0) {
+ mlpChanged = true;
+ mlp.topMargin = 0;
+ }
}
}
if (mlpChanged) {
@@ -1679,7 +1707,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
}
}
}
- if (mStatusGuard != null) {
+ if (!Flags.actionModeEdgeToEdge() && mStatusGuard != null) {
mStatusGuard.setVisibility(showStatusGuard ? VISIBLE : GONE);
}
return insets;
@@ -2183,7 +2211,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
for (int i = getChildCount() - 1; i >= 0; i--) {
View v = getChildAt(i);
if (v != mStatusColorViewState.view && v != mNavigationColorViewState.view
- && v != mStatusGuard) {
+ && (Flags.actionModeEdgeToEdge() || v != mStatusGuard)) {
removeViewAt(i);
}
}
diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
index 2d989943800e..6f7e5ad51b89 100644
--- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
@@ -79,6 +79,7 @@ import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@@ -864,5 +865,24 @@ public abstract class PerfettoProtoLogImpl extends IProtoLogClient.Stub implemen
throw new RuntimeException("Both mMessageString and mMessageHash should never be null");
}
}
+
+ /**
+ * This is only used by unit tests to wait until {@link #connectToConfigurationService} is
+ * done. Because unit tests are sensitive to concurrent accesses.
+ */
+ @VisibleForTesting
+ public static void waitForInitialization() {
+ final IProtoLog currentInstance = ProtoLog.getSingleInstance();
+ if (!(currentInstance instanceof PerfettoProtoLogImpl protoLog)) {
+ return;
+ }
+ try {
+ protoLog.mBackgroundLoggingService.submit(() -> {
+ Log.i(LOG_TAG, "Complete initialization");
+ }).get();
+ } catch (InterruptedException | ExecutionException e) {
+ Log.e(LOG_TAG, "Failed to wait for tracing service", e);
+ }
+ }
}
diff --git a/core/java/com/android/internal/widget/ActionBarContextView.java b/core/java/com/android/internal/widget/ActionBarContextView.java
index 80fc218839d5..d5bb51187ba4 100644
--- a/core/java/com/android/internal/widget/ActionBarContextView.java
+++ b/core/java/com/android/internal/widget/ActionBarContextView.java
@@ -34,6 +34,7 @@ import android.widget.TextView;
import com.android.internal.R;
import com.android.internal.view.menu.MenuBuilder;
+import com.android.window.flags.Flags;
/**
* @hide
@@ -315,12 +316,14 @@ public class ActionBarContextView extends AbsActionBarView {
final int contentWidth = MeasureSpec.getSize(widthMeasureSpec);
- int maxHeight = mContentHeight > 0 ?
- mContentHeight : MeasureSpec.getSize(heightMeasureSpec);
+ final int maxHeight = !Flags.actionModeEdgeToEdge() && mContentHeight > 0
+ ? mContentHeight : MeasureSpec.getSize(heightMeasureSpec);
final int verticalPadding = getPaddingTop() + getPaddingBottom();
int availableWidth = contentWidth - getPaddingLeft() - getPaddingRight();
- final int height = maxHeight - verticalPadding;
+ final int height = Flags.actionModeEdgeToEdge()
+ ? mContentHeight > 0 ? mContentHeight : maxHeight
+ : maxHeight - verticalPadding;
final int childSpecHeight = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
if (mClose != null) {
@@ -376,7 +379,8 @@ public class ActionBarContextView extends AbsActionBarView {
}
setMeasuredDimension(contentWidth, measuredHeight);
} else {
- setMeasuredDimension(contentWidth, maxHeight);
+ setMeasuredDimension(contentWidth, Flags.actionModeEdgeToEdge()
+ ? mContentHeight + verticalPadding : maxHeight);
}
}
diff --git a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java
index ff57fd4fe2ce..362b79db4003 100644
--- a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java
+++ b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java
@@ -294,54 +294,24 @@ public class ActionBarOverlayLayout extends ViewGroup implements DecorContentPar
}
}
- private boolean applyInsets(View view, Rect insets, boolean toPadding,
- boolean left, boolean top, boolean right, boolean bottom) {
- boolean changed;
- if (toPadding) {
- changed = setMargin(view, EMPTY_RECT, left, top, right, bottom);
- changed |= setPadding(view, insets, left, top, right, bottom);
- } else {
- changed = setPadding(view, EMPTY_RECT, left, top, right, bottom);
- changed |= setMargin(view, insets, left, top, right, bottom);
- }
- return changed;
- }
-
- private boolean setPadding(View view, Rect insets,
- boolean left, boolean top, boolean right, boolean bottom) {
- if ((left && view.getPaddingLeft() != insets.left)
- || (top && view.getPaddingTop() != insets.top)
- || (right && view.getPaddingRight() != insets.right)
- || (bottom && view.getPaddingBottom() != insets.bottom)) {
- view.setPadding(
- left ? insets.left : view.getPaddingLeft(),
- top ? insets.top : view.getPaddingTop(),
- right ? insets.right : view.getPaddingRight(),
- bottom ? insets.bottom : view.getPaddingBottom());
- return true;
- }
- return false;
- }
-
- private boolean setMargin(View view, Rect insets,
- boolean left, boolean top, boolean right, boolean bottom) {
+ private boolean setMargin(View view, int left, int top, int right, int bottom) {
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
boolean changed = false;
- if (left && lp.leftMargin != insets.left) {
+ if (lp.leftMargin != left) {
changed = true;
- lp.leftMargin = insets.left;
+ lp.leftMargin = left;
}
- if (top && lp.topMargin != insets.top) {
+ if (lp.topMargin != top) {
changed = true;
- lp.topMargin = insets.top;
+ lp.topMargin = top;
}
- if (right && lp.rightMargin != insets.right) {
+ if (lp.rightMargin != right) {
changed = true;
- lp.rightMargin = insets.right;
+ lp.rightMargin = right;
}
- if (bottom && lp.bottomMargin != insets.bottom) {
+ if (lp.bottomMargin != bottom) {
changed = true;
- lp.bottomMargin = insets.bottom;
+ lp.bottomMargin = bottom;
}
return changed;
}
@@ -367,12 +337,30 @@ public class ActionBarOverlayLayout extends ViewGroup implements DecorContentPar
final Insets sysInsets = insets.getSystemWindowInsets();
mSystemInsets.set(sysInsets.left, sysInsets.top, sysInsets.right, sysInsets.bottom);
- // The top and bottom action bars are always within the content area.
- boolean changed = applyInsets(mActionBarTop, mSystemInsets,
- mActionBarExtendsIntoSystemInsets, true, true, true, false);
- if (mActionBarBottom != null) {
- changed |= applyInsets(mActionBarBottom, mSystemInsets,
- mActionBarExtendsIntoSystemInsets, true, false, true, true);
+ boolean changed = false;
+ if (mActionBarExtendsIntoSystemInsets) {
+ // Don't extend into navigation bar area so the width can align with status bar
+ // color view.
+ final Insets navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars());
+ final int paddingLeft = sysInsets.left - navBarInsets.left;
+ final int paddingRight = sysInsets.right - navBarInsets.right;
+ mActionBarTop.setPadding(paddingLeft, sysInsets.top, paddingRight, 0);
+ changed |= setMargin(
+ mActionBarTop, navBarInsets.left, 0, navBarInsets.right, 0);
+ if (mActionBarBottom != null) {
+ mActionBarBottom.setPadding(paddingLeft, 0, paddingRight, sysInsets.bottom);
+ changed |= setMargin(
+ mActionBarBottom, navBarInsets.left, 0, navBarInsets.right, 0);
+ }
+ } else {
+ mActionBarTop.setPadding(0, 0, 0, 0);
+ changed |= setMargin(
+ mActionBarTop, sysInsets.left, sysInsets.top, sysInsets.right, 0);
+ if (mActionBarBottom != null) {
+ mActionBarBottom.setPadding(0, 0, 0, 0);
+ changed |= setMargin(
+ mActionBarTop, sysInsets.left, 0, sysInsets.right, sysInsets.bottom);
+ }
}
// Cannot use the result of computeSystemWindowInsets, because that consumes the
@@ -521,7 +509,12 @@ public class ActionBarOverlayLayout extends ViewGroup implements DecorContentPar
);
}
}
- setMargin(mContent, mContentInsets, true, true, true, true);
+ setMargin(
+ mContent,
+ mContentInsets.left,
+ mContentInsets.top,
+ mContentInsets.right,
+ mContentInsets.bottom);
if (!mLastInnerInsets.equals(mInnerInsets)) {
// If the inner insets have changed, we need to dispatch this down to
diff --git a/core/java/com/android/internal/widget/NotificationExpandButton.java b/core/java/com/android/internal/widget/NotificationExpandButton.java
index dd12f69a56fb..42b1bdbc51b2 100644
--- a/core/java/com/android/internal/widget/NotificationExpandButton.java
+++ b/core/java/com/android/internal/widget/NotificationExpandButton.java
@@ -129,6 +129,16 @@ public class NotificationExpandButton extends FrameLayout {
updateExpandedState();
}
+ /**
+ * Adjust the padding at the start of the view based on the layout direction (RTL/LTR).
+ * This is needed because RemoteViews don't have an equivalent for
+ * {@link this#setPaddingRelative}.
+ */
+ @RemotableViewMethod
+ public void setStartPadding(int startPadding) {
+ setPaddingRelative(startPadding, getPaddingTop(), getPaddingEnd(), getPaddingBottom());
+ }
+
private void updateExpandedState() {
int drawableId;
int contentDescriptionId;
diff --git a/core/jni/android_database_SQLiteConnection.cpp b/core/jni/android_database_SQLiteConnection.cpp
index 36c08a51c66a..95bab542f9d6 100644
--- a/core/jni/android_database_SQLiteConnection.cpp
+++ b/core/jni/android_database_SQLiteConnection.cpp
@@ -231,12 +231,6 @@ static void nativeClose(JNIEnv* env, jclass clazz, jlong connectionPtr, jboolean
}
}
-// This method is deprecated and should be removed when it is no longer needed by the
-// robolectric tests.
-static void nativeClose(JNIEnv* env, jclass clazz, jlong connectionPtr) {
- nativeClose(env, clazz, connectionPtr, false);
-}
-
static void sqliteCustomScalarFunctionCallback(sqlite3_context *context,
int argc, sqlite3_value **argv) {
JNIEnv* env = AndroidRuntime::getJNIEnv();
@@ -974,8 +968,6 @@ static const JNINativeMethod sMethods[] =
(void*)nativeOpen },
{ "nativeClose", "(JZ)V",
(void*) static_cast<void(*)(JNIEnv*,jclass,jlong,jboolean)>(nativeClose) },
- { "nativeClose", "(J)V",
- (void*) static_cast<void(*)(JNIEnv*,jclass,jlong)>(nativeClose) },
{ "nativeRegisterCustomScalarFunction", "(JLjava/lang/String;Ljava/util/function/UnaryOperator;)V",
(void*)nativeRegisterCustomScalarFunction },
{ "nativeRegisterCustomAggregateFunction", "(JLjava/lang/String;Ljava/util/function/BinaryOperator;)V",
diff --git a/core/jni/com_android_internal_content_FileSystemUtils.cpp b/core/jni/com_android_internal_content_FileSystemUtils.cpp
index 48c92c87f54e..886b9f1ae658 100644
--- a/core/jni/com_android_internal_content_FileSystemUtils.cpp
+++ b/core/jni/com_android_internal_content_FileSystemUtils.cpp
@@ -201,8 +201,8 @@ bool punchHoles(const char *filePath, const uint64_t offset,
return true;
}
-bool getLoadSegmentPhdrs(const char *filePath, const uint64_t offset,
- std::vector<Elf64_Phdr> &programHeaders) {
+read_elf_status_t getLoadSegmentPhdrs(const char *filePath, const uint64_t offset,
+ std::vector<Elf64_Phdr> &programHeaders) {
// Open Elf file
Elf64_Ehdr ehdr;
std::ifstream inputStream(filePath, std::ifstream::in);
@@ -212,13 +212,13 @@ bool getLoadSegmentPhdrs(const char *filePath, const uint64_t offset,
// read executable headers
inputStream.read((char *)&ehdr, sizeof(ehdr));
if (!inputStream.good()) {
- return false;
+ return ELF_READ_ERROR;
}
- // only consider elf64 for punching holes
+ // only consider ELF64 files
if (ehdr.e_ident[EI_CLASS] != ELFCLASS64) {
ALOGW("Provided file is not ELF64");
- return false;
+ return ELF_IS_NOT_64_BIT;
}
// read the program headers from elf file
@@ -229,7 +229,7 @@ bool getLoadSegmentPhdrs(const char *filePath, const uint64_t offset,
uint64_t phOffset;
if (__builtin_add_overflow(offset, programHeaderOffset, &phOffset)) {
ALOGE("Overflow occurred when calculating phOffset");
- return false;
+ return ELF_READ_ERROR;
}
inputStream.seekg(phOffset);
@@ -237,7 +237,7 @@ bool getLoadSegmentPhdrs(const char *filePath, const uint64_t offset,
Elf64_Phdr header;
inputStream.read((char *)&header, sizeof(header));
if (!inputStream.good()) {
- return false;
+ return ELF_READ_ERROR;
}
if (header.p_type != PT_LOAD) {
@@ -246,13 +246,14 @@ bool getLoadSegmentPhdrs(const char *filePath, const uint64_t offset,
programHeaders.push_back(header);
}
- return true;
+ return ELF_READ_OK;
}
bool punchHolesInElf64(const char *filePath, const uint64_t offset) {
std::vector<Elf64_Phdr> programHeaders;
- if (!getLoadSegmentPhdrs(filePath, offset, programHeaders)) {
- ALOGE("Failed to read program headers from ELF file.");
+ read_elf_status_t status = getLoadSegmentPhdrs(filePath, offset, programHeaders);
+ if (status != ELF_READ_OK) {
+ ALOGE("Failed to read program headers from 64 bit ELF file.");
return false;
}
return punchHoles(filePath, offset, programHeaders);
diff --git a/core/jni/com_android_internal_content_FileSystemUtils.h b/core/jni/com_android_internal_content_FileSystemUtils.h
index 4a95686c5a0c..c4dc1115e6c4 100644
--- a/core/jni/com_android_internal_content_FileSystemUtils.h
+++ b/core/jni/com_android_internal_content_FileSystemUtils.h
@@ -22,6 +22,12 @@
namespace android {
+enum read_elf_status_t {
+ ELF_IS_NOT_64_BIT = -2,
+ ELF_READ_ERROR = -1,
+ ELF_READ_OK = 0,
+};
+
/*
* This function deallocates space used by zero padding at the end of LOAD segments in given
* uncompressed ELF file. Read ELF headers and find out the offset and sizes of LOAD segments.
@@ -39,10 +45,10 @@ bool punchHolesInElf64(const char* filePath, uint64_t offset);
bool punchHolesInZip(const char* filePath, uint64_t offset, uint16_t extraFieldLen);
/*
- * This function reads program headers from ELF file. ELF can be specified with file path directly
- * or it should be at offset inside Apk. Program headers passed to function is populated.
+ * This function reads program headers from 64 bit ELF file. ELF can be specified with file path
+ * directly or it should be at offset inside Apk. Program headers passed to function is populated.
*/
-bool getLoadSegmentPhdrs(const char* filePath, const uint64_t offset,
- std::vector<Elf64_Phdr>& programHeaders);
+read_elf_status_t getLoadSegmentPhdrs(const char* filePath, const uint64_t offset,
+ std::vector<Elf64_Phdr>& programHeaders);
} // namespace android \ No newline at end of file
diff --git a/core/jni/com_android_internal_content_NativeLibraryHelper.cpp b/core/jni/com_android_internal_content_NativeLibraryHelper.cpp
index 14132e61ff0e..7cf523f18a90 100644
--- a/core/jni/com_android_internal_content_NativeLibraryHelper.cpp
+++ b/core/jni/com_android_internal_content_NativeLibraryHelper.cpp
@@ -640,7 +640,17 @@ com_android_internal_content_NativeLibraryHelper_openApkFd(JNIEnv *env, jclass,
static jint checkLoadSegmentAlignment(const char* fileName, off64_t offset) {
std::vector<Elf64_Phdr> programHeaders;
- if (!getLoadSegmentPhdrs(fileName, offset, programHeaders)) {
+ read_elf_status_t status = getLoadSegmentPhdrs(fileName, offset, programHeaders);
+ // Ignore the ELFs which are not 64 bit.
+ if (status == ELF_IS_NOT_64_BIT) {
+ ALOGW("ELF file is not 64 Bit");
+ // PAGE_SIZE_APP_COMPAT_FLAG_UNDEFINED is equivalent of skipping the current file.
+ // on return, flag is OR'ed with flags from other ELF files. If some app has 32 bit ELF in
+ // 64 bit directory, alignment of that ELF will be ignored.
+ return PAGE_SIZE_APP_COMPAT_FLAG_UNDEFINED;
+ }
+
+ if (status == ELF_READ_ERROR) {
ALOGE("Failed to read program headers from ELF file.");
return PAGE_SIZE_APP_COMPAT_FLAG_ERROR;
}
diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto
index ac4bac6d206e..1caa9e7af348 100644
--- a/core/proto/android/providers/settings/secure.proto
+++ b/core/proto/android/providers/settings/secure.proto
@@ -112,7 +112,8 @@ message SecureSettingsProto {
optional SettingProto autoclick_ignore_minor_cursor_movement = 63 [ (android.privacy).dest = DEST_AUTOMATIC ];
optional SettingProto autoclick_panel_position = 64 [ (android.privacy).dest = DEST_AUTOMATIC ];
optional SettingProto autoclick_revert_to_left_click = 65 [ (android.privacy).dest = DEST_AUTOMATIC ];
-
+ // Setting for accessibility magnification for cursor following mode.
+ optional SettingProto accessibility_magnification_cursor_following_mode = 66 [ (android.privacy).dest = DEST_AUTOMATIC ];
}
optional Accessibility accessibility = 2;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 36b65ba43162..e16ce9849ff2 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -9292,9 +9292,6 @@
<action android:name="android.intent.action.UPDATE_PINS" />
<data android:scheme="content" android:host="*" android:mimeType="*/*" />
</intent-filter>
- <intent-filter>
- <action android:name="android.intent.action.BOOT_COMPLETED" />
- </intent-filter>
</receiver>
<receiver android:name="com.android.server.updates.IntentFirewallInstallReceiver"
diff --git a/core/res/res/drawable/accessibility_autoclick_scroll_down.xml b/core/res/res/drawable/accessibility_autoclick_scroll_down.xml
new file mode 100644
index 000000000000..13f1ba0dafe8
--- /dev/null
+++ b/core/res/res/drawable/accessibility_autoclick_scroll_down.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/materialColorPrimary"
+ android:pathData="M12,20l8,-8h-5V4h-6v8H4z"/>
+</vector>
diff --git a/core/res/res/drawable/accessibility_autoclick_scroll_exit.xml b/core/res/res/drawable/accessibility_autoclick_scroll_exit.xml
new file mode 100644
index 000000000000..e53301f25d65
--- /dev/null
+++ b/core/res/res/drawable/accessibility_autoclick_scroll_exit.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/materialColorPrimary"
+ android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
+</vector>
diff --git a/core/res/res/drawable/accessibility_autoclick_scroll_left.xml b/core/res/res/drawable/accessibility_autoclick_scroll_left.xml
new file mode 100644
index 000000000000..39475bc689d2
--- /dev/null
+++ b/core/res/res/drawable/accessibility_autoclick_scroll_left.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/materialColorPrimary"
+ android:pathData="M4,12l8,8v-5h8v-6h-8V4z"/>
+</vector>
diff --git a/core/res/res/drawable/accessibility_autoclick_scroll_right.xml b/core/res/res/drawable/accessibility_autoclick_scroll_right.xml
new file mode 100644
index 000000000000..bbd7b2a79459
--- /dev/null
+++ b/core/res/res/drawable/accessibility_autoclick_scroll_right.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/materialColorPrimary"
+ android:pathData="M20,12l-8,-8v5H4v6h8v5z"/>
+</vector>
diff --git a/core/res/res/drawable/accessibility_autoclick_scroll_up.xml b/core/res/res/drawable/accessibility_autoclick_scroll_up.xml
new file mode 100644
index 000000000000..2e2c245effa1
--- /dev/null
+++ b/core/res/res/drawable/accessibility_autoclick_scroll_up.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/materialColorPrimary"
+ android:pathData="M12,4L4,12h5v8h6v-8h5z"/>
+</vector>
diff --git a/core/res/res/layout/accessibility_autoclick_scroll_panel.xml b/core/res/res/layout/accessibility_autoclick_scroll_panel.xml
new file mode 100644
index 000000000000..1e093bbc7c35
--- /dev/null
+++ b/core/res/res/layout/accessibility_autoclick_scroll_panel.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/accessibility_autoclick_scroll_panel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:background="@drawable/accessibility_autoclick_type_panel_rounded_background"
+ android:orientation="vertical"
+ android:padding="16dp">
+
+ <!-- Up arrow -->
+ <LinearLayout
+ android:id="@+id/scroll_up_layout"
+ style="@style/AccessibilityAutoclickScrollPanelButtonLayoutStyle"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginBottom="@dimen/accessibility_autoclick_type_panel_button_spacing">
+ <ImageButton
+ android:id="@+id/scroll_up"
+ style="@style/AccessibilityAutoclickScrollPanelImageButtonStyle"
+ android:contentDescription="@string/accessibility_autoclick_scroll_up"
+ android:src="@drawable/accessibility_autoclick_scroll_up" />
+ </LinearLayout>
+
+ <!-- Middle row: Left, Exit, Right -->
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="horizontal">
+
+ <LinearLayout
+ android:id="@+id/scroll_left_layout"
+ style="@style/AccessibilityAutoclickScrollPanelButtonLayoutStyle"
+ android:layout_marginEnd="@dimen/accessibility_autoclick_type_panel_button_spacing">
+ <ImageButton
+ android:id="@+id/scroll_left"
+ style="@style/AccessibilityAutoclickScrollPanelImageButtonStyle"
+ android:contentDescription="@string/accessibility_autoclick_scroll_left"
+ android:src="@drawable/accessibility_autoclick_scroll_left" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/scroll_exit_layout"
+ style="@style/AccessibilityAutoclickScrollPanelButtonLayoutStyle"
+ android:layout_marginEnd="@dimen/accessibility_autoclick_type_panel_button_spacing">
+ <ImageButton
+ android:id="@+id/scroll_exit"
+ style="@style/AccessibilityAutoclickScrollPanelImageButtonStyle"
+ android:contentDescription="@string/accessibility_autoclick_scroll_exit"
+ android:src="@drawable/ic_close" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/scroll_right_layout"
+ style="@style/AccessibilityAutoclickScrollPanelButtonLayoutStyle">
+ <ImageButton
+ android:id="@+id/scroll_right"
+ style="@style/AccessibilityAutoclickScrollPanelImageButtonStyle"
+ android:contentDescription="@string/accessibility_autoclick_scroll_right"
+ android:src="@drawable/accessibility_autoclick_scroll_right" />
+ </LinearLayout>
+ </LinearLayout>
+
+ <!-- Down arrow -->
+ <LinearLayout
+ android:id="@+id/scroll_down_layout"
+ style="@style/AccessibilityAutoclickScrollPanelButtonLayoutStyle"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginTop="@dimen/accessibility_autoclick_type_panel_button_spacing">
+ <ImageButton
+ android:id="@+id/scroll_down"
+ style="@style/AccessibilityAutoclickScrollPanelImageButtonStyle"
+ android:contentDescription="@string/accessibility_autoclick_scroll_down"
+ android:src="@drawable/accessibility_autoclick_scroll_down" />
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/core/res/res/layout/notification_2025_expand_button.xml b/core/res/res/layout/notification_2025_expand_button.xml
index 1c367544c90a..8ba844a4868b 100644
--- a/core/res/res/layout/notification_2025_expand_button.xml
+++ b/core/res/res/layout/notification_2025_expand_button.xml
@@ -15,6 +15,7 @@
-->
<!-- extends FrameLayout -->
+<!-- Note: The button's padding may be dynamically adjusted in code -->
<com.android.internal.widget.NotificationExpandButton
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/expand_button"
diff --git a/core/res/res/layout/notification_2025_right_icon.xml b/core/res/res/layout/notification_2025_right_icon.xml
new file mode 100644
index 000000000000..24d381d10501
--- /dev/null
+++ b/core/res/res/layout/notification_2025_right_icon.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<!-- Large icon to be used in expanded notification layouts. -->
+<com.android.internal.widget.CachingIconView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/right_icon"
+ android:layout_width="@dimen/notification_right_icon_size"
+ android:layout_height="@dimen/notification_right_icon_size"
+ android:layout_gravity="top|end"
+ android:layout_marginEnd="@dimen/notification_2025_right_icon_expanded_margin_end"
+ android:layout_marginVertical="@dimen/notification_2025_right_icon_vertical_margin"
+ android:background="@drawable/notification_large_icon_outline"
+ android:clipToOutline="true"
+ android:importantForAccessibility="no"
+ android:scaleType="centerCrop"
+ android:maxDrawableWidth="@dimen/notification_right_icon_size"
+ android:maxDrawableHeight="@dimen/notification_right_icon_size"
+ />
diff --git a/core/res/res/layout/notification_2025_template_collapsed_base.xml b/core/res/res/layout/notification_2025_template_collapsed_base.xml
index d29b7af9e24e..63f32e3b3cd2 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_base.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_base.xml
@@ -66,14 +66,17 @@
android:orientation="horizontal"
>
+ <!--
+ We use a smaller vertical margin than usual, to allow the content of custom views to
+ grow up to 48dp height when needed in collapsed notifications.
+ -->
<LinearLayout
android:id="@+id/notification_headerless_view_column"
android:layout_width="0px"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
- android:layout_marginBottom="@dimen/notification_2025_margin"
- android:layout_marginTop="@dimen/notification_2025_margin"
+ android:layout_marginVertical="@dimen/notification_2025_reduced_margin"
android:orientation="vertical"
>
@@ -81,6 +84,7 @@
android:id="@+id/notification_top_line"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/notification_2025_additional_margin"
android:minHeight="@dimen/notification_2025_content_min_height"
android:clipChildren="false"
android:theme="@style/Theme.DeviceDefault.Notification"
@@ -118,17 +122,10 @@
<com.android.internal.widget.NotificationVanishingFrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/notification_2025_additional_margin"
android:minHeight="@dimen/notification_headerless_line_height"
>
- <!-- This is the simplest way to keep this text vertically centered without
- gravity="center_vertical" which causes jumpiness in expansion animations. -->
- <include
- layout="@layout/notification_2025_text"
- android:layout_width="match_parent"
- android:layout_height="@dimen/notification_text_height"
- android:layout_gravity="center_vertical"
- android:layout_marginTop="0dp"
- />
+ <include layout="@layout/notification_2025_text" />
</com.android.internal.widget.NotificationVanishingFrameLayout>
<include
@@ -146,9 +143,8 @@
android:layout_width="@dimen/notification_right_icon_size"
android:layout_height="@dimen/notification_right_icon_size"
android:layout_gravity="center_vertical|end"
- android:layout_marginTop="@dimen/notification_right_icon_headerless_margin"
- android:layout_marginBottom="@dimen/notification_right_icon_headerless_margin"
- android:layout_marginStart="@dimen/notification_right_icon_content_margin"
+ android:layout_marginVertical="@dimen/notification_2025_right_icon_vertical_margin"
+ android:layout_marginStart="@dimen/notification_2025_right_icon_content_margin"
android:background="@drawable/notification_large_icon_outline"
android:clipToOutline="true"
android:importantForAccessibility="no"
diff --git a/core/res/res/layout/notification_2025_template_collapsed_call.xml b/core/res/res/layout/notification_2025_template_collapsed_call.xml
index ee691e4d6894..fbea10d42b2b 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_call.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_call.xml
@@ -128,15 +128,7 @@
android:layout_height="wrap_content"
android:minHeight="@dimen/notification_headerless_line_height"
>
- <!-- This is the simplest way to keep this text vertically centered without
- gravity="center_vertical" which causes jumpiness in expansion animations. -->
- <include
- layout="@layout/notification_2025_text"
- android:layout_width="match_parent"
- android:layout_height="@dimen/notification_text_height"
- android:layout_gravity="center_vertical"
- android:layout_marginTop="0dp"
- />
+ <include layout="@layout/notification_2025_text" />
</com.android.internal.widget.NotificationVanishingFrameLayout>
</LinearLayout>
@@ -147,9 +139,8 @@
android:layout_width="@dimen/notification_right_icon_size"
android:layout_height="@dimen/notification_right_icon_size"
android:layout_gravity="center_vertical|end"
- android:layout_marginTop="@dimen/notification_right_icon_headerless_margin"
- android:layout_marginBottom="@dimen/notification_right_icon_headerless_margin"
- android:layout_marginStart="@dimen/notification_right_icon_content_margin"
+ android:layout_marginVertical="@dimen/notification_2025_right_icon_vertical_margin"
+ android:layout_marginStart="@dimen/notification_2025_right_icon_content_margin"
android:background="@drawable/notification_large_icon_outline"
android:clipToOutline="true"
android:importantForAccessibility="no"
diff --git a/core/res/res/layout/notification_2025_template_collapsed_conversation.xml b/core/res/res/layout/notification_2025_template_collapsed_conversation.xml
index f80411103501..a6fdcd95399e 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_conversation.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_conversation.xml
@@ -149,9 +149,9 @@
android:layout_width="@dimen/notification_right_icon_size"
android:layout_height="@dimen/notification_right_icon_size"
android:layout_gravity="center_vertical|end"
- android:layout_marginTop="@dimen/notification_right_icon_headerless_margin"
- android:layout_marginBottom="@dimen/notification_right_icon_headerless_margin"
- android:layout_marginStart="@dimen/notification_right_icon_content_margin"
+ android:layout_marginTop="@dimen/notification_2025_margin"
+ android:layout_marginBottom="@dimen/notification_2025_margin"
+ android:layout_marginStart="@dimen/notification_2025_right_icon_content_margin"
android:forceHasOverlappingRendering="false"
android:spacing="0dp"
android:clipChildren="false"
@@ -163,9 +163,8 @@
android:layout_width="@dimen/notification_right_icon_size"
android:layout_height="@dimen/notification_right_icon_size"
android:layout_gravity="center_vertical|end"
- android:layout_marginTop="@dimen/notification_right_icon_headerless_margin"
- android:layout_marginBottom="@dimen/notification_right_icon_headerless_margin"
- android:layout_marginStart="@dimen/notification_right_icon_content_margin"
+ android:layout_marginVertical="@dimen/notification_2025_right_icon_vertical_margin"
+ android:layout_marginStart="@dimen/notification_2025_right_icon_content_margin"
android:background="@drawable/notification_large_icon_outline"
android:clipToOutline="true"
android:importantForAccessibility="no"
diff --git a/core/res/res/layout/notification_2025_template_collapsed_media.xml b/core/res/res/layout/notification_2025_template_collapsed_media.xml
index 5beab508aecf..629af77b3dda 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_media.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_media.xml
@@ -68,14 +68,17 @@
android:orientation="horizontal"
>
+ <!--
+ We use a smaller vertical margin than usual, to allow the content of custom views to
+ grow up to 48dp height when needed in collapsed notifications.
+ -->
<LinearLayout
android:id="@+id/notification_headerless_view_column"
android:layout_width="0px"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
- android:layout_marginBottom="@dimen/notification_2025_margin"
- android:layout_marginTop="@dimen/notification_2025_margin"
+ android:layout_marginVertical="@dimen/notification_2025_reduced_margin"
android:orientation="vertical"
>
@@ -83,6 +86,7 @@
android:id="@+id/notification_top_line"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/notification_2025_additional_margin"
android:minHeight="@dimen/notification_headerless_line_height"
android:clipChildren="false"
android:theme="@style/Theme.DeviceDefault.Notification"
@@ -119,17 +123,10 @@
<com.android.internal.widget.NotificationVanishingFrameLayout
android:layout_width="match_parent"
+ android:layout_marginBottom="@dimen/notification_2025_additional_margin"
android:layout_height="@dimen/notification_headerless_line_height"
>
- <!-- This is the simplest way to keep this text vertically centered without
- gravity="center_vertical" which causes jumpiness in expansion animations. -->
- <include
- layout="@layout/notification_template_text"
- android:layout_width="match_parent"
- android:layout_height="@dimen/notification_text_height"
- android:layout_gravity="center_vertical"
- android:layout_marginTop="0dp"
- />
+ <include layout="@layout/notification_2025_text" />
</com.android.internal.widget.NotificationVanishingFrameLayout>
<include
@@ -147,9 +144,8 @@
android:layout_width="@dimen/notification_right_icon_size"
android:layout_height="@dimen/notification_right_icon_size"
android:layout_gravity="center_vertical|end"
- android:layout_marginTop="@dimen/notification_right_icon_headerless_margin"
- android:layout_marginBottom="@dimen/notification_right_icon_headerless_margin"
- android:layout_marginStart="@dimen/notification_right_icon_content_margin"
+ android:layout_marginVertical="@dimen/notification_2025_right_icon_vertical_margin"
+ android:layout_marginStart="@dimen/notification_2025_right_icon_content_margin"
android:background="@drawable/notification_large_icon_outline"
android:clipToOutline="true"
android:importantForAccessibility="no"
diff --git a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml
index d7c3263904d4..3716fa6825b3 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml
@@ -159,9 +159,9 @@
android:layout_width="@dimen/notification_right_icon_size"
android:layout_height="@dimen/notification_right_icon_size"
android:layout_gravity="center_vertical|end"
- android:layout_marginTop="@dimen/notification_right_icon_headerless_margin"
- android:layout_marginBottom="@dimen/notification_right_icon_headerless_margin"
- android:layout_marginStart="@dimen/notification_right_icon_content_margin"
+ android:layout_marginTop="@dimen/notification_2025_margin"
+ android:layout_marginBottom="@dimen/notification_2025_margin"
+ android:layout_marginStart="@dimen/notification_2025_right_icon_content_margin"
android:forceHasOverlappingRendering="false"
android:spacing="0dp"
android:clipChildren="false"
@@ -173,9 +173,8 @@
android:layout_width="@dimen/notification_right_icon_size"
android:layout_height="@dimen/notification_right_icon_size"
android:layout_gravity="center_vertical|end"
- android:layout_marginTop="@dimen/notification_right_icon_headerless_margin"
- android:layout_marginBottom="@dimen/notification_right_icon_headerless_margin"
- android:layout_marginStart="@dimen/notification_right_icon_content_margin"
+ android:layout_marginVertical="@dimen/notification_2025_right_icon_vertical_margin"
+ android:layout_marginStart="@dimen/notification_2025_right_icon_content_margin"
android:background="@drawable/notification_large_icon_outline"
android:clipToOutline="true"
android:importantForAccessibility="no"
diff --git a/core/res/res/layout/notification_2025_template_expanded_base.xml b/core/res/res/layout/notification_2025_template_expanded_base.xml
index e12db2783191..8d99e47c5386 100644
--- a/core/res/res/layout/notification_2025_template_expanded_base.xml
+++ b/core/res/res/layout/notification_2025_template_expanded_base.xml
@@ -63,7 +63,7 @@
/>
</LinearLayout>
- <include layout="@layout/notification_template_right_icon" />
+ <include layout="@layout/notification_2025_right_icon" />
</FrameLayout>
<ViewStub
diff --git a/core/res/res/layout/notification_2025_template_expanded_big_picture.xml b/core/res/res/layout/notification_2025_template_expanded_big_picture.xml
index fac9d1c47f41..e8e460d1b4ae 100644
--- a/core/res/res/layout/notification_2025_template_expanded_big_picture.xml
+++ b/core/res/res/layout/notification_2025_template_expanded_big_picture.xml
@@ -25,7 +25,7 @@
<include layout="@layout/notification_2025_template_header" />
- <include layout="@layout/notification_template_right_icon" />
+ <include layout="@layout/notification_2025_right_icon" />
<LinearLayout
android:id="@+id/notification_action_list_margin_target"
diff --git a/core/res/res/layout/notification_2025_template_expanded_big_text.xml b/core/res/res/layout/notification_2025_template_expanded_big_text.xml
index 4a807cb674c6..b68db7f0a638 100644
--- a/core/res/res/layout/notification_2025_template_expanded_big_text.xml
+++ b/core/res/res/layout/notification_2025_template_expanded_big_text.xml
@@ -91,5 +91,5 @@
<include layout="@layout/notification_material_action_list" />
</com.android.internal.widget.RemeasuringLinearLayout>
- <include layout="@layout/notification_template_right_icon" />
+ <include layout="@layout/notification_2025_right_icon" />
</FrameLayout>
diff --git a/core/res/res/layout/notification_2025_template_expanded_call.xml b/core/res/res/layout/notification_2025_template_expanded_call.xml
index bbc29664d594..7b45b55ba15b 100644
--- a/core/res/res/layout/notification_2025_template_expanded_call.xml
+++ b/core/res/res/layout/notification_2025_template_expanded_call.xml
@@ -68,6 +68,6 @@
</com.android.internal.widget.RemeasuringLinearLayout>
- <include layout="@layout/notification_template_right_icon" />
+ <include layout="@layout/notification_2025_right_icon" />
</com.android.internal.widget.CallLayout>
diff --git a/core/res/res/layout/notification_2025_template_expanded_conversation.xml b/core/res/res/layout/notification_2025_template_expanded_conversation.xml
index d7e8bb3b6da2..592785d53018 100644
--- a/core/res/res/layout/notification_2025_template_expanded_conversation.xml
+++ b/core/res/res/layout/notification_2025_template_expanded_conversation.xml
@@ -70,6 +70,6 @@
</com.android.internal.widget.RemeasuringLinearLayout>
- <include layout="@layout/notification_template_right_icon" />
+ <include layout="@layout/notification_2025_right_icon" />
</com.android.internal.widget.ConversationLayout>
diff --git a/core/res/res/layout/notification_2025_template_expanded_inbox.xml b/core/res/res/layout/notification_2025_template_expanded_inbox.xml
index ccab02e312cc..6459e1eab862 100644
--- a/core/res/res/layout/notification_2025_template_expanded_inbox.xml
+++ b/core/res/res/layout/notification_2025_template_expanded_inbox.xml
@@ -130,5 +130,5 @@
android:layout_marginTop="@dimen/notification_content_margin" />
<include layout="@layout/notification_material_action_list" />
</LinearLayout>
- <include layout="@layout/notification_template_right_icon" />
+ <include layout="@layout/notification_2025_right_icon" />
</FrameLayout>
diff --git a/core/res/res/layout/notification_2025_template_expanded_media.xml b/core/res/res/layout/notification_2025_template_expanded_media.xml
index e90ab792581f..801e339b3a92 100644
--- a/core/res/res/layout/notification_2025_template_expanded_media.xml
+++ b/core/res/res/layout/notification_2025_template_expanded_media.xml
@@ -99,6 +99,6 @@
</LinearLayout>
- <include layout="@layout/notification_template_right_icon" />
+ <include layout="@layout/notification_2025_right_icon" />
</com.android.internal.widget.MediaNotificationView>
diff --git a/core/res/res/layout/notification_2025_template_expanded_messaging.xml b/core/res/res/layout/notification_2025_template_expanded_messaging.xml
index 20abfee6a4b6..82c71527a291 100644
--- a/core/res/res/layout/notification_2025_template_expanded_messaging.xml
+++ b/core/res/res/layout/notification_2025_template_expanded_messaging.xml
@@ -70,6 +70,6 @@
</com.android.internal.widget.RemeasuringLinearLayout>
- <include layout="@layout/notification_template_right_icon" />
+ <include layout="@layout/notification_2025_right_icon" />
</com.android.internal.widget.MessagingLayout>
diff --git a/core/res/res/layout/notification_2025_template_expanded_progress.xml b/core/res/res/layout/notification_2025_template_expanded_progress.xml
index 87ded8975cb0..2ff252747fd2 100644
--- a/core/res/res/layout/notification_2025_template_expanded_progress.xml
+++ b/core/res/res/layout/notification_2025_template_expanded_progress.xml
@@ -99,7 +99,7 @@
</LinearLayout>
</LinearLayout>
- <include layout="@layout/notification_template_right_icon" />
+ <include layout="@layout/notification_2025_right_icon" />
</FrameLayout>
<ViewStub
diff --git a/core/res/res/layout/notification_2025_text.xml b/core/res/res/layout/notification_2025_text.xml
index 474f6d2099c6..a725de44b0bf 100644
--- a/core/res/res/layout/notification_2025_text.xml
+++ b/core/res/res/layout/notification_2025_text.xml
@@ -13,14 +13,16 @@
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
+<!-- Note that we prefer layout_gravity="center_vertical" over gravity="center_vertical", since the
+ latter can cause jumpiness in expansion animations. -->
<com.android.internal.widget.ImageFloatingTextView
xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/Widget.DeviceDefault.Notification.Text"
android:id="@+id/text"
android:layout_width="match_parent"
- android:layout_height="@dimen/notification_text_height"
- android:layout_gravity="top"
- android:layout_marginTop="@dimen/notification_text_margin_top"
+ android:layout_height="wrap_content"
+ android:minHeight="@dimen/notification_text_height"
+ android:layout_gravity="center_vertical"
android:ellipsize="end"
android:fadingEdge="horizontal"
android:gravity="top"
diff --git a/core/res/res/layout/notification_template_notification_progress_bar.xml b/core/res/res/layout/notification_template_notification_progress_bar.xml
index 35748962cfb2..8511d38718d1 100644
--- a/core/res/res/layout/notification_template_notification_progress_bar.xml
+++ b/core/res/res/layout/notification_template_notification_progress_bar.xml
@@ -20,4 +20,5 @@
android:layout_width="match_parent"
android:layout_height="@dimen/notification_progress_tracker_height"
style="@style/Widget.Material.Notification.NotificationProgressBar"
+ android:accessibilityLiveRegion="polite"
/>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 9773f557dfaa..7a38dce296de 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2812,6 +2812,20 @@
<integer name="config_dreamsBatteryLevelDrainCutoff">5</integer>
<!-- Limit of how long the device can remain unlocked due to attention checking. -->
<integer name="config_attentionMaximumExtension">900000</integer> <!-- 15 minutes. -->
+
+ <!-- Enables or disables the 'prevent screen timeout' feature, where when a user manually
+ undims the screen, the feature acquires a wakelock to prevent screen timeout.
+ false = disabled, true = enabled. Disabled by default. -->
+ <bool name="config_defaultPreventScreenTimeoutEnabled">false</bool>
+ <!-- Default value (in milliseconds) to prevent the screen timeout after user undims it
+ manually between screen dims, a sign the user is interacting with the device. -->
+ <integer name="config_defaultPreventScreenTimeoutForMillis">300000</integer> <!-- 5 minutes. -->
+ <!-- Default max duration (in milliseconds) of the time between undims to still consider them
+ consecutive. -->
+ <integer name="config_defaultMaxDurationBetweenUndimsMillis">600000</integer> <!-- 10 min. -->
+ <!-- Default number of user undims required to trigger preventing screen sleep. -->
+ <integer name="config_defaultUndimsRequired">2</integer>
+
<!-- Whether there is to be a chosen Dock User who is the only user allowed to dream. -->
<bool name="config_dreamsOnlyEnabledForDockUser">false</bool>
<!-- Whether dreams are disabled when ambient mode is suppressed. -->
@@ -4170,6 +4184,11 @@
<!-- Whether device supports double tap to wake -->
<bool name="config_supportDoubleTapWake">false</bool>
+ <!-- Whether device supports double tap to sleep. This will allow the user to enable/disable
+ double tap gestures in non-action areas in the lock screen and launcher workspace to go to
+ sleep. -->
+ <bool name="config_supportDoubleTapSleep">false</bool>
+
<!-- The RadioAccessFamilies supported by the device.
Empty is viewed as "all". Only used on devices which
don't support RIL_REQUEST_GET_RADIO_CAPABILITY
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index a1961aedf6b7..465e318511c6 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -390,6 +390,16 @@
<!-- The absolute size of the notification expand icon. -->
<dimen name="notification_header_expand_icon_size">56dp</dimen>
+ <!-- Margin to allow space for the expand button when showing the right icon in expanded -->
+ <!-- notifications. This is equal to notification_2025_expand_button_pill_width -->
+ <!-- + notification_2025_margin (end padding for expand button) -->
+ <!-- + notification_2025_expand_button_right_icon_spacing (space between pill and icon) -->
+ <dimen name="notification_2025_right_icon_expanded_margin_end">52dp</dimen>
+
+ <!-- The large icon has a smaller vertical margin than most other notification content, to -->
+ <!-- allow it to grow up to 48dp. -->
+ <dimen name="notification_2025_right_icon_vertical_margin">12dp</dimen>
+
<!-- the height of the expand button pill -->
<dimen name="notification_expand_button_pill_height">24dp</dimen>
@@ -411,15 +421,24 @@
<!-- the padding of the expand icon in the notification header -->
<dimen name="notification_2025_expand_button_horizontal_icon_padding">6dp</dimen>
- <!-- a smaller padding for the end of the expand button, for use when showing the number -->
+ <!-- smaller padding for the end of the expand icon, for use when showing the number -->
<dimen name="notification_2025_expand_button_reduced_end_padding">4dp</dimen>
+ <!-- the space needed between the expander pill and the large icon when visible -->
+ <dimen name="notification_2025_expand_button_right_icon_spacing">8dp</dimen>
+
<!-- the size of the notification close button -->
<dimen name="notification_close_button_size">16dp</dimen>
<!-- Margin for all notification content -->
<dimen name="notification_2025_margin">16dp</dimen>
+ <!-- A smaller version of the margin to be used when we need more space for the content -->
+ <dimen name="notification_2025_reduced_margin">12dp</dimen>
+
+ <!-- The difference between the usual margin and the reduced margin -->
+ <dimen name="notification_2025_additional_margin">4dp</dimen>
+
<!-- Vertical margin for the headerless notification content, when content has 1 line -->
<!-- 16 * 2 (margins) + 24 (1 line) = 56 (notification) -->
<dimen name="notification_headerless_margin_oneline">16dp</dimen>
@@ -438,7 +457,7 @@
<dimen name="notification_collapsed_height_with_summarization">156dp</dimen>
<!-- Max height of a collapsed (headerless) notification with one or two lines -->
- <!-- 16 * 2 (margins) + 48 (min content height) = 72 (notification) -->
+ <!-- 14 * 2 (reduced margins) + 48 (max collapsed content height) = 72 (notification) -->
<dimen name="notification_2025_min_height">72dp</dimen>
<!-- Height of a headerless notification with one line -->
@@ -729,6 +748,9 @@
<!-- The accessibility autoclick panel divider height -->
<dimen name="accessibility_autoclick_type_panel_divider_height">24dp</dimen>
+ <!-- The accessibility autoclick scroll panel button width and height -->
+ <dimen name="accessibility_autoclick_scroll_panel_button_size">36dp</dimen>
+
<!-- Margin around the various security views -->
<dimen name="keyguard_muliuser_selector_margin">8dp</dimen>
@@ -893,6 +915,8 @@
<dimen name="notification_right_icon_size">48dp</dimen>
<!-- The margin between the right icon and the content. -->
<dimen name="notification_right_icon_content_margin">12dp</dimen>
+ <!-- The margin between the right icon and the content. (2025 redesign version) -->
+ <dimen name="notification_2025_right_icon_content_margin">16dp</dimen>
<!-- The top and bottom margin of the right icon in the normal notification states -->
<dimen name="notification_right_icon_headerless_margin">20dp</dimen>
<!-- The top margin of the right icon in the "big" notification states -->
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index b3cc8837e0bf..d94d659446ac 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -6226,6 +6226,18 @@
<string name="accessibility_autoclick_pause">Pause</string>
<!-- Label for autoclick position button [CHAR LIMIT=NONE] -->
<string name="accessibility_autoclick_position">Position</string>
+ <!-- Label for autoclick scroll up button [CHAR LIMIT=NONE] -->
+ <string name="accessibility_autoclick_scroll_up">Scroll Up</string>
+ <!-- Label for autoclick scroll down button [CHAR LIMIT=NONE] -->
+ <string name="accessibility_autoclick_scroll_down">Scroll Down</string>
+ <!-- Label for autoclick scroll left button [CHAR LIMIT=NONE] -->
+ <string name="accessibility_autoclick_scroll_left">Scroll Left</string>
+ <!-- Label for autoclick scroll right button [CHAR LIMIT=NONE] -->
+ <string name="accessibility_autoclick_scroll_right">Scroll Right</string>
+ <!-- Label for autoclick scroll exit button [CHAR LIMIT=NONE] -->
+ <string name="accessibility_autoclick_scroll_exit">Exit Scroll Mode</string>
+ <!-- Label for autoclick scroll panel title [CHAR LIMIT=NONE] -->
+ <string name="accessibility_autoclick_scroll_panel_title">Scroll Panel</string>
<!-- Text to tell the user that a package has been forced by themselves in the RESTRICTED bucket. [CHAR LIMIT=NONE] -->
<string name="as_app_forced_to_restricted_bucket">
@@ -6698,6 +6710,8 @@ ul.</string>
<string name="profile_label_test">Test</string>
<!-- Communal profile label on a screen. This can be used as a tab label for this profile in tabbed views and can be used to represent the profile in sharing surfaces, etc. [CHAR LIMIT=20] -->
<string name="profile_label_communal">Communal</string>
+ <!-- Supervising profile label on a screen. This can be used as a tab label for this profile in tabbed views and can be used to represent the profile in sharing surfaces, etc. [CHAR LIMIT=20] -->
+ <string name="profile_label_supervising">Supervising</string>
<!-- Accessibility label for managed profile user type [CHAR LIMIT=30] -->
<string name="accessibility_label_managed_profile">Work profile</string>
diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml
index ee1edda838fd..1c3529fa7258 100644
--- a/core/res/res/values/styles.xml
+++ b/core/res/res/values/styles.xml
@@ -1761,6 +1761,19 @@ please see styles_device_defaults.xml.
<item name="android:tint">@color/materialColorPrimary</item>
</style>
+ <style name="AccessibilityAutoclickScrollPanelButtonLayoutStyle">
+ <item name="android:gravity">center</item>
+ <item name="android:layout_width">@dimen/accessibility_autoclick_scroll_panel_button_size </item>
+ <item name="android:layout_height">@dimen/accessibility_autoclick_scroll_panel_button_size</item>
+ </style>
+
+ <style name="AccessibilityAutoclickScrollPanelImageButtonStyle">
+ <item name="android:gravity">center</item>
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:scaleType">center</item>
+ </style>
+
<!--
TODO(b/309578419): Make activities go edge-to-edge properly and then remove this.
-->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 2b7261b62c64..46d18e3d3302 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1129,6 +1129,7 @@
<java-symbol type="string" name="profile_label_work_3" />
<java-symbol type="string" name="profile_label_test" />
<java-symbol type="string" name="profile_label_communal" />
+ <java-symbol type="string" name="profile_label_supervising" />
<java-symbol type="string" name="accessibility_label_managed_profile" />
<java-symbol type="string" name="accessibility_label_private_profile" />
<java-symbol type="string" name="accessibility_label_clone_profile" />
@@ -3141,6 +3142,7 @@
<java-symbol type="color" name="chooser_row_divider" />
<java-symbol type="layout" name="chooser_row_direct_share" />
<java-symbol type="bool" name="config_supportDoubleTapWake" />
+ <java-symbol type="bool" name="config_supportDoubleTapSleep" />
<java-symbol type="drawable" name="ic_perm_device_info" />
<java-symbol type="string" name="config_radio_access_family" />
<java-symbol type="string" name="notification_inbox_ellipsis" />
@@ -3267,6 +3269,8 @@
<java-symbol type="dimen" name="notification_2025_content_margin_start" />
<java-symbol type="dimen" name="notification_2025_expand_button_horizontal_icon_padding" />
<java-symbol type="dimen" name="notification_2025_expand_button_reduced_end_padding" />
+ <java-symbol type="dimen" name="notification_2025_expand_button_right_icon_spacing" />
+ <java-symbol type="dimen" name="notification_2025_right_icon_expanded_margin_end" />
<java-symbol type="dimen" name="notification_progress_margin_horizontal" />
<java-symbol type="dimen" name="notification_header_background_height" />
<java-symbol type="dimen" name="notification_header_touchable_height" />
@@ -3957,6 +3961,7 @@
<java-symbol type="dimen" name="notification_big_picture_max_width"/>
<java-symbol type="dimen" name="notification_right_icon_size"/>
<java-symbol type="dimen" name="notification_right_icon_content_margin"/>
+ <java-symbol type="dimen" name="notification_2025_right_icon_content_margin"/>
<java-symbol type="dimen" name="notification_actions_icon_drawable_size"/>
<java-symbol type="dimen" name="notification_custom_view_max_image_height"/>
<java-symbol type="dimen" name="notification_custom_view_max_image_width"/>
@@ -4442,6 +4447,11 @@
<!-- For Attention Service -->
<java-symbol type="integer" name="config_attentionMaximumExtension" />
+ <java-symbol type="bool" name="config_defaultPreventScreenTimeoutEnabled" />
+ <java-symbol type="integer" name="config_defaultPreventScreenTimeoutForMillis" />
+ <java-symbol type="integer" name="config_defaultMaxDurationBetweenUndimsMillis" />
+ <java-symbol type="integer" name="config_defaultUndimsRequired" />
+
<java-symbol type="string" name="config_incidentReportApproverPackage" />
<java-symbol type="array" name="config_restrictedImagesServices" />
@@ -5666,6 +5676,32 @@
<java-symbol type="drawable" name="accessibility_autoclick_resume" />
<java-symbol type="drawable" name="ic_accessibility_autoclick" />
+ <!-- Accessibility autoclick scroll panel related -->
+ <java-symbol type="layout" name="accessibility_autoclick_scroll_panel" />
+ <java-symbol type="dimen" name="accessibility_autoclick_scroll_panel_button_size" />
+ <java-symbol type="drawable" name="accessibility_autoclick_scroll_up" />
+ <java-symbol type="drawable" name="accessibility_autoclick_scroll_down" />
+ <java-symbol type="drawable" name="accessibility_autoclick_scroll_left" />
+ <java-symbol type="drawable" name="accessibility_autoclick_scroll_right" />
+ <java-symbol type="drawable" name="accessibility_autoclick_scroll_exit" />
+ <java-symbol type="string" name="accessibility_autoclick_scroll_up" />
+ <java-symbol type="string" name="accessibility_autoclick_scroll_down" />
+ <java-symbol type="string" name="accessibility_autoclick_scroll_left" />
+ <java-symbol type="string" name="accessibility_autoclick_scroll_right" />
+ <java-symbol type="string" name="accessibility_autoclick_scroll_exit" />
+ <java-symbol type="string" name="accessibility_autoclick_scroll_panel_title" />
+ <java-symbol type="id" name="accessibility_autoclick_scroll_panel" />
+ <java-symbol type="id" name="scroll_up_layout" />
+ <java-symbol type="id" name="scroll_down_layout" />
+ <java-symbol type="id" name="scroll_left_layout" />
+ <java-symbol type="id" name="scroll_right_layout" />
+ <java-symbol type="id" name="scroll_exit_layout" />
+ <java-symbol type="id" name="scroll_up" />
+ <java-symbol type="id" name="scroll_down" />
+ <java-symbol type="id" name="scroll_left" />
+ <java-symbol type="id" name="scroll_right" />
+ <java-symbol type="id" name="scroll_exit" />
+
<!-- For HapticFeedbackConstants configurability defined at HapticFeedbackCustomization -->
<java-symbol type="string" name="config_hapticFeedbackCustomizationFile" />
<java-symbol type="xml" name="haptic_feedback_customization" />
diff --git a/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java b/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java
index 33a46d0fde17..6d86bd209a3d 100644
--- a/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java
+++ b/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java
@@ -20,6 +20,8 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import android.os.Handler;
+import android.os.Looper;
import android.util.PollingCheck;
import android.view.View;
@@ -486,6 +488,42 @@ public class AnimatorSetCallsTest {
});
}
+ @Test
+ public void testCancelOnPendingEndListener() throws Throwable {
+ final CountDownLatch endLatch = new CountDownLatch(1);
+ final Handler handler = new Handler(Looper.getMainLooper());
+ final boolean[] endCalledRightAfterCancel = new boolean[2];
+ final AnimatorSet set = new AnimatorSet();
+ final ValueAnimatorTests.MyListener asListener = new ValueAnimatorTests.MyListener();
+ final ValueAnimatorTests.MyListener vaListener = new ValueAnimatorTests.MyListener();
+ final ValueAnimator va = new ValueAnimator();
+ va.setFloatValues(0f, 1f);
+ va.setDuration(30);
+ va.addUpdateListener(animation -> {
+ if (animation.getAnimatedFraction() == 1f) {
+ handler.post(() -> {
+ set.cancel();
+ endCalledRightAfterCancel[0] = vaListener.endCalled;
+ endCalledRightAfterCancel[1] = asListener.endCalled;
+ endLatch.countDown();
+ });
+ }
+ });
+ set.addListener(asListener);
+ va.addListener(vaListener);
+ set.play(va);
+
+ ValueAnimator.setPostNotifyEndListenerEnabled(true);
+ try {
+ handler.post(set::start);
+ assertTrue(endLatch.await(1, TimeUnit.SECONDS));
+ assertTrue(endCalledRightAfterCancel[0]);
+ assertTrue(endCalledRightAfterCancel[1]);
+ } finally {
+ ValueAnimator.setPostNotifyEndListenerEnabled(false);
+ }
+ }
+
private void waitForOnUiThread(PollingCheck.PollingCheckCondition condition) {
final boolean[] value = new boolean[1];
PollingCheck.waitFor(() -> {
diff --git a/core/tests/coretests/src/android/animation/ValueAnimatorTests.java b/core/tests/coretests/src/android/animation/ValueAnimatorTests.java
index 04698465e971..a55909f0c193 100644
--- a/core/tests/coretests/src/android/animation/ValueAnimatorTests.java
+++ b/core/tests/coretests/src/android/animation/ValueAnimatorTests.java
@@ -922,6 +922,36 @@ public class ValueAnimatorTests {
}
@Test
+ public void testCancelOnPendingEndListener() throws Throwable {
+ final CountDownLatch endLatch = new CountDownLatch(1);
+ final Handler handler = new Handler(Looper.getMainLooper());
+ final boolean[] endCalledRightAfterCancel = new boolean[1];
+ final MyListener listener = new MyListener();
+ final ValueAnimator va = new ValueAnimator();
+ va.setFloatValues(0f, 1f);
+ va.setDuration(30);
+ va.addUpdateListener(animation -> {
+ if (animation.getAnimatedFraction() == 1f) {
+ handler.post(() -> {
+ va.cancel();
+ endCalledRightAfterCancel[0] = listener.endCalled;
+ endLatch.countDown();
+ });
+ }
+ });
+ va.addListener(listener);
+
+ ValueAnimator.setPostNotifyEndListenerEnabled(true);
+ try {
+ handler.post(va::start);
+ assertThat(endLatch.await(1, TimeUnit.SECONDS)).isTrue();
+ assertThat(endCalledRightAfterCancel[0]).isTrue();
+ } finally {
+ ValueAnimator.setPostNotifyEndListenerEnabled(false);
+ }
+ }
+
+ @Test
public void testZeroDuration() throws Throwable {
// Run two animators with zero duration, with one running forward and the other one
// backward. Check that the animations start and finish with the correct end fractions.
@@ -1182,6 +1212,7 @@ public class ValueAnimatorTests {
assertEquals(A1_START_VALUE, a1.getAnimatedValue());
});
}
+
class MyUpdateListener implements ValueAnimator.AnimatorUpdateListener {
boolean wasRunning = false;
long firstRunningFrameTime = -1;
@@ -1207,7 +1238,7 @@ public class ValueAnimatorTests {
}
}
- class MyListener implements Animator.AnimatorListener {
+ static class MyListener implements Animator.AnimatorListener {
boolean startCalled = false;
boolean cancelCalled = false;
boolean endCalled = false;
diff --git a/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java b/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java
index ee4761b9d024..dccbf4036b3e 100644
--- a/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java
+++ b/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java
@@ -245,6 +245,12 @@ public class RegisteredServicesCacheTest extends AndroidTestCase {
SERVICE_INTERFACE, SERVICE_META_DATA, ATTRIBUTES_NAME, new TestSerializer());
}
+ TestServicesCache(Injector<TestServiceType> injector,
+ XmlSerializerAndParser<TestServiceType> serializerAndParser) {
+ super(injector, SERVICE_INTERFACE, SERVICE_META_DATA, ATTRIBUTES_NAME,
+ serializerAndParser);
+ }
+
@Override
public TestServiceType parseServiceAttributes(Resources res, String packageName,
AttributeSet attrs) {
diff --git a/core/tests/coretests/src/android/text/LayoutTest.java b/core/tests/coretests/src/android/text/LayoutTest.java
index 9e78af57b470..11ec9f8e1912 100644
--- a/core/tests/coretests/src/android/text/LayoutTest.java
+++ b/core/tests/coretests/src/android/text/LayoutTest.java
@@ -16,6 +16,9 @@
package android.text;
+import static android.text.Layout.HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_FACTOR;
+import static android.text.Layout.HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_MIN_DP;
+
import static com.android.graphics.hwui.flags.Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT;
import static org.junit.Assert.assertArrayEquals;
@@ -1073,6 +1076,68 @@ public class LayoutTest {
}
}
+ @Test
+ @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
+ public void highContrastTextEnabled_testRoundedRectSize_belowMinimum_usesMinimumValue() {
+ mTextPaint.setColor(Color.BLACK);
+ mTextPaint.setTextSize(8); // Value chosen so that N * RADIUS_FACTOR < RADIUS_MIN_DP
+ Layout layout = new StaticLayout("Test text", mTextPaint, mWidth,
+ mAlign, mSpacingMult, mSpacingAdd, /* includePad= */ false);
+
+ MockCanvas c = new MockCanvas(/* width= */ 256, /* height= */ 256);
+ c.setHighContrastTextEnabled(true);
+ layout.draw(
+ c,
+ /* highlightPaths= */ null,
+ /* highlightPaints= */ null,
+ /* selectionPath= */ null,
+ /* selectionPaint= */ null,
+ /* cursorOffsetVertical= */ 0
+ );
+
+ final float expectedRoundedRectSize =
+ mTextPaint.density * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_MIN_DP;
+ List<MockCanvas.DrawCommand> drawCommands = c.getDrawCommands();
+ for (int i = 0; i < drawCommands.size(); i++) {
+ MockCanvas.DrawCommand drawCommand = drawCommands.get(i);
+ if (drawCommand.rect != null) {
+ expect.that(drawCommand.rX).isEqualTo(expectedRoundedRectSize);
+ expect.that(drawCommand.rY).isEqualTo(expectedRoundedRectSize);
+ }
+ }
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
+ public void highContrastTextEnabled_testRoundedRectSize_aboveMinimum_usesScaledValue() {
+ mTextPaint.setColor(Color.BLACK);
+ mTextPaint.setTextSize(50); // Value chosen so that N * RADIUS_FACTOR > RADIUS_MIN_DP
+ Layout layout = new StaticLayout("Test text", mTextPaint, mWidth,
+ mAlign, mSpacingMult, mSpacingAdd, /* includePad= */ false);
+
+ MockCanvas c = new MockCanvas(/* width= */ 256, /* height= */ 256);
+ c.setHighContrastTextEnabled(true);
+ layout.draw(
+ c,
+ /* highlightPaths= */ null,
+ /* highlightPaints= */ null,
+ /* selectionPath= */ null,
+ /* selectionPaint= */ null,
+ /* cursorOffsetVertical= */ 0
+ );
+
+ final float expectedRoundedRectSize =
+ mTextPaint.getTextSize() * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_FACTOR;
+ List<MockCanvas.DrawCommand> drawCommands = c.getDrawCommands();
+ for (int i = 0; i < drawCommands.size(); i++) {
+ MockCanvas.DrawCommand drawCommand = drawCommands.get(i);
+ if (drawCommand.rect != null) {
+ expect.that(drawCommand.rX).isEqualTo(expectedRoundedRectSize);
+ expect.that(drawCommand.rY).isEqualTo(expectedRoundedRectSize);
+ }
+ }
+ }
+
private int removeAlpha(int color) {
return Color.rgb(
Color.red(color),
@@ -1087,6 +1152,8 @@ public class LayoutTest {
public final String text;
public final float x;
public final float y;
+ public final float rX;
+ public final float rY;
public final Path path;
public final RectF rect;
public final Paint paint;
@@ -1098,6 +1165,8 @@ public class LayoutTest {
this.paint = new Paint(paint);
path = null;
rect = null;
+ this.rX = 0;
+ this.rY = 0;
}
DrawCommand(Path path, Paint paint) {
@@ -1107,15 +1176,19 @@ public class LayoutTest {
x = 0;
text = null;
rect = null;
+ this.rX = 0;
+ this.rY = 0;
}
- DrawCommand(RectF rect, Paint paint) {
+ DrawCommand(RectF rect, Paint paint, float rX, float rY) {
this.rect = new RectF(rect);
this.paint = new Paint(paint);
path = null;
y = 0;
x = 0;
text = null;
+ this.rX = rX;
+ this.rY = rY;
}
@Override
@@ -1189,12 +1262,12 @@ public class LayoutTest {
@Override
public void drawRect(RectF rect, Paint p) {
- mDrawCommands.add(new DrawCommand(rect, p));
+ mDrawCommands.add(new DrawCommand(rect, p, 0, 0));
}
@Override
public void drawRoundRect(@NonNull RectF rect, float rx, float ry, @NonNull Paint paint) {
- mDrawCommands.add(new DrawCommand(rect, paint));
+ mDrawCommands.add(new DrawCommand(rect, paint, rx, ry));
}
List<DrawCommand> getDrawCommands() {
diff --git a/core/tests/coretests/src/android/util/ArrayMapTest.java b/core/tests/coretests/src/android/util/ArrayMapTest.java
index 711ff9458e11..c7efe6f93f0d 100644
--- a/core/tests/coretests/src/android/util/ArrayMapTest.java
+++ b/core/tests/coretests/src/android/util/ArrayMapTest.java
@@ -14,14 +14,18 @@
package android.util;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.junit.Assert.fail;
import android.platform.test.annotations.IgnoreUnderRavenwood;
+import android.platform.test.annotations.Presubmit;
import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -32,6 +36,7 @@ import java.util.ConcurrentModificationException;
* Unit tests for ArrayMap that don't belong in CTS.
*/
@LargeTest
+@Presubmit
@RunWith(AndroidJUnit4.class)
public class ArrayMapTest {
private static final String TAG = "ArrayMapTest";
@@ -51,6 +56,7 @@ public class ArrayMapTest {
* @throws Exception
*/
@Test
+ @Ignore("Failing; b/399137661")
@IgnoreUnderRavenwood(reason = "Long test runtime")
public void testConcurrentModificationException() throws Exception {
final int TEST_LEN_MS = 5000;
@@ -118,4 +124,49 @@ public class ArrayMapTest {
}
}
}
+
+ @Test
+ public void testToString() {
+ map.put("1", "One");
+ map.put("2", "Two");
+ map.put("3", "Five");
+ map.put("3", "Three");
+
+ assertThat(map.toString()).isEqualTo("{1=One, 2=Two, 3=Three}");
+ assertThat(map.keySet().toString()).isEqualTo("[1, 2, 3]");
+ assertThat(map.values().toString()).isEqualTo("[One, Two, Three]");
+ }
+
+ @Test
+ public void testToStringRecursive() {
+ ArrayMap<Object, Object> weird = new ArrayMap<>();
+ weird.put("1", weird);
+
+ assertThat(weird.toString()).isEqualTo("{1=(this Map)}");
+ assertThat(weird.keySet().toString()).isEqualTo("[1]");
+ assertThat(weird.values().toString()).isEqualTo("[{1=(this Map)}]");
+ }
+
+ @Test
+ public void testToStringRecursiveKeySet() {
+ ArrayMap<Object, Object> weird = new ArrayMap<>();
+ weird.put("1", weird.keySet());
+ weird.put(weird.keySet(), "2");
+
+ assertThat(weird.toString()).isEqualTo("{1=[1, (this KeySet)], [1, (this KeySet)]=2}");
+ assertThat(weird.keySet().toString()).isEqualTo("[1, (this KeySet)]");
+ assertThat(weird.values().toString()).isEqualTo("[[1, (this KeySet)], 2]");
+ }
+
+ @Test
+ public void testToStringRecursiveValues() {
+ ArrayMap<Object, Object> weird = new ArrayMap<>();
+ weird.put("1", weird.values());
+ weird.put(weird.values(), "2");
+
+ assertThat(weird.toString()).isEqualTo(
+ "{1=[(this ValuesCollection), 2], [(this ValuesCollection), 2]=2}");
+ assertThat(weird.keySet().toString()).isEqualTo("[1, [(this ValuesCollection), 2]]");
+ assertThat(weird.values().toString()).isEqualTo("[(this ValuesCollection), 2]");
+ }
}
diff --git a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
index 4d6c30ebbe2b..215c1623a530 100644
--- a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
+++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
@@ -541,24 +541,30 @@ public class WindowOnBackInvokedDispatcherTest {
throws RemoteException, InterruptedException {
// Setup a callback that unregisters itself after the gesture is finished but before the
// progress is animated back to 0f
- final AtomicBoolean unregisterOnProgressUpdate = new AtomicBoolean(false);
+ final AtomicBoolean unregisterOnNextCallbackInvocation = new AtomicBoolean(false);
final AtomicInteger onBackInvokedCalled = new AtomicInteger(0);
final CountDownLatch onBackCancelledCalled = new CountDownLatch(1);
OnBackAnimationCallback onBackAnimationCallback = new OnBackAnimationCallback() {
@Override
public void onBackProgressed(@NonNull BackEvent backEvent) {
- if (unregisterOnProgressUpdate.get()) {
+ if (unregisterOnNextCallbackInvocation.getAndSet(false)) {
mDispatcher.unregisterOnBackInvokedCallback(this);
}
}
@Override
public void onBackInvoked() {
+ if (unregisterOnNextCallbackInvocation.getAndSet(false)) {
+ mDispatcher.unregisterOnBackInvokedCallback(this);
+ }
onBackInvokedCalled.getAndIncrement();
}
@Override
public void onBackCancelled() {
+ if (unregisterOnNextCallbackInvocation.getAndSet(false)) {
+ mDispatcher.unregisterOnBackInvokedCallback(this);
+ }
onBackCancelledCalled.countDown();
}
};
@@ -572,7 +578,7 @@ public class WindowOnBackInvokedDispatcherTest {
// simulate back gesture finished and onBackCancelled() called, which starts the progress
// animation back to 0f. On the first progress emission, the callback will unregister itself
- unregisterOnProgressUpdate.set(true);
+ unregisterOnNextCallbackInvocation.set(true);
callbackInfo.getCallback().onBackCancelled();
waitForIdle();
onBackCancelledCalled.await(1000, TimeUnit.MILLISECONDS);
diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserActivityWorkProfileTest.java b/core/tests/coretests/src/com/android/internal/app/ChooserActivityWorkProfileTest.java
index db69cf2397fc..02a296892c53 100644
--- a/core/tests/coretests/src/com/android/internal/app/ChooserActivityWorkProfileTest.java
+++ b/core/tests/coretests/src/com/android/internal/app/ChooserActivityWorkProfileTest.java
@@ -72,7 +72,8 @@ public class ChooserActivityWorkProfileTest {
private static final UserHandle PERSONAL_USER_HANDLE = InstrumentationRegistry
.getInstrumentation().getTargetContext().getUser();
- private static final UserHandle WORK_USER_HANDLE = UserHandle.of(10);
+ private static final UserHandle WORK_USER_HANDLE =
+ UserHandle.of(PERSONAL_USER_HANDLE.getIdentifier() + 1);
@Rule
public ActivityTestRule<ChooserWrapperActivity> mActivityRule =
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index e141f70d8abb..da25da1256b0 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -54,6 +54,8 @@ import static androidx.window.extensions.embedding.SplitPresenter.sanitizeBounds
import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSplit;
import static androidx.window.extensions.embedding.TaskFragmentContainer.OverlayContainerRestoreParams;
+import static com.android.window.flags.Flags.activityEmbeddingDelayTaskFragmentFinishForActivityLaunch;
+
import android.annotation.CallbackExecutor;
import android.app.Activity;
import android.app.ActivityClient;
@@ -815,11 +817,17 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
.setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE);
mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
} else if (!container.isWaitingActivityAppear()) {
- // Do not finish the container before the expected activity appear until
- // timeout.
- mTransactionManager.getCurrentTransactionRecord()
- .setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE);
- mPresenter.cleanupContainer(wct, container, true /* shouldFinishDependent */);
+ if (activityEmbeddingDelayTaskFragmentFinishForActivityLaunch()
+ && container.hasActivityLaunchHint()) {
+ // If we have recently attempted to launch a new activity into this
+ // TaskFragment, we schedule delayed cleanup. If the new activity appears in
+ // this TaskFragment, we no longer need to finish the TaskFragment.
+ container.scheduleDelayedTaskFragmentCleanup();
+ } else {
+ mTransactionManager.getCurrentTransactionRecord()
+ .setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE);
+ mPresenter.cleanupContainer(wct, container, true /* shouldFinishDependent */);
+ }
}
} else if (wasInPip && isInPip) {
// No update until exit PIP.
@@ -3164,6 +3172,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
// TODO(b/229680885): skip override launching TaskFragment token by split-rule
options.putBinder(KEY_LAUNCH_TASK_FRAGMENT_TOKEN,
launchedInTaskFragment.getTaskFragmentToken());
+ if (activityEmbeddingDelayTaskFragmentFinishForActivityLaunch()) {
+ launchedInTaskFragment.setActivityLaunchHint();
+ }
mCurrentIntent = intent;
} else {
transactionRecord.abort();
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
index b3e003e7ad95..6fa855e8b6d6 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -18,6 +18,8 @@ package androidx.window.extensions.embedding;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static com.android.window.flags.Flags.activityEmbeddingDelayTaskFragmentFinishForActivityLaunch;
+
import android.app.Activity;
import android.app.ActivityThread;
import android.app.WindowConfiguration.WindowingMode;
@@ -53,6 +55,8 @@ import java.util.Objects;
class TaskFragmentContainer {
private static final int APPEAR_EMPTY_TIMEOUT_MS = 3000;
+ private static final int DELAYED_TASK_FRAGMENT_CLEANUP_TIMEOUT_MS = 500;
+
/** Parcelable data of this TaskFragmentContainer. */
@NonNull
private final ParcelableTaskFragmentContainerData mParcelableData;
@@ -165,6 +169,18 @@ class TaskFragmentContainer {
*/
private boolean mLastDimOnTask;
+ /** The timestamp of the latest pending activity launch attempt. 0 means no pending launch. */
+ private long mLastActivityLaunchTimestampMs = 0;
+
+ /**
+ * The scheduled runnable for delayed TaskFragment cleanup. This is used when the TaskFragment
+ * becomes empty, but we expect a new activity to appear in it soon.
+ *
+ * It should be {@code null} when not scheduled.
+ */
+ @Nullable
+ private Runnable mDelayedTaskFragmentCleanupRunnable;
+
/**
* Creates a container with an existing activity that will be re-parented to it in a window
* container transaction.
@@ -540,6 +556,10 @@ class TaskFragmentContainer {
mAppearEmptyTimeout = null;
}
+ if (activityEmbeddingDelayTaskFragmentFinishForActivityLaunch()) {
+ clearActivityLaunchHintIfNecessary(mInfo, info);
+ }
+
mHasCrossProcessActivities = false;
mInfo = info;
if (mInfo == null || mInfo.isEmpty()) {
@@ -1064,6 +1084,89 @@ class TaskFragmentContainer {
return isOverlay() && mParcelableData.mAssociatedActivityToken != null;
}
+ /**
+ * Indicates whether there is possibly a pending activity launching into this TaskFragment.
+ *
+ * This should only be used as a hint because we cannot reliably determine if the new activity
+ * is going to appear into this TaskFragment.
+ *
+ * TODO(b/293800510) improve activity launch tracking in TaskFragment.
+ */
+ boolean hasActivityLaunchHint() {
+ if (mLastActivityLaunchTimestampMs == 0) {
+ return false;
+ }
+ if (System.currentTimeMillis() > mLastActivityLaunchTimestampMs + APPEAR_EMPTY_TIMEOUT_MS) {
+ // The hint has expired after APPEAR_EMPTY_TIMEOUT_MS.
+ mLastActivityLaunchTimestampMs = 0;
+ return false;
+ }
+ return true;
+ }
+
+ /** Records the latest activity launch attempt. */
+ void setActivityLaunchHint() {
+ mLastActivityLaunchTimestampMs = System.currentTimeMillis();
+ }
+
+ /**
+ * If we get a new info showing that the TaskFragment has more activities than the previous
+ * info, we clear the new activity launch hint.
+ *
+ * Note that this is not a reliable way and cannot cover situations when the attempted
+ * activity launch did not cause TaskFragment info activity count changes, such as trampoline
+ * launches or single top launches.
+ *
+ * TODO(b/293800510) improve activity launch tracking in TaskFragment.
+ */
+ private void clearActivityLaunchHintIfNecessary(
+ @Nullable TaskFragmentInfo oldInfo, @NonNull TaskFragmentInfo newInfo) {
+ final int previousActivityCount = oldInfo == null ? 0 : oldInfo.getRunningActivityCount();
+ if (newInfo.getRunningActivityCount() > previousActivityCount) {
+ mLastActivityLaunchTimestampMs = 0;
+ cancelDelayedTaskFragmentCleanup();
+ }
+ }
+
+ /**
+ * Schedules delayed TaskFragment cleanup due to pending activity launch. The scheduled cleanup
+ * will be canceled if a new activity appears in this TaskFragment.
+ */
+ void scheduleDelayedTaskFragmentCleanup() {
+ if (mDelayedTaskFragmentCleanupRunnable != null) {
+ // Remove the previous callback if there is already one scheduled.
+ mController.getHandler().removeCallbacks(mDelayedTaskFragmentCleanupRunnable);
+ }
+ mDelayedTaskFragmentCleanupRunnable = new Runnable() {
+ @Override
+ public void run() {
+ synchronized (mController.mLock) {
+ if (mDelayedTaskFragmentCleanupRunnable != this) {
+ // The scheduled cleanup runnable has been canceled or rescheduled, so
+ // skipping.
+ return;
+ }
+ if (isEmpty()) {
+ mLastActivityLaunchTimestampMs = 0;
+ mController.onTaskFragmentAppearEmptyTimeout(
+ TaskFragmentContainer.this);
+ }
+ mDelayedTaskFragmentCleanupRunnable = null;
+ }
+ }
+ };
+ mController.getHandler().postDelayed(
+ mDelayedTaskFragmentCleanupRunnable, DELAYED_TASK_FRAGMENT_CLEANUP_TIMEOUT_MS);
+ }
+
+ private void cancelDelayedTaskFragmentCleanup() {
+ if (mDelayedTaskFragmentCleanupRunnable == null) {
+ return;
+ }
+ mController.getHandler().removeCallbacks(mDelayedTaskFragmentCleanupRunnable);
+ mDelayedTaskFragmentCleanupRunnable = null;
+ }
+
@Override
public String toString() {
return toString(true /* includeContainersToFinishOnExit */);
diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
index 1e72d64397d7..ab2804626361 100644
--- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig
+++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
@@ -194,3 +194,13 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "enable_gsf"
+ namespace: "multitasking"
+ description: "Applies GSF font styles to multitasking."
+ bug: "400534660"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryScreenshotTest.kt b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryScreenshotTest.kt
index 24f43d347163..5777cb0cc8c5 100644
--- a/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryScreenshotTest.kt
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryScreenshotTest.kt
@@ -94,6 +94,7 @@ class DragZoneFactoryScreenshotTest(private val param: Param) {
private val splitScreenModeName =
when (splitScreenMode) {
+ SplitScreenMode.UNSUPPORTED -> "_split_unsupported"
SplitScreenMode.NONE -> ""
SplitScreenMode.SPLIT_50_50 -> "_split_50_50"
SplitScreenMode.SPLIT_10_90 -> "_split_10_90"
diff --git a/libs/WindowManager/Shell/multivalentTests/Android.bp b/libs/WindowManager/Shell/multivalentTests/Android.bp
index 03076c0940a4..50666911e313 100644
--- a/libs/WindowManager/Shell/multivalentTests/Android.bp
+++ b/libs/WindowManager/Shell/multivalentTests/Android.bp
@@ -51,6 +51,7 @@ android_robolectric_test {
"androidx.test.ext.junit",
"mockito-robolectric-prebuilt",
"mockito-kotlin2",
+ "platform-parametric-runner-lib",
"truth",
"flag-junit-base",
"flag-junit",
@@ -74,6 +75,7 @@ android_test {
"frameworks-base-testutils",
"mockito-kotlin2",
"mockito-target-extended-minus-junit4",
+ "platform-parametric-runner-lib",
"truth",
"platform-test-annotations",
"platform-test-rules",
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleExpandedViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleExpandedViewTest.kt
new file mode 100644
index 000000000000..bdfaef2c6960
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleExpandedViewTest.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.bubbles
+
+import android.content.ComponentName
+import android.content.Context
+import android.platform.test.flag.junit.FlagsParameterization
+import android.platform.test.flag.junit.SetFlagsRule
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.Flags
+import com.android.wm.shell.taskview.TaskView
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors.directExecutor
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+
+/** Tests for [BubbleExpandedView] */
+@SmallTest
+@RunWith(ParameterizedAndroidJunit4::class)
+class BubbleExpandedViewTest(flags: FlagsParameterization) {
+
+ @get:Rule
+ val setFlagsRule = SetFlagsRule(flags)
+
+ private val context = ApplicationProvider.getApplicationContext<Context>()
+ private val componentName = ComponentName(context, "TestClass")
+
+ @Test
+ fun getTaskId_onTaskCreated_returnsCorrectTaskId() {
+ val bubbleTaskView = BubbleTaskView(mock<TaskView>(), directExecutor())
+ val expandedView = BubbleExpandedView(context).apply {
+ initialize(
+ mock<BubbleExpandedViewManager>(),
+ mock<BubbleStackView>(),
+ mock<BubblePositioner>(),
+ false /* isOverflow */,
+ bubbleTaskView,
+ )
+ setAnimating(true) // Skips setContentVisibility for testing.
+ }
+
+ bubbleTaskView.listener.onTaskCreated(123, componentName)
+
+ assertThat(expandedView.getTaskId()).isEqualTo(123)
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams() = FlagsParameterization.allCombinationsOf(
+ Flags.FLAG_ENABLE_BUBBLE_TASK_VIEW_LISTENER,
+ )
+ }
+}
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt
index 9087da34d259..636ff669d6b4 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt
@@ -266,8 +266,6 @@ class BubbleTaskViewListenerTest {
optionsCaptor.capture(),
any())
- assertThat((intentCaptor.lastValue.flags
- and Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0).isTrue()
assertThat(optionsCaptor.lastValue.launchedFromBubble).isFalse() // chat only
assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isFalse() // chat only
assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue()
@@ -295,8 +293,6 @@ class BubbleTaskViewListenerTest {
optionsCaptor.capture(),
any())
- assertThat((intentCaptor.lastValue.flags
- and Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0).isTrue()
assertThat(optionsCaptor.lastValue.launchedFromBubble).isFalse() // chat only
assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isFalse() // chat only
assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue()
@@ -324,8 +320,6 @@ class BubbleTaskViewListenerTest {
optionsCaptor.capture(),
any())
- assertThat((intentCaptor.lastValue.flags
- and Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0).isTrue()
assertThat(optionsCaptor.lastValue.launchedFromBubble).isFalse() // chat only
assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isFalse() // chat only
assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue()
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index 2179128df300..5ef83826840b 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -226,7 +226,7 @@
<string name="windowing_app_handle_education_tooltip">The app menu can be found here</string>
<!-- App handle education tooltip text for tooltip pointing to windowing image button -->
- <string name="windowing_desktop_mode_image_button_education_tooltip">Enter desktop view to open multiple apps together</string>
+ <string name="windowing_desktop_mode_image_button_education_tooltip">Enter desktop windowing to open multiple apps together</string>
<!-- App handle education tooltip text for tooltip pointing to app chip -->
<string name="windowing_desktop_mode_exit_education_tooltip">Return to full screen anytime from the app menu</string>
@@ -293,7 +293,7 @@
<!-- Accessibility text for the handle fullscreen button [CHAR LIMIT=NONE] -->
<string name="fullscreen_text">Fullscreen</string>
<!-- Accessibility text for the handle desktop button [CHAR LIMIT=NONE] -->
- <string name="desktop_text">Desktop View</string>
+ <string name="desktop_text">Desktop windowing</string>
<!-- Accessibility text for the handle split screen button [CHAR LIMIT=NONE] -->
<string name="split_screen_text">Split Screen</string>
<!-- Accessibility text for the handle more options button [CHAR LIMIT=NONE] -->
@@ -319,7 +319,7 @@
<!-- Accessibility text for the handle menu close menu button [CHAR LIMIT=NONE] -->
<string name="collapse_menu_text">Close Menu</string>
<!-- Accessibility text for the App Header's App Chip [CHAR LIMIT=NONE] -->
- <string name="desktop_mode_app_header_chip_text"><xliff:g id="app_name" example="Chrome">%1$s</xliff:g> (Desktop View)</string>
+ <string name="desktop_mode_app_header_chip_text"><xliff:g id="app_name" example="Chrome">%1$s</xliff:g> (Desktop windowing)</string>
<!-- Maximize menu maximize button string. -->
<string name="desktop_mode_maximize_menu_maximize_text">Maximize Screen</string>
<!-- Maximize menu snap buttons string. -->
@@ -348,7 +348,7 @@
<!-- Accessibility action replacement for caption handle app chip buttons [CHAR LIMIT=NONE] -->
<string name="app_handle_chip_accessibility_announce">Open Menu</string>
<!-- Accessibility action replacement for caption handle menu buttons [CHAR LIMIT=NONE] -->
- <string name="app_handle_menu_accessibility_announce">Enter <xliff:g id="windowing_mode" example="Desktop View">%1$s</xliff:g></string>
+ <string name="app_handle_menu_accessibility_announce">Enter <xliff:g id="windowing_mode" example="Desktop windowing">%1$s</xliff:g></string>
<!-- Accessibility action replacement for maximize menu enter snap left button [CHAR LIMIT=NONE] -->
<string name="maximize_menu_talkback_action_snap_left_text">Resize window to left</string>
<!-- Accessibility action replacement for maximize menu enter snap right button [CHAR LIMIT=NONE] -->
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellSharedConstants.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellSharedConstants.java
index 01d2201a5a0c..8bcbd2a3fc9f 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellSharedConstants.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellSharedConstants.java
@@ -22,4 +22,11 @@ package com.android.wm.shell.shared;
public class ShellSharedConstants {
public static final String KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION =
"extra_shell_can_hand_off_animation";
+
+ /**
+ * Defines the max screen width or height in dp for a device to be considered a small tablet.
+ *
+ * @see android.view.WindowManager#LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP
+ */
+ public static final int SMALL_TABLET_MAX_EDGE_DP = 960;
}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TypefaceUtils.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TypefaceUtils.kt
new file mode 100644
index 000000000000..9bf56b075112
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TypefaceUtils.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.shared
+
+import android.graphics.Typeface
+import android.widget.TextView
+import com.android.wm.shell.Flags
+
+/**
+ * Utility class to apply a specified typeface to a [TextView].
+ *
+ * This class provides a method, [setTypeface],
+ * to easily set a pre-defined font family and style to a given [TextView].
+ */
+class TypefaceUtils {
+
+ enum class FontFamily(val value: String) {
+ GSF_DISPLAY_LARGE("variable-display-large"),
+ GSF_DISPLAY_MEDIUM("variable-display-medium"),
+ GSF_DISPLAY_SMALL("variable-display-small"),
+ GSF_HEADLINE_LARGE("variable-headline-large"),
+ GSF_HEADLINE_MEDIUM("variable-headline-medium"),
+ GSF_HEADLINE_SMALL("variable-headline-small"),
+ GSF_TITLE_LARGE("variable-title-large"),
+ GSF_TITLE_MEDIUM("variable-title-medium"),
+ GSF_TITLE_SMALL("variable-title-small"),
+ GSF_LABEL_LARGE("variable-label-large"),
+ GSF_LABEL_MEDIUM("variable-label-medium"),
+ GSF_LABEL_SMALL("variable-label-small"),
+ GSF_BODY_LARGE("variable-body-large"),
+ GSF_BODY_MEDIUM("variable-body-medium"),
+ GSF_BODY_SMALL("variable-body-small"),
+ GSF_DISPLAY_LARGE_EMPHASIZED("variable-display-large-emphasized"),
+ GSF_DISPLAY_MEDIUM_EMPHASIZED("variable-display-medium-emphasized"),
+ GSF_DISPLAY_SMALL_EMPHASIZED("variable-display-small-emphasized"),
+ GSF_HEADLINE_LARGE_EMPHASIZED("variable-headline-large-emphasized"),
+ GSF_HEADLINE_MEDIUM_EMPHASIZED("variable-headline-medium-emphasized"),
+ GSF_HEADLINE_SMALL_EMPHASIZED("variable-headline-small-emphasized"),
+ GSF_TITLE_LARGE_EMPHASIZED("variable-title-large-emphasized"),
+ GSF_TITLE_MEDIUM_EMPHASIZED("variable-title-medium-emphasized"),
+ GSF_TITLE_SMALL_EMPHASIZED("variable-title-small-emphasized"),
+ GSF_LABEL_LARGE_EMPHASIZED("variable-label-large-emphasized"),
+ GSF_LABEL_MEDIUM_EMPHASIZED("variable-label-medium-emphasized"),
+ GSF_LABEL_SMALL_EMPHASIZED("variable-label-small-emphasized"),
+ GSF_BODY_LARGE_EMPHASIZED("variable-body-large-emphasized"),
+ GSF_BODY_MEDIUM_EMPHASIZED("variable-body-medium-emphasized"),
+ GSF_BODY_SMALL_EMPHASIZED("variable-body-small-emphasized"),
+ }
+
+ companion object {
+ /**
+ * Sets the typeface of the provided [textView] to the specified [fontFamily] and [fontStyle].
+ *
+ * The typeface is only applied to the [TextView] when [Flags.enableGsf] is `true`.
+ * If [Flags.enableGsf] is `false`, this method has no effect.
+ *
+ * @param textView The [TextView] to which the typeface should be applied. If `null`, this method does nothing.
+ * @param fontFamily The desired [FontFamily] for the [TextView].
+ * @param fontStyle The desired font style (e.g., [Typeface.NORMAL], [Typeface.BOLD], [Typeface.ITALIC]). Defaults to [Typeface.NORMAL].
+ */
+ @JvmStatic
+ @JvmOverloads
+ fun setTypeface(
+ textView: TextView?,
+ fontFamily: FontFamily,
+ fontStyle: Int = Typeface.NORMAL,
+ ) {
+ if (!Flags.enableGsf()) return
+ textView?.typeface = Typeface.create(fontFamily.name, fontStyle)
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DeviceConfig.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DeviceConfig.kt
index 1b7c9c282304..ad2671b8135d 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DeviceConfig.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DeviceConfig.kt
@@ -26,6 +26,8 @@ import android.view.WindowInsets
import android.view.WindowManager
import kotlin.math.max
+import com.android.wm.shell.shared.ShellSharedConstants.SMALL_TABLET_MAX_EDGE_DP
+
/** Contains device configuration used for positioning bubbles on the screen. */
data class DeviceConfig(
val isLargeScreen: Boolean,
@@ -38,7 +40,6 @@ data class DeviceConfig(
companion object {
private const val LARGE_SCREEN_MIN_EDGE_DP = 600
- private const val SMALL_TABLET_MAX_EDGE_DP = 960
@JvmStatic
fun create(context: Context, windowManager: WindowManager): DeviceConfig {
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt
index 362a5c5c3750..afeaf70c9d62 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt
@@ -303,6 +303,7 @@ class DragZoneFactory(
val isVerticalSplit = deviceConfig.isSmallTablet == deviceConfig.isLandscape
return if (isVerticalSplit) {
when (splitScreenModeChecker.getSplitScreenMode()) {
+ SplitScreenMode.UNSUPPORTED -> emptyList()
SplitScreenMode.SPLIT_50_50,
SplitScreenMode.NONE ->
listOf(
@@ -360,6 +361,7 @@ class DragZoneFactory(
}
} else {
when (splitScreenModeChecker.getSplitScreenMode()) {
+ SplitScreenMode.UNSUPPORTED -> emptyList()
SplitScreenMode.SPLIT_50_50,
SplitScreenMode.NONE ->
listOf(
@@ -453,6 +455,7 @@ class DragZoneFactory(
// vertical split drag zones are aligned with the full screen drag zone width
val splitZoneLeft = windowBounds.right / 2 - fullScreenDragZoneWidth / 2
when (splitScreenModeChecker.getSplitScreenMode()) {
+ SplitScreenMode.UNSUPPORTED -> emptyList()
SplitScreenMode.SPLIT_50_50,
SplitScreenMode.NONE ->
listOf(
@@ -560,7 +563,8 @@ class DragZoneFactory(
NONE,
SPLIT_50_50,
SPLIT_10_90,
- SPLIT_90_10
+ SPLIT_90_10,
+ UNSUPPORTED
}
fun getSplitScreenMode(): SplitScreenMode
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetView.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetView.kt
index 2bb6cf4ec3aa..73277310ffe4 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetView.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetView.kt
@@ -20,6 +20,7 @@ import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
+import android.util.TypedValue
import android.view.View
import com.android.wm.shell.shared.R
@@ -37,14 +38,21 @@ class DropTargetView(context: Context) : View(context) {
private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = context.getColor(com.android.internal.R.color.materialColorPrimaryContainer)
style = Paint.Style.STROKE
- strokeWidth = context.resources.getDimensionPixelSize(R.dimen.drop_target_stroke).toFloat()
+ strokeWidth = 1.dpToPx()
}
- private val cornerRadius = context.resources.getDimensionPixelSize(
- R.dimen.drop_target_radius).toFloat()
+ private val cornerRadius = 28.dpToPx()
private val rect = RectF(0f, 0f, 0f, 0f)
+ // TODO b/396539130: Use shared xml resources once we can access them in launcher
+ private fun Int.dpToPx() =
+ TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ this.toFloat(),
+ context.resources.displayMetrics
+ )
+
override fun onDraw(canvas: Canvas) {
canvas.save()
canvas.drawRoundRect(rect, cornerRadius, cornerRadius, rectPaint)
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/multiinstance/ManageWindowsViewContainer.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/multiinstance/ManageWindowsViewContainer.kt
index 0954b52ff151..ac54ac7a9d99 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/multiinstance/ManageWindowsViewContainer.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/multiinstance/ManageWindowsViewContainer.kt
@@ -48,12 +48,14 @@ abstract class ManageWindowsViewContainer(
lateinit var menuView: ManageWindowsView
/** Creates the base menu view and fills it with icon views. */
- fun createMenu(snapshotList: List<Pair<Int, TaskSnapshot>>,
+ fun createMenu(snapshotList: List<Pair<Int, TaskSnapshot?>>,
onIconClickListener: ((Int) -> Unit),
onOutsideClickListener: (() -> Unit)): ManageWindowsView {
- val bitmapList = snapshotList.map { (index, snapshot) ->
- index to Bitmap.wrapHardwareBuffer(snapshot.hardwareBuffer, snapshot.colorSpace)
- }
+ val bitmapList = snapshotList
+ .filter { it.second != null }
+ .map { (index, snapshot) ->
+ index to Bitmap.wrapHardwareBuffer(snapshot!!.hardwareBuffer, snapshot.colorSpace)
+ }
return createAndShowMenuView(
bitmapList,
onIconClickListener,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index 80b65d025399..53dede6bd227 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -28,7 +28,7 @@ import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_HOME;
-import static com.android.systemui.Flags.predictiveBackDelayTransition;
+import static com.android.systemui.Flags.predictiveBackDelayWmTransition;
import static com.android.window.flags.Flags.unifyBackNavigationTransition;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;
@@ -433,7 +433,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
public void onThresholdCrossed() {
mThresholdCrossed = true;
BackTouchTracker activeTracker = getActiveTracker();
- if (predictiveBackDelayTransition() && activeTracker != null && mActiveCallback == null
+ if (predictiveBackDelayWmTransition() && activeTracker != null && mActiveCallback == null
&& mBackGestureStarted) {
startBackNavigation(activeTracker);
}
@@ -494,12 +494,12 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
if (swipeEdge == EDGE_NONE) {
// start animation immediately for non-gestural sources (without ACTION_MOVE
// events)
- if (!predictiveBackDelayTransition()) {
+ if (!predictiveBackDelayWmTransition()) {
mThresholdCrossed = true;
}
mPointersPilfered = true;
onGestureStarted(touchX, touchY, swipeEdge);
- if (predictiveBackDelayTransition()) {
+ if (predictiveBackDelayWmTransition()) {
onThresholdCrossed();
}
mShouldStartOnNextMoveEvent = false;
@@ -555,7 +555,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
mPostCommitAnimationInProgress = false;
mShellExecutor.removeCallbacks(mAnimationTimeoutRunnable);
startSystemAnimation();
- } else if (!predictiveBackDelayTransition()) {
+ } else if (!predictiveBackDelayWmTransition()) {
startBackNavigation(touchTracker);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index d9489287ff42..4f30a052de80 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -375,7 +375,7 @@ public class Bubble implements BubbleViewProvider {
user,
icon,
BubbleType.TYPE_APP,
- getAppBubbleKeyForApp(intent.getPackage(), user),
+ getAppBubbleKeyForApp(ComponentUtils.getPackageName(intent), user),
mainExecutor, bgExecutor);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 50e2f4d52bf2..3e95a0b1100f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -1604,7 +1604,7 @@ public class BubbleController implements ConfigurationChangeListener,
@Nullable BubbleTransitions.DragData dragData) {
if (!BubbleAnythingFlagHelper.enableBubbleToFullscreen()) return;
Bubble b = mBubbleData.getOrCreateBubble(taskInfo); // Removes from overflow
- ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - intent=%s", taskInfo.taskId);
+ ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - taskId=%s", taskInfo.taskId);
BubbleBarLocation location = null;
if (dragData != null) {
location =
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
index 2c2451cab999..8ac9230c36c3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
@@ -238,7 +238,6 @@ public class BubbleExpandedView extends LinearLayout {
mContext.createContextAsUser(
mBubble.getUser(), Context.CONTEXT_RESTRICTED);
Intent fillInIntent = new Intent();
- fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
PendingIntent pi = PendingIntent.getActivity(
context,
/* requestCode= */ 0,
@@ -467,6 +466,11 @@ public class BubbleExpandedView extends LinearLayout {
new BubbleTaskViewListener.Callback() {
@Override
public void onTaskCreated() {
+ // The taskId is saved to use for removeTask,
+ // preventing appearance in recent tasks.
+ mTaskId = ((BubbleTaskViewListener) mCurrentTaskViewListener)
+ .getTaskId();
+
setContentVisibility(true);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java
index 63d713495177..9c20e3af9ab4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java
@@ -130,7 +130,6 @@ public class BubbleTaskViewListener implements TaskView.Listener {
mContext.createContextAsUser(
mBubble.getUser(), Context.CONTEXT_RESTRICTED);
Intent fillInIntent = new Intent();
- fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
// First try get pending intent from the bubble
PendingIntent pi = mBubble.getPendingIntent();
if (pi == null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java
index e7e7be9cdf6b..51a5b12edb84 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java
@@ -159,19 +159,22 @@ public class BubbleTransitions {
private final WindowContainerTransaction mPendingWct;
private final boolean mReleasedOnLeft;
private final float mTaskScale;
+ private final float mCornerRadius;
private final PointF mDragPosition;
/**
* @param releasedOnLeft true if the bubble was released in the left drop target
* @param taskScale the scale of the task when it was dragged to bubble
+ * @param cornerRadius the corner radius of the task when it was dragged to bubble
* @param dragPosition the position of the task when it was dragged to bubble
* @param wct pending operations to be applied when finishing the drag
*/
- public DragData(boolean releasedOnLeft, float taskScale, @Nullable PointF dragPosition,
- @Nullable WindowContainerTransaction wct) {
+ public DragData(boolean releasedOnLeft, float taskScale, float cornerRadius,
+ @Nullable PointF dragPosition, @Nullable WindowContainerTransaction wct) {
mPendingWct = wct;
mReleasedOnLeft = releasedOnLeft;
mTaskScale = taskScale;
+ mCornerRadius = cornerRadius;
mDragPosition = dragPosition != null ? dragPosition : new PointF(0, 0);
}
@@ -198,6 +201,13 @@ public class BubbleTransitions {
}
/**
+ * @return the corner radius of the task when it was dragged to bubble
+ */
+ public float getCornerRadius() {
+ return mCornerRadius;
+ }
+
+ /**
* @return position of the task when it was dragged to bubble
*/
public PointF getDragPosition() {
@@ -362,6 +372,7 @@ public class BubbleTransitions {
(int) mDragData.getDragPosition().y);
startTransaction.setScale(mSnapshot, mDragData.getTaskScale(),
mDragData.getTaskScale());
+ startTransaction.setCornerRadius(mSnapshot, mDragData.getCornerRadius());
}
// Now update state (and talk to launcher) in parallel with snapshot stuff
@@ -377,12 +388,6 @@ public class BubbleTransitions {
startTransaction.setPosition(mSnapshot, left, top);
startTransaction.setLayer(mSnapshot, Integer.MAX_VALUE);
- BubbleBarExpandedView bbev = mBubble.getBubbleBarExpandedView();
- if (bbev != null) {
- // Corners get reset during the animation. Add them back
- startTransaction.setCornerRadius(mSnapshot, bbev.getRestingCornerRadius());
- }
-
startTransaction.apply();
mTaskViewTransitions.onExternalDone(transition);
@@ -634,7 +639,7 @@ public class BubbleTransitions {
@Override
public void continueCollapse() {
mBubble.cleanupTaskView();
- if (mTaskLeash == null) return;
+ if (mTaskLeash == null || !mTaskLeash.isValid()) return;
SurfaceControl.Transaction t = new SurfaceControl.Transaction();
t.reparent(mTaskLeash, mRootLeash);
t.apply();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
index 6c840f020f90..bdb21f246359 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
@@ -447,9 +447,9 @@ public class BubbleBarLayerView extends FrameLayout
bubble.cleanupViews(!inTransition);
endAction.run();
};
- if (mBubbleData.getBubbles().isEmpty()) {
- // we're removing the last bubble. collapse the expanded view and cleanup bubble views
- // at the end.
+ if (mBubbleData.getBubbles().isEmpty() || inTransition) {
+ // If we are removing the last bubble or removing the current bubble via transition,
+ // collapse the expanded view and clean up bubbles at the end.
collapse(cleanUp);
} else {
cleanUp.run();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ComponentUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ComponentUtils.kt
index 0d0bc9be72b3..9fefb515a539 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ComponentUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ComponentUtils.kt
@@ -25,7 +25,8 @@ import com.android.wm.shell.ShellTaskOrganizer
object ComponentUtils {
/** Retrieves the package name from an [Intent]. */
@JvmStatic
- fun getPackageName(intent: Intent?): String? = intent?.component?.packageName
+ fun getPackageName(intent: Intent?): String? =
+ intent?.component?.packageName ?: intent?.`package`
/** Retrieves the package name from a [PendingIntent]. */
@JvmStatic
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/HomeIntentProvider.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/HomeIntentProvider.kt
new file mode 100644
index 000000000000..8751b65dce94
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/HomeIntentProvider.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common
+
+import android.app.ActivityManager
+import android.app.ActivityOptions
+import android.app.PendingIntent
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.content.Context
+import android.content.Intent
+import android.os.UserHandle
+import android.view.Display.DEFAULT_DISPLAY
+import android.window.WindowContainerTransaction
+import com.android.window.flags.Flags
+
+/** Creates home intent **/
+class HomeIntentProvider(
+ private val context: Context,
+) {
+ fun addLaunchHomePendingIntent(
+ wct: WindowContainerTransaction, displayId: Int, userId: Int? = null
+ ) {
+ val userHandle =
+ if (userId != null) UserHandle.of(userId) else UserHandle.of(ActivityManager.getCurrentUser())
+
+ val launchHomeIntent = Intent(Intent.ACTION_MAIN).apply {
+ if (displayId != DEFAULT_DISPLAY) {
+ addCategory(Intent.CATEGORY_SECONDARY_HOME)
+ } else {
+ addCategory(Intent.CATEGORY_HOME)
+ }
+ }
+ val options = ActivityOptions.makeBasic().apply {
+ launchWindowingMode = WINDOWING_MODE_FULLSCREEN
+ pendingIntentBackgroundActivityStartMode =
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
+ if (Flags.enablePerDisplayDesktopWallpaperActivity()) {
+ launchDisplayId = displayId
+ }
+ }
+ val pendingIntent = PendingIntent.getActivityAsUser(
+ context,
+ /* requestCode= */ 0,
+ launchHomeIntent,
+ PendingIntent.FLAG_IMMUTABLE,
+ /* options= */ null,
+ userHandle,
+ )
+ wct.sendPendingIntent(pendingIntent, launchHomeIntent, options.toBundle())
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java
index 214e6ad455a1..aeef211ae3f3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java
@@ -143,6 +143,9 @@ public class PipBoundsState {
*/
public final Rect mCachedLauncherShelfHeightKeepClearArea = new Rect();
+ private final List<OnPipComponentChangedListener> mOnPipComponentChangedListeners =
+ new ArrayList<>();
+
// the size of the current bounds relative to the max size spec
private float mBoundsScale;
@@ -156,9 +159,7 @@ public class PipBoundsState {
// Update the relative proportion of the bounds compared to max possible size. Max size
// spec takes the aspect ratio of the bounds into account, so both width and height
// scale by the same factor.
- addPipExclusionBoundsChangeCallback((bounds) -> {
- updateBoundsScale();
- });
+ addPipExclusionBoundsChangeCallback((bounds) -> updateBoundsScale());
}
/** Reloads the resources. */
@@ -341,11 +342,14 @@ public class PipBoundsState {
/** Set the last {@link ComponentName} to enter PIP mode. */
public void setLastPipComponentName(@Nullable ComponentName lastPipComponentName) {
final boolean changed = !Objects.equals(mLastPipComponentName, lastPipComponentName);
+ if (!changed) return;
+ clearReentryState();
+ setHasUserResizedPip(false);
+ setHasUserMovedPip(false);
+ final ComponentName oldComponentName = mLastPipComponentName;
mLastPipComponentName = lastPipComponentName;
- if (changed) {
- clearReentryState();
- setHasUserResizedPip(false);
- setHasUserMovedPip(false);
+ for (OnPipComponentChangedListener listener : mOnPipComponentChangedListeners) {
+ listener.onPipComponentChanged(oldComponentName, mLastPipComponentName);
}
}
@@ -616,6 +620,21 @@ public class PipBoundsState {
}
}
+ /** Adds callback to listen on component change. */
+ public void addOnPipComponentChangedListener(@NonNull OnPipComponentChangedListener listener) {
+ if (!mOnPipComponentChangedListeners.contains(listener)) {
+ mOnPipComponentChangedListeners.add(listener);
+ }
+ }
+
+ /** Removes callback to listen on component change. */
+ public void removeOnPipComponentChangedListener(
+ @NonNull OnPipComponentChangedListener listener) {
+ if (mOnPipComponentChangedListeners.contains(listener)) {
+ mOnPipComponentChangedListeners.remove(listener);
+ }
+ }
+
public LauncherState getLauncherState() {
return mLauncherState;
}
@@ -695,7 +714,7 @@ public class PipBoundsState {
* Represents the state of pip to potentially restore upon reentry.
*/
@VisibleForTesting
- public static final class PipReentryState {
+ static final class PipReentryState {
private static final String TAG = PipReentryState.class.getSimpleName();
private final float mSnapFraction;
@@ -722,6 +741,22 @@ public class PipBoundsState {
}
}
+ /**
+ * Listener interface for PiP component change, i.e. the app in pip mode changes
+ * TODO: Move this out of PipBoundsState once pip1 is deprecated.
+ */
+ public interface OnPipComponentChangedListener {
+ /**
+ * Callback when the component in pip mode changes.
+ * @param oldPipComponent previous component in pip mode,
+ * {@code null} if this is the very first time PiP appears.
+ * @param newPipComponent new component that enters pip mode.
+ */
+ void onPipComponentChanged(
+ @Nullable ComponentName oldPipComponent,
+ @NonNull ComponentName newPipComponent);
+ }
+
/** Dumps internal state. */
public void dump(PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
index e20a3d839def..fec1f56c76bb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
@@ -1466,11 +1466,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
// Freeze the configuration size with offset to prevent app get a configuration
// changed or relaunch. This is required to make sure client apps will calculate
// insets properly after layout shifted.
- if (mTargetYOffset == 0) {
- mSplitLayoutHandler.setLayoutOffsetTarget(0, 0, SplitLayout.this);
- } else {
- mSplitLayoutHandler.setLayoutOffsetTarget(0, mTargetYOffset, SplitLayout.this);
- }
+ mSplitLayoutHandler.setLayoutOffsetTarget(0, mTargetYOffset, SplitLayout.this);
}
// Make {@link DividerView} non-interactive while IME showing in split mode. Listen to
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
index 9b11e4ab16fa..4413c8715c0d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
@@ -18,6 +18,8 @@ package com.android.wm.shell.compatui;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static com.android.wm.shell.compatui.impl.CompatUIRequestsKt.DISPLAY_COMPAT_SHOW_RESTART_DIALOG;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.TaskInfo;
@@ -53,7 +55,9 @@ import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.compatui.api.CompatUIEvent;
import com.android.wm.shell.compatui.api.CompatUIHandler;
import com.android.wm.shell.compatui.api.CompatUIInfo;
+import com.android.wm.shell.compatui.api.CompatUIRequest;
import com.android.wm.shell.compatui.impl.CompatUIEvents.SizeCompatRestartButtonClicked;
+import com.android.wm.shell.compatui.impl.CompatUIRequests;
import com.android.wm.shell.desktopmode.DesktopUserRepositories;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import com.android.wm.shell.sysui.KeyguardChangeListener;
@@ -245,6 +249,21 @@ public class CompatUIController implements OnDisplaysChangedListener,
mCallback = callback;
}
+ @Override
+ public void sendCompatUIRequest(CompatUIRequest compatUIRequest) {
+ switch(compatUIRequest.getRequestId()) {
+ case DISPLAY_COMPAT_SHOW_RESTART_DIALOG:
+ handleDisplayCompatShowRestartDialog(compatUIRequest.asType());
+ break;
+ default:
+ }
+ }
+
+ private void handleDisplayCompatShowRestartDialog(
+ CompatUIRequests.DisplayCompatShowRestartDialog request) {
+ onRestartButtonClicked(new Pair<>(request.getTaskInfo(), request.getTaskListener()));
+ }
+
/**
* Called when the Task info changed. Creates and updates the compat UI if there is an
* activity in size compat, or removes the UI if there is no size compat activity.
@@ -254,13 +273,17 @@ public class CompatUIController implements OnDisplaysChangedListener,
public void onCompatInfoChanged(@NonNull CompatUIInfo compatUIInfo) {
final TaskInfo taskInfo = compatUIInfo.getTaskInfo();
final ShellTaskOrganizer.TaskListener taskListener = compatUIInfo.getListener();
- if (taskInfo != null && !taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat()) {
+ final boolean isInDisplayCompatMode =
+ taskInfo.appCompatTaskInfo.isRestartMenuEnabledForDisplayMove();
+ if (taskInfo != null && !taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat()
+ && !isInDisplayCompatMode) {
mSetOfTaskIdsShowingRestartDialog.remove(taskInfo.taskId);
}
mIsInDesktopMode = isInDesktopMode(taskInfo);
// We close all the Compat UI educations in case TaskInfo has no configuration or
// TaskListener or in desktop mode.
- if (taskInfo.configuration == null || taskListener == null || mIsInDesktopMode) {
+ if (taskInfo.configuration == null || taskListener == null
+ || (mIsInDesktopMode && !isInDisplayCompatMode)) {
// Null token means the current foreground activity is not in compatibility mode.
removeLayouts(taskInfo.taskId);
return;
@@ -552,8 +575,11 @@ public class CompatUIController implements OnDisplaysChangedListener,
@Nullable ShellTaskOrganizer.TaskListener taskListener) {
RestartDialogWindowManager layout =
mTaskIdToRestartDialogWindowManagerMap.get(taskInfo.taskId);
+ final boolean isInNonDisplayCompatDesktopMode = mIsInDesktopMode
+ && !taskInfo.appCompatTaskInfo.isRestartMenuEnabledForDisplayMove();
if (layout != null) {
- if (layout.needsToBeRecreated(taskInfo, taskListener) || mIsInDesktopMode) {
+ if (layout.needsToBeRecreated(taskInfo, taskListener)
+ || isInNonDisplayCompatDesktopMode) {
mTaskIdToRestartDialogWindowManagerMap.remove(taskInfo.taskId);
layout.release();
} else {
@@ -568,8 +594,9 @@ public class CompatUIController implements OnDisplaysChangedListener,
return;
}
}
- if (mIsInDesktopMode) {
- // Return if in desktop mode.
+ if (isInNonDisplayCompatDesktopMode) {
+ // No restart dialog can be shown in desktop mode unless the task is in display compat
+ // mode.
return;
}
// Create a new UI layout.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIHandler.kt
index 817e554b550e..f71f8099f29f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIHandler.kt
@@ -28,6 +28,11 @@ interface CompatUIHandler {
fun onCompatInfoChanged(compatUIInfo: CompatUIInfo)
/**
+ * Invoked when another component in Shell requests a CompatUI state change.
+ */
+ fun sendCompatUIRequest(compatUIRequest: CompatUIRequest)
+
+ /**
* Optional reference to the object responsible to send {@link CompatUIEvent}
*/
fun setCallback(compatUIEventSender: Consumer<CompatUIEvent>?)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIRequest.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIRequest.kt
new file mode 100644
index 000000000000..069fd9b062a6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIRequest.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.compatui.api
+
+/**
+ * Abstraction for all the possible Compat UI Component requests.
+ */
+interface CompatUIRequest {
+ /**
+ * Unique request identifier
+ */
+ val requestId: Int
+
+ @Suppress("UNCHECKED_CAST")
+ fun <T : CompatUIRequest> asType(): T? = this as? T
+
+ fun <T : CompatUIRequest> asType(clazz: Class<T>): T? {
+ return if (clazz.isInstance(this)) clazz.cast(this) else null
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIRequests.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIRequests.kt
new file mode 100644
index 000000000000..da4fc99491dc
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIRequests.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.compatui.impl
+
+import android.app.TaskInfo
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.compatui.api.CompatUIRequest
+
+internal const val DISPLAY_COMPAT_SHOW_RESTART_DIALOG = 0
+
+/**
+ * All the {@link CompatUIRequest} the Compat UI Framework can handle
+ */
+sealed class CompatUIRequests(override val requestId: Int) : CompatUIRequest {
+ /** Sent when the restart handle menu is clicked, and a restart dialog is requested. */
+ data class DisplayCompatShowRestartDialog(val taskInfo: TaskInfo,
+ val taskListener: ShellTaskOrganizer.TaskListener) :
+ CompatUIRequests(DISPLAY_COMPAT_SHOW_RESTART_DIALOG)
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt
index 02db85a4f99d..7dcb16c10097 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt
@@ -23,6 +23,7 @@ import com.android.wm.shell.compatui.api.CompatUIEvent
import com.android.wm.shell.compatui.api.CompatUIHandler
import com.android.wm.shell.compatui.api.CompatUIInfo
import com.android.wm.shell.compatui.api.CompatUIRepository
+import com.android.wm.shell.compatui.api.CompatUIRequest
import com.android.wm.shell.compatui.api.CompatUIState
import java.util.function.Consumer
@@ -102,4 +103,6 @@ class DefaultCompatUIHandler(
override fun setCallback(compatUIEventSender: Consumer<CompatUIEvent>?) {
this.compatUIEventSender = compatUIEventSender
}
+
+ override fun sendCompatUIRequest(compatUIRequest: CompatUIRequest) {}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/crashhandling/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/crashhandling/OWNERS
new file mode 100644
index 000000000000..007528e2e054
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/crashhandling/OWNERS
@@ -0,0 +1,2 @@
+# WM shell sub-module crash handling owners
+uysalorhan@google.com \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/crashhandling/ShellCrashHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/crashhandling/ShellCrashHandler.kt
new file mode 100644
index 000000000000..2e34d043cbe1
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/crashhandling/ShellCrashHandler.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.crashhandling
+
+import android.app.WindowConfiguration
+import android.content.Context
+import android.view.Display.DEFAULT_DISPLAY
+import android.window.DesktopExperienceFlags
+import android.window.WindowContainerTransaction
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.common.HomeIntentProvider
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.android.wm.shell.sysui.ShellInit
+
+/** [ShellCrashHandler] for shell to use when it's being initialized. Currently it only restores
+ * the home task to top.
+ **/
+class ShellCrashHandler(
+ private val context: Context,
+ private val shellTaskOrganizer: ShellTaskOrganizer,
+ private val homeIntentProvider: HomeIntentProvider,
+ shellInit: ShellInit,
+) {
+ init {
+ shellInit.addInitCallback(::onInit, this)
+ }
+
+ private fun onInit() {
+ handleCrashIfNeeded()
+ }
+
+ private fun handleCrashIfNeeded() {
+ // For now only handle crashes when desktop mode is enabled on the device.
+ if (DesktopModeStatus.canEnterDesktopMode(context) &&
+ !DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
+ var freeformTaskExists = false
+ // If there are running tasks at init, WMShell has crashed but WMCore is still alive.
+ for (task in shellTaskOrganizer.getRunningTasks()) {
+ if (task.windowingMode == WindowConfiguration.WINDOWING_MODE_FREEFORM) {
+ freeformTaskExists = true
+ }
+
+ if (freeformTaskExists) {
+ shellTaskOrganizer.applyTransaction(
+ addLaunchHomePendingIntent(WindowContainerTransaction(), DEFAULT_DISPLAY)
+ )
+ break
+ }
+ }
+ }
+ }
+
+ private fun addLaunchHomePendingIntent(
+ wct: WindowContainerTransaction, displayId: Int
+ ): WindowContainerTransaction {
+ // TODO: b/400462917 - Check that crashes are also handled correctly on HSUM devices. We
+ // might need to pass the [userId] here to launch the correct home.
+ homeIntentProvider.addLaunchHomePendingIntent(wct, displayId)
+ return wct
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 423d1750de75..bc2ed3f35b45 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -69,6 +69,7 @@ import com.android.wm.shell.common.DisplayImeController;
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.FloatingContentCoordinator;
+import com.android.wm.shell.common.HomeIntentProvider;
import com.android.wm.shell.common.LaunchAdjacentController;
import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorController;
import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorSurface;
@@ -80,6 +81,7 @@ import com.android.wm.shell.common.UserProfileContexts;
import com.android.wm.shell.common.split.SplitState;
import com.android.wm.shell.compatui.letterbox.LetterboxCommandHandler;
import com.android.wm.shell.compatui.letterbox.LetterboxTransitionObserver;
+import com.android.wm.shell.crashhandling.ShellCrashHandler;
import com.android.wm.shell.dagger.back.ShellBackAnimationModule;
import com.android.wm.shell.dagger.pip.PipModule;
import com.android.wm.shell.desktopmode.CloseDesktopTaskTransitionHandler;
@@ -451,7 +453,8 @@ public abstract class WMShellModule {
Optional<DesktopImmersiveController> desktopImmersiveController,
WindowDecorViewModel windowDecorViewModel,
Optional<TaskChangeListener> taskChangeListener,
- FocusTransitionObserver focusTransitionObserver) {
+ FocusTransitionObserver focusTransitionObserver,
+ Optional<DesksTransitionObserver> desksTransitionObserver) {
return new FreeformTaskTransitionObserver(
context,
shellInit,
@@ -459,7 +462,8 @@ public abstract class WMShellModule {
desktopImmersiveController,
windowDecorViewModel,
taskChangeListener,
- focusTransitionObserver);
+ focusTransitionObserver,
+ desksTransitionObserver);
}
@WMSingleton
@@ -725,9 +729,11 @@ public abstract class WMShellModule {
static DesksOrganizer provideDesksOrganizer(
@NonNull ShellInit shellInit,
@NonNull ShellCommandHandler shellCommandHandler,
- @NonNull ShellTaskOrganizer shellTaskOrganizer
+ @NonNull ShellTaskOrganizer shellTaskOrganizer,
+ @NonNull LaunchAdjacentController launchAdjacentController
) {
- return new RootTaskDesksOrganizer(shellInit, shellCommandHandler, shellTaskOrganizer);
+ return new RootTaskDesksOrganizer(shellInit, shellCommandHandler, shellTaskOrganizer,
+ launchAdjacentController);
}
@WMSingleton
@@ -753,6 +759,7 @@ public abstract class WMShellModule {
ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler,
DragToDesktopTransitionHandler dragToDesktopTransitionHandler,
@DynamicOverride DesktopUserRepositories desktopUserRepositories,
+ DesktopRepositoryInitializer desktopRepositoryInitializer,
Optional<DesktopImmersiveController> desktopImmersiveController,
DesktopModeLoggerTransitionObserver desktopModeLoggerTransitionObserver,
LaunchAdjacentController launchAdjacentController,
@@ -772,11 +779,12 @@ public abstract class WMShellModule {
Optional<BubbleController> bubbleController,
OverviewToDesktopTransitionObserver overviewToDesktopTransitionObserver,
DesksOrganizer desksOrganizer,
- DesksTransitionObserver desksTransitionObserver,
+ Optional<DesksTransitionObserver> desksTransitionObserver,
UserProfileContexts userProfileContexts,
DesktopModeCompatPolicy desktopModeCompatPolicy,
DragToDisplayTransitionHandler dragToDisplayTransitionHandler,
- DesktopModeMoveToDisplayTransitionHandler moveToDisplayTransitionHandler) {
+ DesktopModeMoveToDisplayTransitionHandler moveToDisplayTransitionHandler,
+ HomeIntentProvider homeIntentProvider) {
return new DesktopTasksController(
context,
shellInit,
@@ -798,6 +806,7 @@ public abstract class WMShellModule {
dragToDesktopTransitionHandler,
desktopImmersiveController.get(),
desktopUserRepositories,
+ desktopRepositoryInitializer,
recentsTransitionHandler,
multiInstanceHelper,
mainExecutor,
@@ -813,11 +822,12 @@ public abstract class WMShellModule {
bubbleController,
overviewToDesktopTransitionObserver,
desksOrganizer,
- desksTransitionObserver,
+ desksTransitionObserver.get(),
userProfileContexts,
desktopModeCompatPolicy,
dragToDisplayTransitionHandler,
- moveToDisplayTransitionHandler);
+ moveToDisplayTransitionHandler,
+ homeIntentProvider);
}
@WMSingleton
@@ -874,6 +884,7 @@ public abstract class WMShellModule {
Transitions transitions,
@DynamicOverride DesktopUserRepositories desktopUserRepositories,
ShellTaskOrganizer shellTaskOrganizer,
+ DesksOrganizer desksOrganizer,
InteractionJankMonitor interactionJankMonitor,
@ShellMainThread Handler handler) {
int maxTaskLimit = DesktopModeStatus.getMaxTaskLimit(context);
@@ -886,6 +897,7 @@ public abstract class WMShellModule {
transitions,
desktopUserRepositories,
shellTaskOrganizer,
+ desksOrganizer,
maxTaskLimit <= 0 ? null : maxTaskLimit,
interactionJankMonitor,
context,
@@ -1215,7 +1227,6 @@ public abstract class WMShellModule {
Optional<DesktopMixedTransitionHandler> desktopMixedTransitionHandler,
Optional<BackAnimationController> backAnimationController,
DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider,
- @NonNull DesksTransitionObserver desksTransitionObserver,
ShellInit shellInit) {
return desktopUserRepositories.flatMap(
repository ->
@@ -1228,17 +1239,21 @@ public abstract class WMShellModule {
desktopMixedTransitionHandler.get(),
backAnimationController.get(),
desktopWallpaperActivityTokenProvider,
- desksTransitionObserver,
shellInit)));
}
@WMSingleton
@Provides
- static DesksTransitionObserver provideDesksTransitionObserver(
- @NonNull @DynamicOverride DesktopUserRepositories desktopUserRepositories,
+ static Optional<DesksTransitionObserver> provideDesksTransitionObserver(
+ Context context,
+ @DynamicOverride DesktopUserRepositories desktopUserRepositories,
@NonNull DesksOrganizer desksOrganizer
) {
- return new DesksTransitionObserver(desktopUserRepositories, desksOrganizer);
+ if (DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)) {
+ return Optional.of(
+ new DesksTransitionObserver(desktopUserRepositories, desksOrganizer));
+ }
+ return Optional.empty();
}
@WMSingleton
@@ -1300,10 +1315,12 @@ public abstract class WMShellModule {
static Optional<DesktopDisplayEventHandler> provideDesktopDisplayEventHandler(
Context context,
ShellInit shellInit,
+ @ShellMainThread CoroutineScope mainScope,
DisplayController displayController,
Optional<DesktopUserRepositories> desktopUserRepositories,
Optional<DesktopTasksController> desktopTasksController,
- Optional<DesktopDisplayModeController> desktopDisplayModeController
+ Optional<DesktopDisplayModeController> desktopDisplayModeController,
+ DesktopRepositoryInitializer desktopRepositoryInitializer
) {
if (!DesktopModeStatus.canEnterDesktopMode(context)) {
return Optional.empty();
@@ -1312,7 +1329,9 @@ public abstract class WMShellModule {
new DesktopDisplayEventHandler(
context,
shellInit,
+ mainScope,
displayController,
+ desktopRepositoryInitializer,
desktopUserRepositories.get(),
desktopTasksController.get(),
desktopDisplayModeController.get()));
@@ -1453,7 +1472,9 @@ public abstract class WMShellModule {
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
IWindowManager windowManager,
ShellTaskOrganizer shellTaskOrganizer,
- DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider
+ DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider,
+ InputManager inputManager,
+ @ShellMainThread Handler mainHandler
) {
if (!DesktopModeStatus.canEnterDesktopMode(context)) {
return Optional.empty();
@@ -1465,7 +1486,9 @@ public abstract class WMShellModule {
rootTaskDisplayAreaOrganizer,
windowManager,
shellTaskOrganizer,
- desktopWallpaperActivityTokenProvider));
+ desktopWallpaperActivityTokenProvider,
+ inputManager,
+ mainHandler));
}
//
@@ -1547,7 +1570,8 @@ public abstract class WMShellModule {
Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional,
Optional<DesktopDisplayEventHandler> desktopDisplayEventHandler,
Optional<DesktopModeKeyGestureHandler> desktopModeKeyGestureHandler,
- Optional<SystemModalsTransitionHandler> systemModalsTransitionHandler) {
+ Optional<SystemModalsTransitionHandler> systemModalsTransitionHandler,
+ ShellCrashHandler shellCrashHandler) {
return new Object();
}
@@ -1567,4 +1591,20 @@ public abstract class WMShellModule {
return new UserProfileContexts(context, shellController, shellInit);
}
+ @WMSingleton
+ @Provides
+ static ShellCrashHandler provideShellCrashHandler(
+ Context context,
+ ShellTaskOrganizer shellTaskOrganizer,
+ HomeIntentProvider homeIntentProvider,
+ ShellInit shellInit) {
+ return new ShellCrashHandler(context, shellTaskOrganizer, homeIntentProvider, shellInit);
+ }
+
+ @WMSingleton
+ @Provides
+ static HomeIntentProvider provideHomeIntentProvider(Context context) {
+ return new HomeIntentProvider(context);
+ }
+
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt
index afc48acad4f5..683b74392fa6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt
@@ -23,15 +23,21 @@ import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener
import com.android.wm.shell.desktopmode.multidesks.OnDeskRemovedListener
+import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.sysui.ShellInit
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
/** Handles display events in desktop mode */
class DesktopDisplayEventHandler(
private val context: Context,
shellInit: ShellInit,
+ private val mainScope: CoroutineScope,
private val displayController: DisplayController,
+ private val desktopRepositoryInitializer: DesktopRepositoryInitializer,
private val desktopUserRepositories: DesktopUserRepositories,
private val desktopTasksController: DesktopTasksController,
private val desktopDisplayModeController: DesktopDisplayModeController,
@@ -61,15 +67,19 @@ class DesktopDisplayEventHandler(
logV("Display #$displayId does not support desks")
return
}
- logV("Creating new desk in new display#$displayId")
- // TODO: b/362720497 - when SystemUI crashes with a freeform task open for any reason, the
- // task is recreated and received in [FreeformTaskListener] before this display callback
- // is invoked, which results in the repository trying to add the task to a desk before the
- // desk has been recreated here, which may result in a crash-loop if the repository is
- // checking that the desk exists before adding a task to it. See b/391984373.
- desktopTasksController.createDesk(displayId)
- // TODO: b/393978539 - consider activating the desk on creation when applicable, such as
- // for connected displays.
+
+ mainScope.launch {
+ desktopRepositoryInitializer.isInitialized.collect { initialized ->
+ if (!initialized) return@collect
+ if (desktopRepository.getNumberOfDesks(displayId) == 0) {
+ logV("Creating new desk in new display#$displayId")
+ // TODO: b/393978539 - consider activating the desk on creation when
+ // applicable, such as for connected displays.
+ desktopTasksController.createDesk(displayId)
+ }
+ cancel()
+ }
+ }
}
override fun onDisplayRemoved(displayId: Int) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt
index e89aafe267ed..904d86282c39 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt
@@ -22,6 +22,8 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
import android.app.WindowConfiguration.windowingModeToString
import android.content.Context
+import android.hardware.input.InputManager
+import android.os.Handler
import android.provider.Settings
import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS
import android.view.Display.DEFAULT_DISPLAY
@@ -29,11 +31,13 @@ import android.view.IWindowManager
import android.view.WindowManager.TRANSIT_CHANGE
import android.window.DesktopExperienceFlags
import android.window.WindowContainerTransaction
+import com.android.internal.annotations.VisibleForTesting
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
+import com.android.wm.shell.shared.annotations.ShellMainThread
import com.android.wm.shell.transition.Transitions
/** Controls the display windowing mode in desktop mode */
@@ -44,8 +48,26 @@ class DesktopDisplayModeController(
private val windowManager: IWindowManager,
private val shellTaskOrganizer: ShellTaskOrganizer,
private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider,
+ private val inputManager: InputManager,
+ @ShellMainThread private val mainHandler: Handler,
) {
+ private val onTabletModeChangedListener =
+ object : InputManager.OnTabletModeChangedListener {
+ override fun onTabletModeChanged(whenNanos: Long, inTabletMode: Boolean) {
+ refreshDisplayWindowingMode()
+ }
+ }
+
+ init {
+ if (DesktopExperienceFlags.FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH.isTrue) {
+ inputManager.registerOnTabletModeChangedListener(
+ onTabletModeChangedListener,
+ mainHandler,
+ )
+ }
+ }
+
fun refreshDisplayWindowingMode() {
if (!DesktopExperienceFlags.ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING.isTrue) return
@@ -89,10 +111,20 @@ class DesktopDisplayModeController(
transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
}
- private fun getTargetWindowingModeForDefaultDisplay(): Int {
+ @VisibleForTesting
+ fun getTargetWindowingModeForDefaultDisplay(): Int {
if (isExtendedDisplayEnabled() && hasExternalDisplay()) {
return WINDOWING_MODE_FREEFORM
}
+ if (DesktopExperienceFlags.FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH.isTrue) {
+ if (isInClamshellMode()) {
+ return WINDOWING_MODE_FREEFORM
+ }
+ return WINDOWING_MODE_FULLSCREEN
+ }
+
+ // If form factor-based desktop first switch is disabled, use the default display windowing
+ // mode here to keep the freeform mode for some form factors (e.g., FEATURE_PC).
return windowManager.getWindowingMode(DEFAULT_DISPLAY)
}
@@ -108,6 +140,8 @@ class DesktopDisplayModeController(
private fun hasExternalDisplay() =
rootTaskDisplayAreaOrganizer.getDisplayIds().any { it != DEFAULT_DISPLAY }
+ private fun isInClamshellMode() = inputManager.isInTabletMode() == InputManager.SWITCH_STATE_OFF
+
private fun logV(msg: String, vararg arguments: Any?) {
ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
index 1f7edb413908..4646662073e6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
@@ -452,6 +452,11 @@ class DesktopMixedTransitionHandler(
private fun findTaskChange(info: TransitionInfo, taskId: Int): TransitionInfo.Change? =
info.changes.firstOrNull { change -> change.taskInfo?.taskId == taskId }
+ private fun findLaunchChange(info: TransitionInfo): TransitionInfo.Change? =
+ info.changes.firstOrNull { change ->
+ change.mode == TRANSIT_OPEN && change.taskInfo != null && change.taskInfo!!.isFreeform
+ }
+
private fun findDesktopTaskLaunchChange(
info: TransitionInfo,
launchTaskId: Int?,
@@ -459,14 +464,18 @@ class DesktopMixedTransitionHandler(
return if (launchTaskId != null) {
// Launching a known task (probably from background or moving to front), so
// specifically look for it.
- findTaskChange(info, launchTaskId)
+ val launchChange = findTaskChange(info, launchTaskId)
+ if (
+ DesktopModeFlags.ENABLE_DESKTOP_OPENING_DEEPLINK_MINIMIZE_ANIMATION_BUGFIX.isTrue &&
+ launchChange == null
+ ) {
+ findLaunchChange(info)
+ } else {
+ launchChange
+ }
} else {
// Launching a new task, so the first opening freeform task.
- info.changes.firstOrNull { change ->
- change.mode == TRANSIT_OPEN &&
- change.taskInfo != null &&
- change.taskInfo!!.isFreeform
- }
+ findLaunchChange(info)
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt
index 5269318943d9..1ea545f3ab67 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt
@@ -56,7 +56,6 @@ class DesktopModeKeyGestureHandler(
override fun handleKeyGestureEvent(event: KeyGestureEvent, focusedToken: IBinder?): Boolean {
if (
- !isKeyGestureSupported(event.keyGestureType) ||
!desktopTasksController.isPresent ||
!desktopModeWindowDecorViewModel.isPresent
) {
@@ -136,19 +135,6 @@ class DesktopModeKeyGestureHandler(
}
}
- override fun isKeyGestureSupported(gestureType: Int): Boolean =
- when (gestureType) {
- KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY ->
- enableMoveToNextDisplayShortcut()
- KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW,
- KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW,
- KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW,
- KeyGestureEvent.KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW ->
- DesktopModeFlags.ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS.isTrue &&
- manageKeyGestures()
- else -> false
- }
-
// TODO: b/364154795 - wait for the completion of moveToNextDisplay transition, otherwise it
// will pick a wrong task when a user quickly perform other actions with keyboard shortcuts
// after moveToNextDisplay, and move this to FocusTransitionObserver class.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandler.kt
index fbf170f13a40..91bd3c9b6c22 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandler.kt
@@ -17,6 +17,7 @@
package com.android.wm.shell.desktopmode
import android.animation.Animator
+import android.animation.AnimatorSet
import android.animation.ValueAnimator
import android.os.IBinder
import android.view.Choreographer
@@ -28,9 +29,7 @@ import com.android.wm.shell.shared.animation.Interpolators
import com.android.wm.shell.transition.Transitions
import kotlin.time.Duration.Companion.milliseconds
-/**
- * Transition handler for moving a window to a different display.
- */
+/** Transition handler for moving a window to a different display. */
class DesktopModeMoveToDisplayTransitionHandler(
private val animationTransaction: SurfaceControl.Transaction
) : Transitions.TransitionHandler {
@@ -47,46 +46,52 @@ class DesktopModeMoveToDisplayTransitionHandler(
finishTransaction: SurfaceControl.Transaction,
finishCallback: Transitions.TransitionFinishCallback,
): Boolean {
- val change = info.changes.find { it.startDisplayId != it.endDisplayId } ?: return false
- ValueAnimator.ofFloat(0f, 1f)
- .apply {
- duration = ANIM_DURATION.inWholeMilliseconds
- interpolator = Interpolators.LINEAR
- addUpdateListener { animation ->
- animationTransaction
- .setAlpha(change.leash, animation.animatedValue as Float)
- .setFrameTimeline(Choreographer.getInstance().vsyncId)
- .apply()
+ val changes = info.changes.filter { it.startDisplayId != it.endDisplayId }
+ if (changes.isEmpty()) return false
+ for (change in changes) {
+ val endBounds = change.endAbsBounds
+ // The position should be relative to the parent. For example, in ActivityEmbedding, the
+ // leash surface for the embedded Activity is parented to the container.
+ val endPosition = change.endRelOffset
+ startTransaction
+ .setPosition(change.leash, endPosition.x.toFloat(), endPosition.y.toFloat())
+ .setWindowCrop(change.leash, endBounds.width(), endBounds.height())
+ }
+ startTransaction.apply()
+
+ val animator = AnimatorSet()
+ animator.playTogether(
+ changes.map {
+ ValueAnimator.ofFloat(0f, 1f).apply {
+ duration = ANIM_DURATION.inWholeMilliseconds
+ interpolator = Interpolators.LINEAR
+ addUpdateListener { animation ->
+ animationTransaction
+ .setAlpha(it.leash, animation.animatedValue as Float)
+ .setFrameTimeline(Choreographer.getInstance().vsyncId)
+ .apply()
+ }
}
- addListener(
- object : Animator.AnimatorListener {
- override fun onAnimationStart(animation: Animator) {
- val endBounds = change.endAbsBounds
- startTransaction
- .setPosition(
- change.leash,
- endBounds.left.toFloat(),
- endBounds.top.toFloat(),
- )
- .setWindowCrop(change.leash, endBounds.width(), endBounds.height())
- .apply()
- }
+ }
+ )
+ animator.addListener(
+ object : Animator.AnimatorListener {
+ override fun onAnimationStart(animation: Animator) = Unit
- override fun onAnimationEnd(animation: Animator) {
- finishTransaction.apply()
- finishCallback.onTransitionFinished(null)
- }
+ override fun onAnimationEnd(animation: Animator) {
+ finishTransaction.apply()
+ finishCallback.onTransitionFinished(null)
+ }
- override fun onAnimationCancel(animation: Animator) {
- finishTransaction.apply()
- finishCallback.onTransitionFinished(null)
- }
+ override fun onAnimationCancel(animation: Animator) {
+ finishTransaction.apply()
+ finishCallback.onTransitionFinished(null)
+ }
- override fun onAnimationRepeat(animation: Animator) = Unit
- }
- )
+ override fun onAnimationRepeat(animation: Animator) = Unit
}
- .start()
+ )
+ animator.start()
return true
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
index c5ee3137e5ba..fa98d0339a65 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
@@ -22,6 +22,13 @@ import android.annotation.DimenRes
import android.app.ActivityManager.RunningTaskInfo
import android.app.TaskInfo
import android.content.Context
+import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
+import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
+import android.content.pm.ActivityInfo.LAUNCH_MULTIPLE
+import android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE
+import android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE_PER_TASK
+import android.content.pm.ActivityInfo.LAUNCH_SINGLE_TASK
import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
import android.content.pm.ActivityInfo.isFixedOrientationLandscape
import android.content.pm.ActivityInfo.isFixedOrientationPortrait
@@ -30,7 +37,9 @@ import android.content.res.Configuration.ORIENTATION_PORTRAIT
import android.graphics.Rect
import android.os.SystemProperties
import android.util.Size
+import android.window.DesktopModeFlags
import com.android.wm.shell.R
+import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayLayout
import kotlin.math.ceil
@@ -264,6 +273,58 @@ fun getAppHeaderHeight(context: Context): Int =
@DimenRes fun getAppHeaderHeightId(): Int = R.dimen.desktop_mode_freeform_decor_caption_height
/**
+ * Returns the task bounds a launching task should inherit from an existing running instance.
+ * Returns null if there are no bounds to inherit.
+ */
+fun getInheritedExistingTaskBounds(
+ taskRepository: DesktopRepository,
+ shellTaskOrganizer: ShellTaskOrganizer,
+ task: RunningTaskInfo,
+ deskId: Int,
+): Rect? {
+ if (!DesktopModeFlags.INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES.isTrue) return null
+ val activeTask = taskRepository.getExpandedTasksIdsInDeskOrdered(deskId).firstOrNull()
+ if (activeTask == null) return null
+ val lastTask = shellTaskOrganizer.getRunningTaskInfo(activeTask)
+ val lastTaskTopActivity = lastTask?.topActivity
+ val currentTaskTopActivity = task.topActivity
+ val intentFlags = task.baseIntent.flags
+ val launchMode = task.topActivityInfo?.launchMode ?: LAUNCH_MULTIPLE
+ return when {
+ // No running task activity to inherit bounds from.
+ lastTaskTopActivity == null -> null
+ // No current top activity to set bounds for.
+ currentTaskTopActivity == null -> null
+ // Top task is not an instance of the launching activity, do not inherit its bounds.
+ lastTaskTopActivity.packageName != currentTaskTopActivity.packageName -> null
+ // Top task is an instance of launching activity. Activity will be launching in a new
+ // task with the existing task also being closed. Inherit existing task bounds to
+ // prevent new task jumping.
+ (isLaunchingNewTask(launchMode, intentFlags) && isClosingExitingInstance(intentFlags)) ->
+ lastTask.configuration.windowConfiguration.bounds
+ else -> null
+ }
+}
+
+/**
+ * Returns true if the launch mode or intent will result in a new task being created for the
+ * activity.
+ */
+private fun isLaunchingNewTask(launchMode: Int, intentFlags: Int) =
+ launchMode == LAUNCH_SINGLE_TASK ||
+ launchMode == LAUNCH_SINGLE_INSTANCE ||
+ launchMode == LAUNCH_SINGLE_INSTANCE_PER_TASK ||
+ (intentFlags and FLAG_ACTIVITY_NEW_TASK) != 0
+
+/**
+ * Returns true if the intent will result in an existing task instance being closed if a new one
+ * appears.
+ */
+private fun isClosingExitingInstance(intentFlags: Int) =
+ (intentFlags and FLAG_ACTIVITY_CLEAR_TASK) != 0 ||
+ (intentFlags and FLAG_ACTIVITY_MULTIPLE_TASK) == 0
+
+/**
* Calculates the desired initial bounds for applications in desktop windowing. This is done as a
* scale of the screen bounds.
*/
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
index 70539902f651..b3b4d59090e8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
@@ -32,6 +32,7 @@ import android.content.Context;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.Region;
+import android.view.Display;
import android.view.SurfaceControl;
import android.window.DesktopModeFlags;
@@ -114,6 +115,7 @@ public class DesktopModeVisualIndicator {
private final Context mContext;
private final DisplayController mDisplayController;
private final ActivityManager.RunningTaskInfo mTaskInfo;
+ private final Display mDisplay;
private IndicatorType mCurrentType;
private final DragStartState mDragStartState;
@@ -145,9 +147,10 @@ public class DesktopModeVisualIndicator {
mCurrentType = NO_INDICATOR;
mDragStartState = dragStartState;
mSnapEventHandler = snapEventHandler;
+ mDisplay = mDisplayController.getDisplay(mTaskInfo.displayId);
mVisualIndicatorViewContainer.createView(
mContext,
- mDisplayController.getDisplay(mTaskInfo.displayId),
+ mDisplay,
mDisplayController.getDisplayLayout(mTaskInfo.displayId),
mTaskInfo,
taskSurface
@@ -175,6 +178,7 @@ public class DesktopModeVisualIndicator {
/** Start the fade-in animation. */
void fadeInIndicator() {
+ if (mCurrentType == NO_INDICATOR) return;
mVisualIndicatorViewContainer.fadeInIndicator(
mDisplayController.getDisplayLayout(mTaskInfo.displayId), mCurrentType,
mTaskInfo.displayId);
@@ -193,7 +197,7 @@ public class DesktopModeVisualIndicator {
if (inputCoordinates.x > layout.width()) return TO_SPLIT_RIGHT_INDICATOR;
IndicatorType result;
if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()
- && !DesktopModeStatus.canEnterDesktopMode(mContext)) {
+ && !DesktopModeStatus.isDesktopModeSupportedOnDisplay(mContext, mDisplay)) {
// If desktop is not available, default to "no indicator"
result = NO_INDICATOR;
} else {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
index bbb300ea42da..8636bc1f56c2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
@@ -235,6 +235,10 @@ class DesktopRepository(
/** Returns the default desk in the given display. */
private fun getDefaultDesk(displayId: Int): Desk? = desktopData.getDefaultDesk(displayId)
+ /** Returns whether the given desk is active in its display. */
+ fun isDeskActive(deskId: Int): Boolean =
+ desktopData.getAllActiveDesks().any { desk -> desk.deskId == deskId }
+
/** Sets the given desk as the active one in the given display. */
fun setActiveDesk(displayId: Int, deskId: Int) {
logD("setActiveDesk for displayId=%d and deskId=%d", displayId, deskId)
@@ -485,7 +489,7 @@ class DesktopRepository(
fun getExpandedTasksOrdered(displayId: Int): List<Int> =
getFreeformTasksInZOrder(displayId).filter { !isMinimizedTask(it) }
- @VisibleForTesting
+ /** Returns all active non-minimized tasks for [deskId] ordered from top to bottom. */
fun getExpandedTasksIdsInDeskOrdered(deskId: Int): List<Int> =
getFreeformTasksIdsInDeskInZOrder(deskId).filter { !isMinimizedTask(it) }
@@ -712,12 +716,15 @@ class DesktopRepository(
}
/**
- * Returns the top transparent fullscreen task id for a given display's active desk, or null.
+ * Returns the top transparent fullscreen task id for a given display, or null.
*
* TODO: b/389960283 - add explicit [deskId] argument.
*/
fun getTopTransparentFullscreenTaskId(displayId: Int): Int? =
- desktopData.getActiveDesk(displayId)?.topTransparentFullscreenTaskId
+ desktopData
+ .desksSequence(displayId)
+ .mapNotNull { it.topTransparentFullscreenTaskId }
+ .firstOrNull()
/**
* Clears the top transparent fullscreen task id info for a given display's active desk.
@@ -814,7 +821,6 @@ class DesktopRepository(
}
/** Minimizes the task in its desk. */
- @VisibleForTesting
fun minimizeTaskInDesk(displayId: Int, deskId: Int, taskId: Int) {
logD("MinimizeTaskInDesk: displayId=%d deskId=%d, task=%d", displayId, deskId, taskId)
desktopData.getDesk(deskId)?.minimizedTasks?.add(taskId)
@@ -929,6 +935,12 @@ class DesktopRepository(
listener.onDeskRemoved(displayId = desk.displayId, deskId = desk.deskId)
}
}
+ if (
+ DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue &&
+ DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue
+ ) {
+ removeDeskFromPersistentRepository(desk)
+ }
return activeTasks
}
@@ -1027,6 +1039,24 @@ class DesktopRepository(
}
}
+ private fun removeDeskFromPersistentRepository(desk: Desk) {
+ mainCoroutineScope.launch {
+ try {
+ logD(
+ "updatePersistentRepositoryForRemovedDesk user=%d desk=%d",
+ userId,
+ desk.deskId,
+ )
+ persistentRepository.removeDesktop(userId = userId, desktopId = desk.deskId)
+ } catch (throwable: Throwable) {
+ logE(
+ "An exception occurred while updating the persistent repository \n%s",
+ throwable.stackTrace,
+ )
+ }
+ }
+ }
+
internal fun dump(pw: PrintWriter, prefix: String) {
val innerPrefix = "$prefix "
pw.println("${prefix}DesktopRepository")
@@ -1045,6 +1075,7 @@ class DesktopRepository(
}
.forEach { (displayId, activeDeskId, desks) ->
pw.println("${prefix}Display #$displayId:")
+ pw.println("${innerPrefix}numOfDesks=${desks.size}")
pw.println("${innerPrefix}activeDesk=$activeDeskId")
pw.println("${innerPrefix}desks:")
val desksPrefix = "$innerPrefix "
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 301b79afd537..d0356d55035d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -32,6 +32,8 @@ import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
import android.app.WindowConfiguration.WindowingMode
import android.content.Context
import android.content.Intent
+import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.graphics.Point
import android.graphics.PointF
import android.graphics.Rect
@@ -48,6 +50,7 @@ import android.view.DragEvent
import android.view.MotionEvent
import android.view.SurfaceControl
import android.view.SurfaceControl.Transaction
+import android.view.WindowManager
import android.view.WindowManager.TRANSIT_CHANGE
import android.view.WindowManager.TRANSIT_CLOSE
import android.view.WindowManager.TRANSIT_NONE
@@ -84,6 +87,7 @@ import com.android.wm.shell.bubbles.BubbleController
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayLayout
import com.android.wm.shell.common.ExternalInterfaceBinder
+import com.android.wm.shell.common.HomeIntentProvider
import com.android.wm.shell.common.MultiInstanceHelper
import com.android.wm.shell.common.MultiInstanceHelper.Companion.getComponent
import com.android.wm.shell.common.RemoteCallable
@@ -111,6 +115,9 @@ import com.android.wm.shell.desktopmode.multidesks.DeskTransition
import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer
import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver
import com.android.wm.shell.desktopmode.multidesks.OnDeskRemovedListener
+import com.android.wm.shell.desktopmode.multidesks.createDesk
+import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer
+import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer.DeskRecreationFactory
import com.android.wm.shell.draganddrop.DragAndDropController
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
@@ -190,6 +197,7 @@ class DesktopTasksController(
private val dragToDesktopTransitionHandler: DragToDesktopTransitionHandler,
private val desktopImmersiveController: DesktopImmersiveController,
private val userRepositories: DesktopUserRepositories,
+ desktopRepositoryInitializer: DesktopRepositoryInitializer,
private val recentsTransitionHandler: RecentsTransitionHandler,
private val multiInstanceHelper: MultiInstanceHelper,
@ShellMainThread private val mainExecutor: ShellExecutor,
@@ -210,6 +218,7 @@ class DesktopTasksController(
private val desktopModeCompatPolicy: DesktopModeCompatPolicy,
private val dragToDisplayTransitionHandler: DragToDisplayTransitionHandler,
private val moveToDisplayTransitionHandler: DesktopModeMoveToDisplayTransitionHandler,
+ private val homeIntentProvider: HomeIntentProvider,
) :
RemoteCallable<DesktopTasksController>,
Transitions.TransitionHandler,
@@ -235,6 +244,10 @@ class DesktopTasksController(
removeVisualIndicator()
}
+ override fun onTransitionInterrupted() {
+ removeVisualIndicator()
+ }
+
private fun removeVisualIndicator() {
visualIndicator?.fadeOutIndicator { releaseVisualIndicator() }
}
@@ -267,6 +280,19 @@ class DesktopTasksController(
}
userId = ActivityManager.getCurrentUser()
taskRepository = userRepositories.getProfile(userId)
+
+ if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
+ desktopRepositoryInitializer.deskRecreationFactory =
+ DeskRecreationFactory { deskUserId, destinationDisplayId, deskId ->
+ if (deskUserId != userId) {
+ // TODO: b/400984250 - add multi-user support for multi-desk restoration.
+ logW("Tried to recreated desk of another user.")
+ deskId
+ } else {
+ desksOrganizer.createDesk(destinationDisplayId)
+ }
+ }
+ }
}
private fun onInit() {
@@ -762,7 +788,9 @@ class DesktopTasksController(
fun minimizeTask(taskInfo: RunningTaskInfo, minimizeReason: MinimizeReason) {
val wct = WindowContainerTransaction()
- val isMinimizingToPip = taskInfo.pictureInPictureParams?.isAutoEnterEnabled() ?: false
+ val isMinimizingToPip =
+ DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue &&
+ (taskInfo.pictureInPictureParams?.isAutoEnterEnabled() ?: false)
// If task is going to PiP, start a PiP transition instead of a minimize transition
if (isMinimizingToPip) {
val requestInfo =
@@ -961,10 +989,13 @@ class DesktopTasksController(
.apply { launchWindowingMode = WINDOWING_MODE_FREEFORM }
.toBundle(),
)
+ val deskId = taskRepository.getDeskIdForTask(taskId) ?: getDefaultDeskId(DEFAULT_DISPLAY)
startLaunchTransition(
TRANSIT_OPEN,
wct,
taskId,
+ deskId = deskId,
+ displayId = DEFAULT_DISPLAY,
remoteTransition = remoteTransition,
unminimizeReason = unminimizeReason,
)
@@ -982,19 +1013,26 @@ class DesktopTasksController(
remoteTransition: RemoteTransition? = null,
unminimizeReason: UnminimizeReason = UnminimizeReason.UNKNOWN,
) {
- logV("moveTaskToFront taskId=%s", taskInfo.taskId)
+ val deskId =
+ taskRepository.getDeskIdForTask(taskInfo.taskId) ?: getDefaultDeskId(taskInfo.displayId)
+ logV("moveTaskToFront taskId=%s deskId=%s", taskInfo.taskId, deskId)
// If a task is tiled, another task should be brought to foreground with it so let
// tiling controller handle the request.
if (snapEventHandler.moveTaskToFrontIfTiled(taskInfo)) {
return
}
val wct = WindowContainerTransaction()
- wct.reorder(taskInfo.token, /* onTop= */ true, /* includingParents= */ true)
+ if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
+ desksOrganizer.reorderTaskToFront(wct, deskId, taskInfo)
+ } else {
+ wct.reorder(taskInfo.token, /* onTop= */ true, /* includingParents= */ true)
+ }
startLaunchTransition(
transitionType = TRANSIT_TO_FRONT,
wct = wct,
launchingTaskId = taskInfo.taskId,
remoteTransition = remoteTransition,
+ deskId = deskId,
displayId = taskInfo.displayId,
unminimizeReason = unminimizeReason,
)
@@ -1006,14 +1044,22 @@ class DesktopTasksController(
wct: WindowContainerTransaction,
launchingTaskId: Int?,
remoteTransition: RemoteTransition? = null,
- displayId: Int = DEFAULT_DISPLAY,
+ deskId: Int,
+ displayId: Int,
unminimizeReason: UnminimizeReason = UnminimizeReason.UNKNOWN,
): IBinder {
+ logV(
+ "startLaunchTransition type=%s launchingTaskId=%d deskId=%d displayId=%d",
+ WindowManager.transitTypeToString(transitionType),
+ launchingTaskId,
+ deskId,
+ displayId,
+ )
// TODO: b/397619806 - Consolidate sharable logic with [handleFreeformTaskLaunch].
var launchTransaction = wct
val taskIdToMinimize =
addAndGetMinimizeChanges(
- displayId,
+ deskId,
launchTransaction,
newTaskId = launchingTaskId,
launchingNewIntent = launchingTaskId == null,
@@ -1027,21 +1073,20 @@ class DesktopTasksController(
)
var activationRunOnTransitStart: RunOnTransitStart? = null
val shouldActivateDesk =
- (DesktopExperienceFlags.ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING.isTrue ||
- DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) &&
- !isDesktopModeShowing(displayId)
+ when {
+ DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue ->
+ !taskRepository.isDeskActive(deskId)
+ DesktopExperienceFlags.ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING.isTrue -> {
+ !isDesktopModeShowing(displayId)
+ }
+ else -> false
+ }
if (shouldActivateDesk) {
- val deskIdToActivate =
- checkNotNull(
- launchingTaskId?.let { taskRepository.getDeskIdForTask(it) }
- ?: getDefaultDeskId(displayId)
- )
val activateDeskWct = WindowContainerTransaction()
// TODO: b/391485148 - pass in the launching task here to apply task-limit policy,
// but make sure to not do it twice since it is also done at the start of this
// function.
- activationRunOnTransitStart =
- addDeskActivationChanges(deskIdToActivate, activateDeskWct)
+ activationRunOnTransitStart = addDeskActivationChanges(deskId, activateDeskWct)
// Desk activation must be handled before app launch-related transactions.
activateDeskWct.merge(launchTransaction, /* transfer= */ true)
launchTransaction = activateDeskWct
@@ -1152,7 +1197,14 @@ class DesktopTasksController(
}
wct.sendPendingIntent(pendingIntent, intent, ops.toBundle())
- startLaunchTransition(TRANSIT_OPEN, wct, launchingTaskId = null)
+ val deskId = getDefaultDeskId(displayId)
+ startLaunchTransition(
+ TRANSIT_OPEN,
+ wct,
+ launchingTaskId = null,
+ deskId = deskId,
+ displayId = displayId,
+ )
}
/**
@@ -1169,6 +1221,11 @@ class DesktopTasksController(
return
}
+ if (splitScreenController.isTaskInSplitScreen(task.taskId)) {
+ moveSplitPairToDisplay(task, displayId)
+ return
+ }
+
val wct = WindowContainerTransaction()
val displayAreaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId)
if (displayAreaInfo == null) {
@@ -1176,39 +1233,6 @@ class DesktopTasksController(
return
}
- // check if the task is part of splitscreen
- if (
- Flags.enableNonDefaultDisplaySplit() &&
- Flags.enableMoveToNextDisplayShortcut() &&
- splitScreenController.isTaskInSplitScreen(task.taskId)
- ) {
- val activeDeskId = taskRepository.getActiveDeskId(displayId)
- logV("moveToDisplay: moving split root to displayId=%d", displayId)
- val stageCoordinatorRootTaskToken =
- splitScreenController.multiDisplayProvider.getDisplayRootForDisplayId(
- DEFAULT_DISPLAY
- )
- wct.reparent(stageCoordinatorRootTaskToken, displayAreaInfo.token, true /* onTop */)
- val deactivationRunnable =
- if (activeDeskId != null) {
- // Split is being placed on top of an existing desk in the target display. Make
- // sure it is cleaned up.
- performDesktopExitCleanUp(
- wct = wct,
- deskId = activeDeskId,
- displayId = displayId,
- willExitDesktop = true,
- shouldEndUpAtHome = false,
- )
- } else {
- null
- }
- val transition =
- transitions.startTransition(TRANSIT_CHANGE, wct, moveToDisplayTransitionHandler)
- deactivationRunnable?.invoke(transition)
- return
- }
-
val destinationDeskId = taskRepository.getDefaultDeskId(displayId)
if (destinationDeskId == null) {
logW("moveToDisplay: desk not found for display: $displayId")
@@ -1270,6 +1294,53 @@ class DesktopTasksController(
}
/**
+ * Move split pair associated with the [task] to display with [displayId].
+ *
+ * No-op if task is already on that display per [RunningTaskInfo.displayId].
+ */
+ private fun moveSplitPairToDisplay(task: RunningTaskInfo, displayId: Int) {
+ if (!splitScreenController.isTaskInSplitScreen(task.taskId)) {
+ return
+ }
+
+ if (!Flags.enableNonDefaultDisplaySplit() || !Flags.enableMoveToNextDisplayShortcut()) {
+ return
+ }
+
+ val wct = WindowContainerTransaction()
+ val displayAreaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId)
+ if (displayAreaInfo == null) {
+ logW("moveSplitPairToDisplay: display not found")
+ return
+ }
+
+ val activeDeskId = taskRepository.getActiveDeskId(displayId)
+ logV("moveSplitPairToDisplay: moving split root to displayId=%d", displayId)
+
+ val stageCoordinatorRootTaskToken =
+ splitScreenController.multiDisplayProvider.getDisplayRootForDisplayId(DEFAULT_DISPLAY)
+ wct.reparent(stageCoordinatorRootTaskToken, displayAreaInfo.token, true /* onTop */)
+
+ val deactivationRunnable =
+ if (activeDeskId != null) {
+ // Split is being placed on top of an existing desk in the target display. Make
+ // sure it is cleaned up.
+ performDesktopExitCleanUp(
+ wct = wct,
+ deskId = activeDeskId,
+ displayId = displayId,
+ willExitDesktop = true,
+ shouldEndUpAtHome = false,
+ )
+ } else {
+ null
+ }
+ val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
+ deactivationRunnable?.invoke(transition)
+ return
+ }
+
+ /**
* Quick-resizes a desktop task, toggling between a fullscreen state (represented by the stable
* bounds) and a free floating state (either the last saved bounds if available or the default
* bounds otherwise).
@@ -1664,15 +1735,7 @@ class DesktopTasksController(
wct.reorder(runningTaskInfo.token, /* onTop= */ true)
} else if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) {
// Task is not running, start it
- wct.startTask(
- taskId,
- ActivityOptions.makeBasic()
- .apply {
- launchWindowingMode = WINDOWING_MODE_FREEFORM
- splashScreenStyle = SPLASH_SCREEN_STYLE_ICON
- }
- .toBundle(),
- )
+ wct.startTask(taskId, createActivityOptionsForStartTask().toBundle())
}
}
@@ -1691,34 +1754,7 @@ class DesktopTasksController(
}
private fun addLaunchHomePendingIntent(wct: WindowContainerTransaction, displayId: Int) {
- val userHandle = UserHandle.of(userId)
- val launchHomeIntent =
- Intent(Intent.ACTION_MAIN).apply {
- if (displayId != DEFAULT_DISPLAY) {
- addCategory(Intent.CATEGORY_SECONDARY_HOME)
- } else {
- addCategory(Intent.CATEGORY_HOME)
- }
- }
- val options =
- ActivityOptions.makeBasic().apply {
- launchWindowingMode = WINDOWING_MODE_FULLSCREEN
- pendingIntentBackgroundActivityStartMode =
- ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
- if (Flags.enablePerDisplayDesktopWallpaperActivity()) {
- launchDisplayId = displayId
- }
- }
- val pendingIntent =
- PendingIntent.getActivityAsUser(
- context,
- /* requestCode= */ 0,
- launchHomeIntent,
- PendingIntent.FLAG_IMMUTABLE,
- /* options= */ null,
- userHandle,
- )
- wct.sendPendingIntent(pendingIntent, launchHomeIntent, options.toBundle())
+ homeIntentProvider.addLaunchHomePendingIntent(wct, displayId, userId)
}
private fun addWallpaperActivity(displayId: Int, wct: WindowContainerTransaction) {
@@ -2103,6 +2139,7 @@ class DesktopTasksController(
// TODO(b/337915660): Add a transition handler for these; animations
// need updates in some cases.
val baseActivity = callingTaskInfo.baseActivity ?: return
+ val userHandle = UserHandle.of(callingTaskInfo.userId)
val fillIn: Intent =
userProfileContexts
.getOrCreate(callingTaskInfo.userId)
@@ -2110,11 +2147,13 @@ class DesktopTasksController(
.getLaunchIntentForPackage(baseActivity.packageName) ?: return
fillIn.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
val launchIntent =
- PendingIntent.getActivity(
+ PendingIntent.getActivityAsUser(
context,
/* requestCode= */ 0,
fillIn,
PendingIntent.FLAG_IMMUTABLE,
+ /* options= */ null,
+ userHandle,
)
val options = createNewWindowOptions(callingTaskInfo)
when (options.launchWindowingMode) {
@@ -2141,10 +2180,14 @@ class DesktopTasksController(
WINDOWING_MODE_FREEFORM -> {
val wct = WindowContainerTransaction()
wct.sendPendingIntent(launchIntent, fillIn, options.toBundle())
+ val deskId =
+ taskRepository.getDeskIdForTask(callingTaskInfo.taskId)
+ ?: getDefaultDeskId(callingTaskInfo.displayId)
startLaunchTransition(
transitionType = TRANSIT_OPEN,
wct = wct,
launchingTaskId = null,
+ deskId = deskId,
displayId = callingTaskInfo.displayId,
)
}
@@ -2224,6 +2267,7 @@ class DesktopTasksController(
logV("skip keyguard is locked")
return null
}
+ val deskId = getDefaultDeskId(task.displayId)
val wct = WindowContainerTransaction()
if (shouldFreeformTaskLaunchSwitchToFullscreen(task)) {
logD("Bring desktop tasks to front on transition=taskId=%d", task.taskId)
@@ -2246,17 +2290,24 @@ class DesktopTasksController(
runOnTransitStart?.invoke(transition)
return wct
}
- val deskId = getDefaultDeskId(task.displayId)
val runOnTransitStart = addDeskActivationChanges(deskId, wct, task)
runOnTransitStart?.invoke(transition)
wct.reorder(task.token, true)
return wct
}
+ val inheritedTaskBounds =
+ getInheritedExistingTaskBounds(taskRepository, shellTaskOrganizer, task, deskId)
+ if (!taskRepository.isActiveTask(task.taskId) && inheritedTaskBounds != null) {
+ // Inherit bounds from closing task instance to prevent application jumping different
+ // cascading positions.
+ wct.setBounds(task.token, inheritedTaskBounds)
+ }
// TODO(b/365723620): Handle non running tasks that were launched after reboot.
// If task is already visible, it must have been handled already and added to desktop mode.
- // Cascade task only if it's not visible yet.
+ // Cascade task only if it's not visible yet and has no inherited bounds.
if (
- DesktopModeFlags.ENABLE_CASCADING_WINDOWS.isTrue() &&
+ inheritedTaskBounds == null &&
+ DesktopModeFlags.ENABLE_CASCADING_WINDOWS.isTrue() &&
!taskRepository.isVisibleTask(task.taskId)
) {
val displayLayout = displayController.getDisplayLayout(task.displayId)
@@ -2288,7 +2339,7 @@ class DesktopTasksController(
reason = DesktopImmersiveController.ExitReason.TASK_LAUNCH,
)
// 2) minimize a Task if needed.
- val taskIdToMinimize = addAndGetMinimizeChanges(task.displayId, wct, task.taskId)
+ val taskIdToMinimize = addAndGetMinimizeChanges(deskId, wct, task.taskId)
addPendingAppLaunchTransition(transition, task.taskId, taskIdToMinimize)
if (taskIdToMinimize != null) {
addPendingMinimizeTransition(transition, taskIdToMinimize, MinimizeReason.TASK_LIMIT)
@@ -2334,7 +2385,7 @@ class DesktopTasksController(
// The desk was already showing and we're launching a new Task - we
// might need to minimize another Task.
val taskIdToMinimize =
- addAndGetMinimizeChanges(task.displayId, wct, task.taskId)
+ addAndGetMinimizeChanges(deskId, wct, task.taskId)
taskIdToMinimize?.let { minimizingTaskId ->
addPendingMinimizeTransition(
transition,
@@ -2492,9 +2543,17 @@ class DesktopTasksController(
) {
val targetDisplayId = taskRepository.getDisplayForDesk(deskId)
val displayLayout = displayController.getDisplayLayout(targetDisplayId) ?: return
- val initialBounds = getInitialBounds(displayLayout, task, targetDisplayId)
- if (canChangeTaskPosition(task)) {
- wct.setBounds(task.token, initialBounds)
+ val inheritedTaskBounds =
+ getInheritedExistingTaskBounds(taskRepository, shellTaskOrganizer, task, deskId)
+ if (inheritedTaskBounds != null) {
+ // Inherit bounds from closing task instance to prevent application jumping different
+ // cascading positions.
+ wct.setBounds(task.token, inheritedTaskBounds)
+ } else {
+ val initialBounds = getInitialBounds(displayLayout, task, targetDisplayId)
+ if (canChangeTaskPosition(task)) {
+ wct.setBounds(task.token, initialBounds)
+ }
}
if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
desksOrganizer.moveTaskToDesk(wct = wct, deskId = deskId, task = task)
@@ -2670,7 +2729,7 @@ class DesktopTasksController(
/** Returns the ID of the Task that will be minimized, or null if no task will be minimized. */
private fun addAndGetMinimizeChanges(
- displayId: Int,
+ deskId: Int,
wct: WindowContainerTransaction,
newTaskId: Int?,
launchingNewIntent: Boolean = false,
@@ -2679,7 +2738,7 @@ class DesktopTasksController(
require(newTaskId == null || !launchingNewIntent)
return desktopTasksLimiter
.get()
- .addAndGetMinimizeTaskChanges(displayId, wct, newTaskId, launchingNewIntent)
+ .addAndGetMinimizeTaskChanges(deskId, wct, newTaskId, launchingNewIntent)
}
private fun addPendingMinimizeTransition(
@@ -2776,15 +2835,36 @@ class DesktopTasksController(
}
prepareForDeskActivation(displayId, wct)
desksOrganizer.activateDesk(wct, deskId)
- if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) {
- // TODO: 362720497 - do non-running tasks need to be restarted with |wct#startTask|?
- }
taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(
doesAnyTaskRequireTaskbarRounding(displayId)
)
- // TODO: b/362720497 - activating a desk with the intention to move a new task to
- // it means we may need to minimize something in the activating desk. Do so here
- // similar to how it's done in #bringDesktopAppsToFront.
+ val expandedTasksOrderedFrontToBack =
+ taskRepository.getExpandedTasksIdsInDeskOrdered(deskId = deskId)
+ // If we're adding a new Task we might need to minimize an old one
+ val taskIdToMinimize =
+ desktopTasksLimiter
+ .getOrNull()
+ ?.getTaskIdToMinimize(expandedTasksOrderedFrontToBack, newTaskIdInFront)
+ if (taskIdToMinimize != null) {
+ val taskToMinimize = shellTaskOrganizer.getRunningTaskInfo(taskIdToMinimize)
+ // TODO(b/365725441): Handle non running task minimization
+ if (taskToMinimize != null) {
+ desksOrganizer.minimizeTask(wct, deskId, taskToMinimize)
+ }
+ }
+ if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue) {
+ expandedTasksOrderedFrontToBack
+ .filter { taskId -> taskId != taskIdToMinimize }
+ .reversed()
+ .forEach { taskId ->
+ val runningTaskInfo = shellTaskOrganizer.getRunningTaskInfo(taskId)
+ if (runningTaskInfo == null) {
+ wct.startTask(taskId, createActivityOptionsForStartTask().toBundle())
+ } else {
+ desksOrganizer.reorderTaskToFront(wct, deskId, runningTaskInfo)
+ }
+ }
+ }
return { transition ->
val activateDeskTransition =
if (newTaskIdInFront != null) {
@@ -2802,6 +2882,9 @@ class DesktopTasksController(
)
}
desksTransitionObserver.addPendingTransition(activateDeskTransition)
+ taskIdToMinimize?.let { minimizingTask ->
+ addPendingMinimizeTransition(transition, minimizingTask, MinimizeReason.TASK_LIMIT)
+ }
}
}
@@ -3381,7 +3464,14 @@ class DesktopTasksController(
if (windowingMode == WINDOWING_MODE_FREEFORM) {
if (DesktopModeFlags.ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX.isTrue()) {
// TODO b/376389593: Use a custom tab tearing transition/animation
- startLaunchTransition(TRANSIT_OPEN, wct, launchingTaskId = null)
+ val deskId = getDefaultDeskId(DEFAULT_DISPLAY)
+ startLaunchTransition(
+ TRANSIT_OPEN,
+ wct,
+ launchingTaskId = null,
+ deskId = deskId,
+ displayId = DEFAULT_DISPLAY,
+ )
} else {
desktopModeDragAndDropTransitionHandler.handleDropEvent(wct)
}
@@ -3426,6 +3516,13 @@ class DesktopTasksController(
}
}
+ private fun createActivityOptionsForStartTask(): ActivityOptions {
+ return ActivityOptions.makeBasic().apply {
+ launchWindowingMode = WINDOWING_MODE_FREEFORM
+ splashScreenStyle = SPLASH_SCREEN_STYLE_ICON
+ }
+ }
+
private fun dump(pw: PrintWriter, prefix: String) {
val innerPrefix = "$prefix "
pw.println("${prefix}DesktopTasksController")
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
index da369f094405..4ca58823b52b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
@@ -22,6 +22,7 @@ import android.os.Handler
import android.os.IBinder
import android.view.SurfaceControl
import android.view.WindowManager.TRANSIT_TO_BACK
+import android.window.DesktopExperienceFlags
import android.window.DesktopModeFlags
import android.window.TransitionInfo
import android.window.WindowContainerTransaction
@@ -31,6 +32,7 @@ import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.MinimizeReason
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.UnminimizeReason
+import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
import com.android.wm.shell.shared.annotations.ShellMainThread
import com.android.wm.shell.sysui.UserChangeListener
@@ -48,6 +50,7 @@ class DesktopTasksLimiter(
transitions: Transitions,
private val desktopUserRepositories: DesktopUserRepositories,
private val shellTaskOrganizer: ShellTaskOrganizer,
+ private val desksOrganizer: DesksOrganizer,
private val maxTasksLimit: Int?,
private val interactionJankMonitor: InteractionJankMonitor,
private val context: Context,
@@ -258,7 +261,7 @@ class DesktopTasksLimiter(
* returning the task to minimize.
*/
fun addAndGetMinimizeTaskChanges(
- displayId: Int,
+ deskId: Int,
wct: WindowContainerTransaction,
newFrontTaskId: Int?,
launchingNewIntent: Boolean = false,
@@ -267,15 +270,19 @@ class DesktopTasksLimiter(
val taskRepository = desktopUserRepositories.current
val taskIdToMinimize =
getTaskIdToMinimize(
- taskRepository.getExpandedTasksOrdered(displayId),
+ taskRepository.getExpandedTasksIdsInDeskOrdered(deskId),
newFrontTaskId,
launchingNewIntent,
)
- // If it's a running task, reorder it to back.
taskIdToMinimize
?.let { shellTaskOrganizer.getRunningTaskInfo(it) }
- // TODO: b/391485148 - this won't really work with multi-desks enabled.
- ?.let { wct.reorder(it.token, /* onTop= */ false) }
+ ?.let { task ->
+ if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
+ wct.reorder(task.token, /* onTop= */ false)
+ } else {
+ desksOrganizer.minimizeTask(wct, deskId, task)
+ }
+ }
return taskIdToMinimize
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
index bd9c30e2a495..7dabeb7c9d15 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
@@ -36,7 +36,6 @@ import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.back.BackAnimationController
import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.isExitDesktopModeTransition
import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider
-import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
import com.android.wm.shell.shared.TransitionUtil
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
@@ -58,7 +57,6 @@ class DesktopTasksTransitionObserver(
private val desktopMixedTransitionHandler: DesktopMixedTransitionHandler,
private val backAnimationController: BackAnimationController,
private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider,
- private val desksTransitionObserver: DesksTransitionObserver,
shellInit: ShellInit,
) : Transitions.TransitionObserver {
@@ -88,7 +86,6 @@ class DesktopTasksTransitionObserver(
finishTransaction: SurfaceControl.Transaction,
) {
// TODO: b/332682201 Update repository state
- desksTransitionObserver.onTransitionReady(transition, info)
if (
DesktopModeFlags.INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC
.isTrue() && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue()
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
index e943c42dcdfc..24b2e4879546 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
@@ -24,8 +24,10 @@ import android.os.SystemClock
import android.os.SystemProperties
import android.os.UserHandle
import android.view.SurfaceControl
+import android.view.SurfaceControl.Transaction
import android.view.WindowManager.TRANSIT_CLOSE
import android.window.DesktopModeFlags
+import android.window.DesktopModeFlags.ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX
import android.window.TransitionInfo
import android.window.TransitionInfo.Change
import android.window.TransitionRequestInfo
@@ -185,18 +187,29 @@ sealed class DragToDesktopTransitionHandler(
*/
fun finishDragToDesktopTransition(wct: WindowContainerTransaction): IBinder? {
if (!inProgress) {
+ logV("finishDragToDesktop: not in progress, returning")
// Don't attempt to finish a drag to desktop transition since there is no transition in
// progress which means that the drag to desktop transition was never successfully
// started.
return null
}
- if (requireTransitionState().startAborted) {
+ val state = requireTransitionState()
+ if (state.startAborted) {
+ logV("finishDragToDesktop: start was aborted, clearing state")
// Don't attempt to complete the drag-to-desktop since the start transition didn't
// succeed as expected. Just reset the state as if nothing happened.
clearState()
return null
}
- return transitions.startTransition(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, wct, this)
+ if (state.startInterrupted) {
+ logV("finishDragToDesktop: start was interrupted, returning")
+ // We should only have interrupted the start transition after receiving a cancel/end
+ // request, let that existing request play out and just return here.
+ return null
+ }
+ state.endTransitionToken =
+ transitions.startTransition(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, wct, this)
+ return state.endTransitionToken
}
/**
@@ -220,6 +233,11 @@ sealed class DragToDesktopTransitionHandler(
clearState()
return
}
+ if (state.startInterrupted) {
+ // We should only have interrupted the start transition after receiving a cancel/end
+ // request, let that existing request play out and just return here.
+ return
+ }
state.cancelState = cancelState
if (state.draggedTaskChange != null && cancelState == CancelState.STANDARD_CANCEL) {
@@ -227,7 +245,7 @@ sealed class DragToDesktopTransitionHandler(
// transient to start and merge. Animate the cancellation (scale back to original
// bounds) first before actually starting the cancel transition so that the wallpaper
// is visible behind the animating task.
- startCancelAnimation()
+ state.activeCancelAnimation = startCancelAnimation()
} else if (
state.draggedTaskChange != null &&
(cancelState == CancelState.CANCEL_SPLIT_LEFT ||
@@ -255,7 +273,7 @@ sealed class DragToDesktopTransitionHandler(
) {
if (bubbleController.isEmpty || state !is TransitionState.FromFullscreen) {
// TODO(b/388853233): add support for dragging split task to bubble
- startCancelAnimation()
+ state.activeCancelAnimation = startCancelAnimation()
} else {
// Animation is handled by BubbleController
val wct = WindowContainerTransaction()
@@ -327,8 +345,9 @@ sealed class DragToDesktopTransitionHandler(
val taskInfo = state.draggedTaskChange?.taskInfo ?: error("Expected non-null taskInfo")
val dragPosition = PointF(state.dragAnimator.position)
val scale = state.dragAnimator.scale
+ val cornerRadius = state.dragAnimator.cornerRadius
state.dragAnimator.cancelAnimator()
- requestBubble(wct, taskInfo, onLeft, scale, dragPosition)
+ requestBubble(wct, taskInfo, onLeft, scale, cornerRadius, dragPosition)
}
private fun requestBubble(
@@ -336,13 +355,14 @@ sealed class DragToDesktopTransitionHandler(
taskInfo: RunningTaskInfo,
onLeft: Boolean,
taskScale: Float = 1f,
+ cornerRadius: Float = 0f,
dragPosition: PointF = PointF(0f, 0f),
) {
val controller =
bubbleController.orElseThrow { IllegalStateException("BubbleController not set") }
controller.expandStackAndSelectBubble(
taskInfo,
- BubbleTransitions.DragData(onLeft, taskScale, dragPosition, wct),
+ BubbleTransitions.DragData(onLeft, taskScale, cornerRadius, dragPosition, wct),
)
}
@@ -355,6 +375,19 @@ sealed class DragToDesktopTransitionHandler(
): Boolean {
val state = requireTransitionState()
+ if (
+ handleCancelOrExitAfterInterrupt(
+ transition,
+ info,
+ startTransaction,
+ finishTransaction,
+ finishCallback,
+ state,
+ )
+ ) {
+ return true
+ }
+
val isStartDragToDesktop =
info.type == TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP &&
transition == state.startTransitionToken
@@ -537,6 +570,58 @@ sealed class DragToDesktopTransitionHandler(
}
}
+ private fun handleCancelOrExitAfterInterrupt(
+ transition: IBinder,
+ info: TransitionInfo,
+ startTransaction: Transaction,
+ finishTransaction: Transaction,
+ finishCallback: Transitions.TransitionFinishCallback,
+ state: TransitionState,
+ ): Boolean {
+ if (!ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX.isTrue) {
+ return false
+ }
+ val isCancelDragToDesktop =
+ info.type == TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP &&
+ transition == state.cancelTransitionToken
+ val isEndDragToDesktop =
+ info.type == TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP &&
+ transition == state.endTransitionToken
+ // We should only receive cancel or end transitions through startAnimation() if the
+ // start transition was interrupted while a cancel- or end-transition had already
+ // been requested. Finish the cancel/end transition to avoid having to deal with more
+ // incoming transitions, and clear the state for the next start-drag transition.
+ if (!isCancelDragToDesktop && !isEndDragToDesktop) {
+ return false
+ }
+ if (!state.startInterrupted) {
+ logW(
+ "Not interrupted, but received startAnimation for cancel/end drag." +
+ "isCancel=$isCancelDragToDesktop, isEnd=$isEndDragToDesktop"
+ )
+ return false
+ }
+ logV(
+ "startAnimation: interrupted -> " +
+ "isCancel=$isCancelDragToDesktop, isEnd=$isEndDragToDesktop"
+ )
+ if (isEndDragToDesktop) {
+ setupEndDragToDesktop(info, startTransaction, finishTransaction)
+ animateEndDragToDesktop(startTransaction = startTransaction, finishCallback)
+ } else { // isCancelDragToDesktop
+ // Similar to when we merge the cancel transition: ensure all tasks involved in the
+ // cancel transition are shown, and finish the transition immediately.
+ info.changes.forEach { change ->
+ startTransaction.show(change.leash)
+ finishTransaction.show(change.leash)
+ }
+ }
+ startTransaction.apply()
+ finishCallback.onTransitionFinished(/* wct= */ null)
+ clearState()
+ return true
+ }
+
/**
* Calculates start drag to desktop layers for transition [info]. The leash layer is calculated
* based on its change position in the transition, e.g. `appLayer = appLayers - i`, where i is
@@ -588,6 +673,7 @@ sealed class DragToDesktopTransitionHandler(
?: error("Start transition expected to be waiting for merge but wasn't")
if (isEndTransition) {
logV("mergeAnimation: end-transition, target=$mergeTarget")
+ state.mergedEndTransition = true
setupEndDragToDesktop(
info,
startTransaction = startT,
@@ -615,6 +701,41 @@ sealed class DragToDesktopTransitionHandler(
return
}
logW("unhandled merge transition: transitionInfo=$info")
+ // Handle unknown incoming transitions by finishing the start transition. For now, only do
+ // this if we've already requested a cancel- or end transition. If we've already merged the
+ // end-transition, or if the end-transition is running on its own, then just wait until that
+ // finishes instead. If we've merged the cancel-transition we've finished the
+ // start-transition and won't reach this code.
+ if (
+ mergeTarget == state.startTransitionToken &&
+ isCancelOrEndTransitionRequested(state) &&
+ !state.mergedEndTransition
+ ) {
+ interruptStartTransition(state)
+ }
+ }
+
+ private fun isCancelOrEndTransitionRequested(state: TransitionState): Boolean =
+ state.cancelTransitionToken != null || state.endTransitionToken != null
+
+ private fun interruptStartTransition(state: TransitionState) {
+ if (!ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX.isTrue) {
+ return
+ }
+ logV("interruptStartTransition")
+ state.startTransitionFinishCb?.onTransitionFinished(/* wct= */ null)
+ state.dragAnimator.cancelAnimator()
+ state.activeCancelAnimation?.removeAllListeners()
+ state.activeCancelAnimation?.cancel()
+ state.activeCancelAnimation = null
+ // Keep the transition state so we can deal with Cancel/End properly in #startAnimation.
+ state.startInterrupted = true
+ dragToDesktopStateListener?.onTransitionInterrupted()
+ // Cancel CUJs here as they won't be accurate now that an incoming transition is playing.
+ interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD)
+ interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE)
+ LatencyTracker.getInstance(context)
+ .onActionCancel(LatencyTracker.ACTION_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG)
}
protected open fun setupEndDragToDesktop(
@@ -781,7 +902,7 @@ sealed class DragToDesktopTransitionHandler(
} ?: false
}
- private fun startCancelAnimation() {
+ private fun startCancelAnimation(): Animator {
val state = requireTransitionState()
val dragToDesktopAnimator = state.dragAnimator
@@ -798,7 +919,7 @@ sealed class DragToDesktopTransitionHandler(
val dx = targetX - x
val dy = targetY - y
val tx: SurfaceControl.Transaction = transactionSupplier.get()
- ValueAnimator.ofFloat(DRAG_FREEFORM_SCALE, 1f)
+ return ValueAnimator.ofFloat(DRAG_FREEFORM_SCALE, 1f)
.setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS)
.apply {
addUpdateListener { animator ->
@@ -816,6 +937,7 @@ sealed class DragToDesktopTransitionHandler(
addListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
+ state.activeCancelAnimation = null
dragToDesktopStateListener?.onCancelToDesktopAnimationEnd()
// Start the cancel transition to restore order.
startCancelDragToDesktopTransition()
@@ -908,10 +1030,16 @@ sealed class DragToDesktopTransitionHandler(
val dragLayer: Int,
)
+ /** Listener for various events happening during the DragToDesktop transition. */
interface DragToDesktopStateListener {
+ /** Indicates that the animation into Desktop has started. */
fun onCommitToDesktopAnimationStart()
+ /** Called when the animation to cancel the desktop-drag has finished. */
fun onCancelToDesktopAnimationEnd()
+
+ /** Indicates that the drag-to-desktop transition has been interrupted. */
+ fun onTransitionInterrupted()
}
sealed class TransitionState {
@@ -928,6 +1056,10 @@ sealed class DragToDesktopTransitionHandler(
abstract var cancelState: CancelState
abstract var startAborted: Boolean
abstract val visualIndicator: DesktopModeVisualIndicator?
+ abstract var startInterrupted: Boolean
+ abstract var endTransitionToken: IBinder?
+ abstract var mergedEndTransition: Boolean
+ abstract var activeCancelAnimation: Animator?
data class FromFullscreen(
override val draggedTaskId: Int,
@@ -943,6 +1075,10 @@ sealed class DragToDesktopTransitionHandler(
override var cancelState: CancelState = CancelState.NO_CANCEL,
override var startAborted: Boolean = false,
override val visualIndicator: DesktopModeVisualIndicator?,
+ override var startInterrupted: Boolean = false,
+ override var endTransitionToken: IBinder? = null,
+ override var mergedEndTransition: Boolean = false,
+ override var activeCancelAnimation: Animator? = null,
var otherRootChanges: MutableList<Change> = mutableListOf(),
) : TransitionState()
@@ -960,6 +1096,10 @@ sealed class DragToDesktopTransitionHandler(
override var cancelState: CancelState = CancelState.NO_CANCEL,
override var startAborted: Boolean = false,
override val visualIndicator: DesktopModeVisualIndicator?,
+ override var startInterrupted: Boolean = false,
+ override var endTransitionToken: IBinder? = null,
+ override var mergedEndTransition: Boolean = false,
+ override var activeCancelAnimation: Animator? = null,
var splitRootChange: Change? = null,
var otherSplitTask: Int,
) : TransitionState()
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt
index fc359d7d67b6..5a988fcd1b77 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt
@@ -18,6 +18,8 @@ package com.android.wm.shell.desktopmode.multidesks
import android.app.ActivityManager
import android.window.TransitionInfo
import android.window.WindowContainerTransaction
+import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer.OnCreateCallback
+import kotlin.coroutines.suspendCoroutine
/** An organizer of desk containers in which to host child desktop windows. */
interface DesksOrganizer {
@@ -40,6 +42,13 @@ interface DesksOrganizer {
task: ActivityManager.RunningTaskInfo,
)
+ /** Reorders a desk's task to the front. */
+ fun reorderTaskToFront(
+ wct: WindowContainerTransaction,
+ deskId: Int,
+ task: ActivityManager.RunningTaskInfo,
+ )
+
/** Minimizes the given task of the given deskId. */
fun minimizeTask(
wct: WindowContainerTransaction,
@@ -47,6 +56,13 @@ interface DesksOrganizer {
task: ActivityManager.RunningTaskInfo,
)
+ /** Unminimize the given task of the given desk. */
+ fun unminimizeTask(
+ wct: WindowContainerTransaction,
+ deskId: Int,
+ task: ActivityManager.RunningTaskInfo,
+ )
+
/** Whether the change is for the given desk id. */
fun isDeskChange(change: TransitionInfo.Change, deskId: Int): Boolean
@@ -68,3 +84,9 @@ interface DesksOrganizer {
fun onCreated(deskId: Int)
}
}
+
+/** Creates a new desk container in the given display. */
+suspend fun DesksOrganizer.createDesk(displayId: Int): Int = suspendCoroutine { cont ->
+ val onCreateCallback = OnCreateCallback { deskId -> cont.resumeWith(Result.success(deskId)) }
+ createDesk(displayId, onCreateCallback)
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt
index f576258ebdaa..49ca58e7b32a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt
@@ -33,6 +33,7 @@ import androidx.core.util.forEach
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.common.LaunchAdjacentController
import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer.OnCreateCallback
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
import com.android.wm.shell.sysui.ShellCommandHandler
@@ -44,6 +45,7 @@ class RootTaskDesksOrganizer(
shellInit: ShellInit,
shellCommandHandler: ShellCommandHandler,
private val shellTaskOrganizer: ShellTaskOrganizer,
+ private val launchAdjacentController: LaunchAdjacentController,
) : DesksOrganizer, ShellTaskOrganizer.TaskListener {
private val createDeskRootRequests = mutableListOf<CreateDeskRequest>()
@@ -110,7 +112,31 @@ class RootTaskDesksOrganizer(
wct.reparent(task.token, root.taskInfo.token, /* onTop= */ true)
}
+ override fun reorderTaskToFront(
+ wct: WindowContainerTransaction,
+ deskId: Int,
+ task: RunningTaskInfo,
+ ) {
+ logV("reorderTaskToFront task=${task.taskId} desk=$deskId")
+ val root = deskRootsByDeskId[deskId] ?: error("Root not found for desk: $deskId")
+ if (task.taskId in root.children) {
+ wct.reorder(task.token, /* onTop= */ true, /* includingParents= */ true)
+ return
+ }
+ val minimizationRoot =
+ checkNotNull(deskMinimizationRootsByDeskId[deskId]) {
+ "Minimization root not found for desk: $deskId"
+ }
+ if (task.taskId in minimizationRoot.children) {
+ unminimizeTask(wct, deskId, task)
+ wct.reorder(task.token, /* onTop= */ true, /* includingParents= */ true)
+ return
+ }
+ logE("Attempted to reorder task=${task.taskId} in desk=$deskId but it was not a child")
+ }
+
override fun minimizeTask(wct: WindowContainerTransaction, deskId: Int, task: RunningTaskInfo) {
+ logV("minimizeTask task=${task.taskId} desk=$deskId")
val deskRoot =
checkNotNull(deskRootsByDeskId[deskId]) { "Root not found for desk: $deskId" }
val minimizationRoot =
@@ -129,6 +155,30 @@ class RootTaskDesksOrganizer(
wct.reparent(task.token, minimizationRoot.token, /* onTop= */ true)
}
+ override fun unminimizeTask(
+ wct: WindowContainerTransaction,
+ deskId: Int,
+ task: RunningTaskInfo,
+ ) {
+ val taskId = task.taskId
+ logV("unminimizeTask task=$taskId desk=$deskId")
+ val deskRoot =
+ checkNotNull(deskRootsByDeskId[deskId]) { "Root not found for desk: $deskId" }
+ val minimizationRoot =
+ checkNotNull(deskMinimizationRootsByDeskId[deskId]) {
+ "Minimization root not found for desk: $deskId"
+ }
+ if (taskId in deskRoot.children) {
+ logV("Task #$taskId is already unminimized in desk=$deskId")
+ return
+ }
+ if (taskId !in minimizationRoot.children) {
+ logE("Attempted to unminimize task=$taskId in desk=$deskId but it was not a child")
+ return
+ }
+ wct.reparent(task.token, deskRoot.token, /* onTop= */ true)
+ }
+
override fun isDeskChange(change: TransitionInfo.Change, deskId: Int): Boolean =
(isDeskRootChange(change) && change.taskId == deskId) ||
(getDeskMinimizationRootInChange(change)?.deskId == deskId)
@@ -164,6 +214,21 @@ class RootTaskDesksOrganizer(
change.mode == TRANSIT_TO_FRONT
override fun onTaskAppeared(taskInfo: RunningTaskInfo, leash: SurfaceControl) {
+ handleTaskAppeared(taskInfo, leash)
+ updateLaunchAdjacentController()
+ }
+
+ override fun onTaskInfoChanged(taskInfo: RunningTaskInfo) {
+ handleTaskInfoChanged(taskInfo)
+ updateLaunchAdjacentController()
+ }
+
+ override fun onTaskVanished(taskInfo: RunningTaskInfo) {
+ handleTaskVanished(taskInfo)
+ updateLaunchAdjacentController()
+ }
+
+ private fun handleTaskAppeared(taskInfo: RunningTaskInfo, leash: SurfaceControl) {
// Check whether this task is appearing inside a desk.
if (taskInfo.parentTaskId in deskRootsByDeskId) {
val deskId = taskInfo.parentTaskId
@@ -216,7 +281,7 @@ class RootTaskDesksOrganizer(
hideMinimizationRoot(deskMinimizationRoot)
}
- override fun onTaskInfoChanged(taskInfo: RunningTaskInfo) {
+ private fun handleTaskInfoChanged(taskInfo: RunningTaskInfo) {
if (deskRootsByDeskId.contains(taskInfo.taskId)) {
val deskId = taskInfo.taskId
deskRootsByDeskId[deskId] = deskRootsByDeskId[deskId].copy(taskInfo = taskInfo)
@@ -254,7 +319,7 @@ class RootTaskDesksOrganizer(
logE("onTaskInfoChanged: unknown task: ${taskInfo.taskId}")
}
- override fun onTaskVanished(taskInfo: RunningTaskInfo) {
+ private fun handleTaskVanished(taskInfo: RunningTaskInfo) {
if (deskRootsByDeskId.contains(taskInfo.taskId)) {
val deskId = taskInfo.taskId
val deskRoot = deskRootsByDeskId[deskId]
@@ -336,6 +401,18 @@ class RootTaskDesksOrganizer(
deskRootsByDeskId.forEach { _, deskRoot -> deskRoot.children -= taskId }
}
+ private fun updateLaunchAdjacentController() {
+ deskRootsByDeskId.forEach { deskId, root ->
+ if (root.taskInfo.isVisible) {
+ // Disable launch adjacent handling if any desk is active, otherwise the split
+ // launch root and the desk root will both be eligible to take launching tasks.
+ launchAdjacentController.launchAdjacentEnabled = false
+ return
+ }
+ }
+ launchAdjacentController.launchAdjacentEnabled = true
+ }
+
@VisibleForTesting
data class DeskRoot(
val deskId: Int,
@@ -377,6 +454,9 @@ class RootTaskDesksOrganizer(
override fun dump(pw: PrintWriter, prefix: String) {
val innerPrefix = "$prefix "
pw.println("$prefix$TAG")
+ pw.println(
+ "${innerPrefix}launchAdjacentEnabled=" + launchAdjacentController.launchAdjacentEnabled
+ )
pw.println("${innerPrefix}Desk Roots:")
deskRootsByDeskId.forEach { deskId, root ->
val minimizationRoot = deskMinimizationRootsByDeskId[deskId]
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt
index 1566544f5303..f71eacab518d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt
@@ -132,10 +132,7 @@ class DesktopPersistentRepository(private val dataStore: DataStore<DesktopPersis
.toBuilder()
.putDesktopRepoByUser(
userId,
- currentRepository
- .toBuilder()
- .putDesktop(desktopId, desktop.build())
- .build(),
+ currentRepository.toBuilder().putDesktop(desktopId, desktop.build()).build(),
)
.build()
}
@@ -149,6 +146,33 @@ class DesktopPersistentRepository(private val dataStore: DataStore<DesktopPersis
}
}
+ /** Removes the desktop from the persistent repository. */
+ suspend fun removeDesktop(userId: Int, desktopId: Int) {
+ try {
+ dataStore.updateData { persistentRepositories: DesktopPersistentRepositories ->
+ val currentRepository =
+ persistentRepositories.getDesktopRepoByUserOrDefault(
+ userId,
+ DesktopRepositoryState.getDefaultInstance(),
+ )
+ persistentRepositories
+ .toBuilder()
+ .putDesktopRepoByUser(
+ userId,
+ currentRepository.toBuilder().removeDesktop(desktopId).build(),
+ )
+ .build()
+ }
+ } catch (throwable: Throwable) {
+ Log.e(
+ TAG,
+ "Error in removing desktop related data, data is " +
+ "stored in a file named $DESKTOP_REPOSITORIES_DATASTORE_FILE",
+ throwable,
+ )
+ }
+ }
+
suspend fun removeUsers(uids: List<Int>) {
try {
dataStore.updateData { persistentRepositories: DesktopPersistentRepositories ->
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializer.kt
index a26ebbf4c99a..8191181cac11 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializer.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializer.kt
@@ -17,8 +17,22 @@
package com.android.wm.shell.desktopmode.persistence
import com.android.wm.shell.desktopmode.DesktopUserRepositories
+import kotlinx.coroutines.flow.StateFlow
/** Interface for initializing the [DesktopUserRepositories]. */
-fun interface DesktopRepositoryInitializer {
+interface DesktopRepositoryInitializer {
+ /** A factory used to recreate a desk from persistence. */
+ var deskRecreationFactory: DeskRecreationFactory
+
+ /** A flow that emits true when the repository has been initialized. */
+ val isInitialized: StateFlow<Boolean>
+
+ /** Initialize the user repositories from a persistent data store. */
fun initialize(userRepositories: DesktopUserRepositories)
+
+ /** A factory for recreating desks. */
+ fun interface DeskRecreationFactory {
+ /** Recreates a restored desk and returns the new desk id. */
+ suspend fun recreateDesk(userId: Int, destinationDisplayId: Int, deskId: Int): Int
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt
index 0507e59c06e1..49cb7391fe97 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt
@@ -17,13 +17,19 @@
package com.android.wm.shell.desktopmode.persistence
import android.content.Context
+import android.view.Display
import android.window.DesktopExperienceFlags
import android.window.DesktopModeFlags
+import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.desktopmode.DesktopRepository
import com.android.wm.shell.desktopmode.DesktopUserRepositories
+import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer.DeskRecreationFactory
+import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
import com.android.wm.shell.shared.annotations.ShellMainThread
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
/**
@@ -37,62 +43,136 @@ class DesktopRepositoryInitializerImpl(
private val persistentRepository: DesktopPersistentRepository,
@ShellMainThread private val mainCoroutineScope: CoroutineScope,
) : DesktopRepositoryInitializer {
+
+ override var deskRecreationFactory: DeskRecreationFactory = DefaultDeskRecreationFactory()
+
+ private val _isInitialized = MutableStateFlow(false)
+ override val isInitialized: StateFlow<Boolean> = _isInitialized
+
override fun initialize(userRepositories: DesktopUserRepositories) {
- if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) return
+ if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue) {
+ _isInitialized.value = true
+ return
+ }
// TODO: b/365962554 - Handle the case that user moves to desktop before it's initialized
mainCoroutineScope.launch {
- val desktopUserPersistentRepositoryMap =
- persistentRepository.getUserDesktopRepositoryMap() ?: return@launch
- for (userId in desktopUserPersistentRepositoryMap.keys) {
- val repository = userRepositories.getProfile(userId)
- val desktopRepositoryState =
- persistentRepository.getDesktopRepositoryState(userId) ?: continue
- val desktopByDesktopIdMap = desktopRepositoryState.desktopMap
- for (desktopId in desktopByDesktopIdMap.keys) {
- val persistentDesktop =
- persistentRepository.readDesktop(userId, desktopId) ?: continue
- val maxTasks =
- DesktopModeStatus.getMaxTaskLimit(context).takeIf { it > 0 }
- ?: persistentDesktop.zOrderedTasksCount
- var visibleTasksCount = 0
- repository.addDesk(
- displayId = persistentDesktop.displayId,
- deskId =
- if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
- persistentDesktop.desktopId
- } else {
- // When disabled, desk ids are always the display id.
- persistentDesktop.displayId
- },
+ try {
+ val desktopUserPersistentRepositoryMap =
+ persistentRepository.getUserDesktopRepositoryMap() ?: return@launch
+ for (userId in desktopUserPersistentRepositoryMap.keys) {
+ val repository = userRepositories.getProfile(userId)
+ val desktopRepositoryState =
+ persistentRepository.getDesktopRepositoryState(userId) ?: continue
+ val desksToRestore = getDesksToRestore(desktopRepositoryState, userId)
+ logV(
+ "initialize() will restore desks=%s user=%d",
+ desksToRestore.map { it.desktopId },
+ userId,
)
- persistentDesktop.zOrderedTasksList
- // Reverse it so we initialize the repo from bottom to top.
- .reversed()
- .mapNotNull { taskId -> persistentDesktop.tasksByTaskIdMap[taskId] }
- // TODO: b/362720497 - add tasks to their respective desk when multi-desk
- // persistence is implemented.
- .forEach { task ->
- if (
- task.desktopTaskState == DesktopTaskState.VISIBLE &&
- visibleTasksCount < maxTasks
- ) {
- visibleTasksCount++
- repository.addTask(
- persistentDesktop.displayId,
- task.taskId,
- isVisible = false,
- )
- } else {
- repository.addTask(
- persistentDesktop.displayId,
- task.taskId,
+ desksToRestore.forEach { persistentDesktop ->
+ val maxTasks = getTaskLimit(persistentDesktop)
+ val displayId = persistentDesktop.displayId
+ val deskId = persistentDesktop.desktopId
+ // TODO: b/401107440 - Implement desk restoration to other displays.
+ val newDisplayId = Display.DEFAULT_DISPLAY
+ val newDeskId =
+ deskRecreationFactory.recreateDesk(
+ userId = userId,
+ destinationDisplayId = newDisplayId,
+ deskId = deskId,
+ )
+ logV(
+ "Recreated desk=%d in display=%d using new deskId=%d and displayId=%d",
+ deskId,
+ displayId,
+ newDeskId,
+ newDisplayId,
+ )
+ if (newDeskId != deskId || newDisplayId != displayId) {
+ logV("Removing obsolete desk from persistence under deskId=%d", deskId)
+ persistentRepository.removeDesktop(userId, deskId)
+ }
+
+ // TODO: b/393961770 - [DesktopRepository] doesn't save desks to the
+ // persistent repository until a task is added to them. Update it so that
+ // empty desks can be restored too.
+ repository.addDesk(displayId = displayId, deskId = newDeskId)
+ var visibleTasksCount = 0
+ persistentDesktop.zOrderedTasksList
+ // Reverse it so we initialize the repo from bottom to top.
+ .reversed()
+ .mapNotNull { taskId -> persistentDesktop.tasksByTaskIdMap[taskId] }
+ .forEach { task ->
+ // Visible here means non-minimized a.k.a. expanded, it does not
+ // mean
+ // it is visible in WM (and |DesktopRepository|) terms.
+ val isVisible =
+ task.desktopTaskState == DesktopTaskState.VISIBLE &&
+ visibleTasksCount < maxTasks
+
+ repository.addTaskToDesk(
+ displayId = displayId,
+ deskId = newDeskId,
+ taskId = task.taskId,
isVisible = false,
)
- repository.minimizeTask(persistentDesktop.displayId, task.taskId)
+
+ if (isVisible) {
+ visibleTasksCount++
+ } else {
+ repository.minimizeTaskInDesk(
+ displayId = displayId,
+ deskId = newDeskId,
+ taskId = task.taskId,
+ )
+ }
}
- }
+ }
}
+ } finally {
+ _isInitialized.value = true
}
}
}
+
+ private suspend fun getDesksToRestore(
+ state: DesktopRepositoryState,
+ userId: Int,
+ ): Set<Desktop> {
+ // TODO: b/365873835 - what about desks that won't be restored?
+ // - invalid desk ids from multi-desk -> single-desk switching can be ignored / deleted.
+ val limitToSingleDeskPerDisplay =
+ !DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue
+ return state.desktopMap.keys
+ .mapNotNull { deskId ->
+ persistentRepository.readDesktop(userId, deskId)?.takeIf { desk ->
+ // Do not restore invalid desks when multi-desks is disabled. This is
+ // possible if the feature is disabled after having created multiple desks.
+ val isValidSingleDesk = desk.desktopId == desk.displayId
+ (!limitToSingleDeskPerDisplay || isValidSingleDesk)
+ }
+ }
+ .toSet()
+ }
+
+ private fun getTaskLimit(persistedDesk: Desktop): Int =
+ DesktopModeStatus.getMaxTaskLimit(context).takeIf { it > 0 }
+ ?: persistedDesk.zOrderedTasksCount
+
+ private fun logV(msg: String, vararg arguments: Any?) {
+ ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
+ }
+
+ /** A default implementation of [DeskRecreationFactory] that reuses the desk id. */
+ private class DefaultDeskRecreationFactory : DeskRecreationFactory {
+ override suspend fun recreateDesk(
+ userId: Int,
+ destinationDisplayId: Int,
+ deskId: Int,
+ ): Int = deskId
+ }
+
+ companion object {
+ private const val TAG = "DesktopRepositoryInitializerImpl"
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
index 897e2d1601a5..2fe786531af5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -24,6 +24,7 @@ import android.app.ActivityManager.RunningTaskInfo;
import android.content.Context;
import android.util.SparseArray;
import android.view.SurfaceControl;
+import android.window.DesktopExperienceFlags;
import android.window.DesktopModeFlags;
import com.android.internal.protolog.ProtoLog;
@@ -167,6 +168,11 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
}
private void updateLaunchAdjacentController() {
+ if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue()) {
+ // With multiple desks, freeform tasks are children of a root task controlled by
+ // DesksOrganizer, so toggling launch-adjacent should be managed there.
+ return;
+ }
for (int i = 0; i < mTasks.size(); i++) {
if (mTasks.valueAt(i).mTaskInfo.isVisible) {
mLaunchAdjacentController.setLaunchAdjacentEnabled(false);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
index 8059b94685ba..f89ba0a168d1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
@@ -29,6 +29,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.android.wm.shell.desktopmode.DesktopImmersiveController;
+import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.FocusTransitionObserver;
import com.android.wm.shell.transition.Transitions;
@@ -52,6 +53,7 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs
private final WindowDecorViewModel mWindowDecorViewModel;
private final Optional<TaskChangeListener> mTaskChangeListener;
private final FocusTransitionObserver mFocusTransitionObserver;
+ private final Optional<DesksTransitionObserver> mDesksTransitionObserver;
private final Map<IBinder, List<ActivityManager.RunningTaskInfo>> mTransitionToTaskInfo =
new HashMap<>();
@@ -63,12 +65,14 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs
Optional<DesktopImmersiveController> desktopImmersiveController,
WindowDecorViewModel windowDecorViewModel,
Optional<TaskChangeListener> taskChangeListener,
- FocusTransitionObserver focusTransitionObserver) {
+ FocusTransitionObserver focusTransitionObserver,
+ Optional<DesksTransitionObserver> desksTransitionObserver) {
mTransitions = transitions;
mDesktopImmersiveController = desktopImmersiveController;
mWindowDecorViewModel = windowDecorViewModel;
mTaskChangeListener = taskChangeListener;
mFocusTransitionObserver = focusTransitionObserver;
+ mDesksTransitionObserver = desksTransitionObserver;
if (FreeformComponents.requiresFreeformComponents(context)) {
shellInit.addInitCallback(this::onInit, this);
}
@@ -85,6 +89,10 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs
@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startT,
@NonNull SurfaceControl.Transaction finishT) {
+ // Update desk state first, otherwise [TaskChangeListener] may update desktop task state
+ // under an outdated active desk if a desk switch and a task update happen in the same
+ // transition, such as when unminimizing a task from an inactive desk.
+ mDesksTransitionObserver.ifPresent(o -> o.onTransitionReady(transition, info));
if (DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue()) {
// TODO(b/367268953): Remove when DesktopTaskListener is introduced and the repository
// is updated from there **before** the |mWindowDecorViewModel| methods are invoked.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java
index 65099c2dfb9d..671eae3d84ef 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java
@@ -153,7 +153,12 @@ public class PhonePipMenuController implements PipMenuController,
mPipUiEventLogger = pipUiEventLogger;
mPipTransitionState.addPipTransitionStateChangedListener(this);
-
+ // Clear actions after exit PiP. Otherwise, next PiP could accidentally inherit the
+ // actions provided by the previous app in PiP mode.
+ mPipBoundsState.addOnPipComponentChangedListener(((oldPipComponent, newPipComponent) -> {
+ if (mAppActions != null) mAppActions.clear();
+ mCloseAction = null;
+ }));
mPipTaskListener.addParamsChangedListener(new PipTaskListener.PipParamsChangedCallback() {
@Override
public void onActionsChanged(List<RemoteAction> actions, RemoteAction closeAction) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java
index 383afcf6f821..f81f330e50c4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java
@@ -20,6 +20,7 @@ import android.app.PictureInPictureParams;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.Rect;
+import android.os.Bundle;
import android.os.SystemProperties;
import android.view.SurfaceControl;
import android.window.WindowContainerToken;
@@ -47,7 +48,7 @@ import java.util.function.Supplier;
/**
* Scheduler for Shell initiated PiP transitions and animations.
*/
-public class PipScheduler {
+public class PipScheduler implements PipTransitionState.PipTransitionStateChangedListener {
private static final String TAG = PipScheduler.class.getSimpleName();
/**
@@ -71,6 +72,7 @@ public class PipScheduler {
private final PipSurfaceTransactionHelper mPipSurfaceTransactionHelper;
@Nullable private Runnable mUpdateMovementBoundsRunnable;
+ @Nullable private PipAlphaAnimator mOverlayFadeoutAnimator;
private PipAlphaAnimatorSupplier mPipAlphaAnimatorSupplier;
private Supplier<PictureInPictureParams> mPipParamsSupplier;
@@ -85,6 +87,7 @@ public class PipScheduler {
mPipBoundsState = pipBoundsState;
mMainExecutor = mainExecutor;
mPipTransitionState = pipTransitionState;
+ mPipTransitionState.addPipTransitionStateChangedListener(this);
mPipDesktopState = pipDesktopState;
mSplitScreenControllerOptional = splitScreenControllerOptional;
@@ -238,12 +241,16 @@ public class PipScheduler {
void startOverlayFadeoutAnimation(@NonNull SurfaceControl overlayLeash,
boolean withStartDelay, @NonNull Runnable onAnimationEnd) {
- PipAlphaAnimator animator = mPipAlphaAnimatorSupplier.get(mContext, overlayLeash,
+ mOverlayFadeoutAnimator = mPipAlphaAnimatorSupplier.get(mContext, overlayLeash,
null /* startTx */, null /* finishTx */, PipAlphaAnimator.FADE_OUT);
- animator.setDuration(CONTENT_OVERLAY_FADE_OUT_DURATION_MS);
- animator.setStartDelay(withStartDelay ? EXTRA_CONTENT_OVERLAY_FADE_OUT_DELAY_MS : 0);
- animator.setAnimationEndCallback(onAnimationEnd);
- animator.start();
+ mOverlayFadeoutAnimator.setDuration(CONTENT_OVERLAY_FADE_OUT_DURATION_MS);
+ mOverlayFadeoutAnimator.setStartDelay(withStartDelay
+ ? EXTRA_CONTENT_OVERLAY_FADE_OUT_DELAY_MS : 0);
+ mOverlayFadeoutAnimator.setAnimationEndCallback(() -> {
+ onAnimationEnd.run();
+ mOverlayFadeoutAnimator = null;
+ });
+ mOverlayFadeoutAnimator.start();
}
void setUpdateMovementBoundsRunnable(@Nullable Runnable updateMovementBoundsRunnable) {
@@ -289,6 +296,21 @@ public class PipScheduler {
mSurfaceControlTransactionFactory = factory;
}
+ @Override
+ public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState,
+ @PipTransitionState.TransitionState int newState,
+ @android.annotation.Nullable Bundle extra) {
+ switch (newState) {
+ case PipTransitionState.EXITING_PIP:
+ case PipTransitionState.SCHEDULED_BOUNDS_CHANGE:
+ if (mOverlayFadeoutAnimator != null && mOverlayFadeoutAnimator.isStarted()) {
+ mOverlayFadeoutAnimator.end();
+ mOverlayFadeoutAnimator = null;
+ }
+ break;
+ }
+ }
+
@VisibleForTesting
interface PipAlphaAnimatorSupplier {
PipAlphaAnimator get(@NonNull Context context,
@@ -303,6 +325,17 @@ public class PipScheduler {
mPipAlphaAnimatorSupplier = supplier;
}
+ @VisibleForTesting
+ void setOverlayFadeoutAnimator(@NonNull PipAlphaAnimator animator) {
+ mOverlayFadeoutAnimator = animator;
+ }
+
+ @VisibleForTesting
+ @Nullable
+ PipAlphaAnimator getOverlayFadeoutAnimator() {
+ return mOverlayFadeoutAnimator;
+ }
+
void setPipParamsSupplier(@NonNull Supplier<PictureInPictureParams> pipParamsSupplier) {
mPipParamsSupplier = pipParamsSupplier;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java
index d6634845ee21..294ef48c01d0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java
@@ -61,7 +61,7 @@ public class PipTaskListener implements ShellTaskOrganizer.TaskListener,
private final PipBoundsState mPipBoundsState;
private final PipBoundsAlgorithm mPipBoundsAlgorithm;
private final ShellExecutor mMainExecutor;
- private final PictureInPictureParams mPictureInPictureParams =
+ private PictureInPictureParams mPictureInPictureParams =
new PictureInPictureParams.Builder().build();
private boolean mWaitingForAspectRatioChange = false;
@@ -92,6 +92,11 @@ public class PipTaskListener implements ShellTaskOrganizer.TaskListener,
}
mPipResizeAnimatorSupplier = PipResizeAnimator::new;
mPipScheduler.setPipParamsSupplier(this::getPictureInPictureParams);
+ // Reset {@link #mPictureInPictureParams} after exiting PiP. For instance, next Activity
+ // with null aspect ratio would accidentally inherit the aspect ratio from a previous
+ // PiP Activity.
+ mPipBoundsState.addOnPipComponentChangedListener(((oldPipComponent, newPipComponent) ->
+ mPictureInPictureParams = new PictureInPictureParams.Builder().build()));
}
void setPictureInPictureParams(@Nullable PictureInPictureParams params) {
@@ -138,9 +143,8 @@ public class PipTaskListener implements ShellTaskOrganizer.TaskListener,
if (mPictureInPictureParams.hasSetAspectRatio()
&& mPipBoundsAlgorithm.isValidPictureInPictureAspectRatio(newAspectRatio)
&& PipUtils.aspectRatioChanged(newAspectRatio, mPipBoundsState.getAspectRatio())) {
- mPipTransitionState.setOnIdlePipTransitionStateRunnable(() -> {
- onAspectRatioChanged(newAspectRatio);
- });
+ mPipTransitionState.setOnIdlePipTransitionStateRunnable(
+ () -> onAspectRatioChanged(newAspectRatio));
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 7472b0ea56ca..c370c0cb0930 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -2920,7 +2920,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
prepareEnterSplitScreen(out);
mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(),
TRANSIT_SPLIT_SCREEN_PAIR_OPEN, !mIsDropEntering, SNAP_TO_2_50_50);
- } else if (isSplitScreenVisible() && isOpening) {
+ } else if (enableFlexibleTwoAppSplit() && isSplitScreenVisible() && isOpening) {
// launching into an existing split stage; possibly launchAdjacent
// If we're replacing a pip-able app, we need to let mixed handler take care of
// it. Otherwise we'll just treat it as an enter+resize
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt
index 01fc6440712d..adc5cdf340fd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt
@@ -43,7 +43,7 @@ class DesktopHandleManageWindowsMenu(
private val captionWidth: Int,
private val windowManagerWrapper: WindowManagerWrapper,
context: Context,
- snapshotList: List<Pair<Int, TaskSnapshot>>,
+ snapshotList: List<Pair<Int, TaskSnapshot?>>,
onIconClickListener: ((Int) -> Unit),
onOutsideClickListener: (() -> Unit)
) : ManageWindowsViewContainer(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt
index 02a5433147ca..3a75933f59d7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt
@@ -54,7 +54,7 @@ class DesktopHeaderManageWindowsMenu(
private val desktopUserRepositories: DesktopUserRepositories,
private val surfaceControlBuilderSupplier: Supplier<SurfaceControl.Builder>,
private val surfaceControlTransactionSupplier: Supplier<SurfaceControl.Transaction>,
- snapshotList: List<Pair<Int, TaskSnapshot>>,
+ snapshotList: List<Pair<Int, TaskSnapshot?>>,
onIconClickListener: ((Int) -> Unit),
onOutsideClickListener: (() -> Unit)
) : ManageWindowsViewContainer(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index ffb81f8c33e5..a1d2774ee428 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -1218,7 +1218,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
if (id == R.id.caption_handle) {
handleCaptionThroughStatusBar(e, decoration);
final boolean wasDragging = mIsDragging;
- updateDragStatus(e.getActionMasked());
+ updateDragStatus(decoration, e);
final boolean upOrCancel = e.getActionMasked() == ACTION_UP
|| e.getActionMasked() == ACTION_CANCEL;
if (wasDragging && upOrCancel) {
@@ -1234,6 +1234,13 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
return false;
}
+ private void setIsDragging(
+ @Nullable DesktopModeWindowDecoration decor, boolean isDragging) {
+ mIsDragging = isDragging;
+ if (decor == null) return;
+ decor.setIsDragging(isDragging);
+ }
+
private boolean handleFreeformMotionEvent(DesktopModeWindowDecoration decoration,
RunningTaskInfo taskInfo, View v, MotionEvent e) {
final int id = v.getId();
@@ -1253,7 +1260,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
final Rect initialBounds = mDragPositioningCallback.onDragPositioningStart(
0 /* ctrlType */, e.getDisplayId(), e.getRawX(0),
e.getRawY(0));
- updateDragStatus(e.getActionMasked());
+ updateDragStatus(decoration, e);
mOnDragStartInitialBounds.set(initialBounds);
}
// Do not consume input event if a button is touched, otherwise it would
@@ -1280,7 +1287,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
newTaskBounds);
// Flip mIsDragging only if the bounds actually changed.
if (mIsDragging || !newTaskBounds.equals(mOnDragStartInitialBounds)) {
- updateDragStatus(e.getActionMasked());
+ updateDragStatus(decoration, e);
}
return true;
}
@@ -1313,7 +1320,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
// onClick call that results.
return false;
} else {
- updateDragStatus(e.getActionMasked());
+ updateDragStatus(decoration, e);
return true;
}
}
@@ -1321,16 +1328,16 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
return true;
}
- private void updateDragStatus(int eventAction) {
- switch (eventAction) {
+ private void updateDragStatus(DesktopModeWindowDecoration decor, MotionEvent e) {
+ switch (e.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
- mIsDragging = false;
+ setIsDragging(decor, false /* isDragging */);
break;
}
case MotionEvent.ACTION_MOVE: {
- mIsDragging = true;
+ setIsDragging(decor, true /* isDragging */);
break;
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 2a5315739396..673c3f66cb6c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -29,6 +29,7 @@ import static android.window.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_
import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopMode;
import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopModeOrShowAppHandle;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.isDesktopModeSupportedOnDisplay;
import static com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
import static com.android.wm.shell.windowdecor.DragPositioningCallbackUtility.DragEventListener;
@@ -207,7 +208,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository;
private final DesktopUserRepositories mDesktopUserRepositories;
private boolean mIsRecentsTransitionRunning = false;
-
+ private boolean mIsDragging = false;
private Runnable mLoadAppInfoRunnable;
private Runnable mSetAppInfoRunnable;
@@ -513,7 +514,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
updateRelayoutParams(mRelayoutParams, mContext, taskInfo, mSplitScreenController,
applyStartTransactionOnDraw, shouldSetTaskVisibilityPositionAndCrop,
mIsStatusBarVisible, mIsKeyguardVisibleAndOccluded, inFullImmersive,
- mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus,
+ mIsDragging, mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus,
displayExclusionRegion, mIsRecentsTransitionRunning,
mDesktopModeCompatPolicy.shouldExcludeCaptionFromAppBounds(taskInfo));
@@ -911,6 +912,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
boolean isStatusBarVisible,
boolean isKeyguardVisibleAndOccluded,
boolean inFullImmersiveMode,
+ boolean isDragging,
@NonNull InsetsState displayInsetsState,
boolean hasGlobalFocus,
@NonNull Region displayExclusionRegion,
@@ -933,9 +935,13 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
relayoutParams.mAsyncViewHost = isAppHandle;
final boolean showCaption;
- if (DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue()) {
+ if (DesktopModeFlags.ENABLE_DESKTOP_IMMERSIVE_DRAG_BUGFIX.isTrue() && isDragging) {
+ // If the task is being dragged, the caption should not be hidden so that it continues
+ // receiving input
+ showCaption = true;
+ } else if (DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue()) {
if (inFullImmersiveMode) {
- showCaption = isStatusBarVisible && !isKeyguardVisibleAndOccluded;
+ showCaption = (isStatusBarVisible && !isKeyguardVisibleAndOccluded);
} else {
showCaption = taskInfo.isFreeform()
|| (isStatusBarVisible && !isKeyguardVisibleAndOccluded);
@@ -1405,7 +1411,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
supportsMultiInstance,
shouldShowManageWindowsButton,
shouldShowChangeAspectRatioButton,
- canEnterDesktopMode(mContext),
+ isDesktopModeSupportedOnDisplay(mContext, mDisplay),
isBrowserApp,
isBrowserApp ? getAppLink() : getBrowserLink(),
mResult.mCaptionWidth,
@@ -1799,6 +1805,13 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
}
/**
+ * Declares whether the window decoration is being dragged.
+ */
+ void setIsDragging(boolean isDragging) {
+ mIsDragging = isDragging;
+ }
+
+ /**
* Called when there is a {@link MotionEvent#ACTION_HOVER_EXIT} on the maximize window button.
*/
void onMaximizeButtonHoverExit() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
index ce786177290f..c544468f5191 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
@@ -39,7 +39,6 @@ import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.Space
-import android.widget.TextView
import android.window.DesktopModeFlags
import android.window.SurfaceSyncGroup
import androidx.annotation.StringRes
@@ -59,10 +58,10 @@ import com.android.wm.shell.splitscreen.SplitScreenController
import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer
import com.android.wm.shell.windowdecor.common.DecorThemeUtil
+import com.android.wm.shell.windowdecor.common.DrawableInsets
import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader
import com.android.wm.shell.windowdecor.common.calculateMenuPosition
-import com.android.wm.shell.windowdecor.common.DrawableInsets
-import com.android.wm.shell.windowdecor.common.createRippleDrawable
+import com.android.wm.shell.windowdecor.common.createBackgroundDrawable
import com.android.wm.shell.windowdecor.extension.isFullscreen
import com.android.wm.shell.windowdecor.extension.isMultiWindow
import com.android.wm.shell.windowdecor.extension.isPinned
@@ -478,13 +477,13 @@ class HandleMenu(
R.dimen.desktop_mode_handle_menu_icon_button_ripple_inset_base)
private val iconButtonRippleRadius = context.resources.getDimensionPixelSize(
R.dimen.desktop_mode_handle_menu_icon_button_ripple_radius)
- private val iconButtonDrawableInsetsBase = DrawableInsets(t=iconButtondrawableBaseInset,
- b=iconButtondrawableBaseInset, l=iconButtondrawableBaseInset,
- r=iconButtondrawableBaseInset)
- private val iconButtonDrawableInsetsLeft = DrawableInsets(t=iconButtondrawableBaseInset,
- b=iconButtondrawableBaseInset, l=iconButtondrawableShiftInset, r=0)
- private val iconButtonDrawableInsetsRight = DrawableInsets(t=iconButtondrawableBaseInset,
- b=iconButtondrawableBaseInset, l=0, r=iconButtondrawableShiftInset)
+ private val iconButtonDrawableInsetsBase = DrawableInsets(t = iconButtondrawableBaseInset,
+ b = iconButtondrawableBaseInset, l = iconButtondrawableBaseInset,
+ r = iconButtondrawableBaseInset)
+ private val iconButtonDrawableInsetsLeft = DrawableInsets(t = iconButtondrawableBaseInset,
+ b = iconButtondrawableBaseInset, l = iconButtondrawableShiftInset, r = 0)
+ private val iconButtonDrawableInsetsRight = DrawableInsets(t = iconButtondrawableBaseInset,
+ b = iconButtondrawableBaseInset, l = 0, r = iconButtondrawableShiftInset)
// App Info Pill.
private val appInfoPill = rootView.requireViewById<View>(R.id.app_info_pill)
@@ -702,7 +701,7 @@ class HandleMenu(
imageTintList = ColorStateList.valueOf(style.textColor)
this.taskInfo = this@HandleMenuView.taskInfo
- background = createRippleDrawable(
+ background = createBackgroundDrawable(
color = style.textColor,
cornerRadius = iconButtonRippleRadius,
drawableInsets = iconButtonDrawableInsetsBase
@@ -740,7 +739,7 @@ class HandleMenu(
else iconButtonDrawableInsetsRight
fullscreenBtn.apply {
- background = createRippleDrawable(
+ background = createBackgroundDrawable(
color = style.textColor,
cornerRadius = iconButtonRippleRadius,
drawableInsets = startInsets
@@ -748,7 +747,7 @@ class HandleMenu(
}
splitscreenBtn.apply {
- background = createRippleDrawable(
+ background = createBackgroundDrawable(
color = style.textColor,
cornerRadius = iconButtonRippleRadius,
drawableInsets = iconButtonDrawableInsetsBase
@@ -756,7 +755,7 @@ class HandleMenu(
}
floatingBtn.apply {
- background = createRippleDrawable(
+ background = createBackgroundDrawable(
color = style.textColor,
cornerRadius = iconButtonRippleRadius,
drawableInsets = iconButtonDrawableInsetsBase
@@ -764,7 +763,7 @@ class HandleMenu(
}
desktopBtn.apply {
- background = createRippleDrawable(
+ background = createBackgroundDrawable(
color = style.textColor,
cornerRadius = iconButtonRippleRadius,
drawableInsets = endInsets
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt
index 22bc9782170b..cf536eba8382 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt
@@ -42,8 +42,6 @@ class MoveToDesktopAnimator @JvmOverloads constructor(
.setDuration(ANIMATION_DURATION.toLong())
.apply {
val t = SurfaceControl.Transaction()
- val cornerRadius = context.resources
- .getDimensionPixelSize(R.dimen.desktop_mode_dragged_task_radius).toFloat()
addUpdateListener {
setTaskPosition(mostRecentInput.x, mostRecentInput.y)
t.setScale(taskSurface, scale, scale)
@@ -57,6 +55,8 @@ class MoveToDesktopAnimator @JvmOverloads constructor(
val taskId get() = taskInfo.taskId
val position: PointF = PointF(0.0f, 0.0f)
+ val cornerRadius: Float = context.resources
+ .getDimensionPixelSize(R.dimen.desktop_mode_dragged_task_radius).toFloat()
/**
* Whether motion events from the drag gesture should affect the dragged surface or not. Used
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index 7baef2b2dc97..bde46a1bc375 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -361,6 +361,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
outResult.mRootView = rootView;
final boolean fontScaleChanged = mWindowDecorConfig != null
&& mWindowDecorConfig.fontScale != mTaskInfo.configuration.fontScale;
+ final boolean localeListChanged = mWindowDecorConfig != null
+ && !mWindowDecorConfig.getLocales()
+ .equals(mTaskInfo.getConfiguration().getLocales());
final int oldDensityDpi = mWindowDecorConfig != null
? mWindowDecorConfig.densityDpi : DENSITY_DPI_UNDEFINED;
final int oldNightMode = mWindowDecorConfig != null
@@ -376,7 +379,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
|| oldLayoutResId != mLayoutResId
|| oldNightMode != newNightMode
|| mDecorWindowContext == null
- || fontScaleChanged) {
+ || fontScaleChanged
+ || localeListChanged) {
releaseViews(wct);
if (!obtainDisplayOrRegisterListener()) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt
index f44b15a23b90..f08cfa987cc7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt
@@ -20,7 +20,6 @@ import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
-import android.graphics.drawable.RippleDrawable
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.RoundRectShape
@@ -48,45 +47,6 @@ fun replaceColorAlpha(@ColorInt color: Int, alpha: Int): Int {
}
/**
- * Creates a RippleDrawable with specified color, corner radius, and insets.
- */
-fun createRippleDrawable(
- @ColorInt color: Int,
- cornerRadius: Int,
- drawableInsets: DrawableInsets,
-): RippleDrawable {
- return RippleDrawable(
- ColorStateList(
- arrayOf(
- intArrayOf(android.R.attr.state_hovered),
- intArrayOf(android.R.attr.state_pressed),
- intArrayOf(),
- ),
- intArrayOf(
- replaceColorAlpha(color, OPACITY_11),
- replaceColorAlpha(color, OPACITY_15),
- Color.TRANSPARENT,
- )
- ),
- null /* content */,
- LayerDrawable(arrayOf(
- ShapeDrawable().apply {
- shape = RoundRectShape(
- FloatArray(8) { cornerRadius.toFloat() },
- null /* inset */,
- null /* innerRadii */
- )
- paint.color = Color.WHITE
- }
- )).apply {
- require(numberOfLayers == 1) { "Must only contain one layer" }
- setLayerInset(0 /* index */,
- drawableInsets.l, drawableInsets.t, drawableInsets.r, drawableInsets.b)
- }
- )
-}
-
-/**
* Creates a background drawable with specified color, corner radius, and insets.
*/
fun createBackgroundDrawable(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt
index 801048adda4d..957898fd0088 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt
@@ -21,6 +21,7 @@ import android.content.Context
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.graphics.Bitmap
+import android.os.LocaleList
import android.os.UserHandle
import androidx.tracing.Trace
import com.android.internal.annotations.VisibleForTesting
@@ -80,6 +81,13 @@ class WindowDecorTaskResourceLoader(
*/
private val existingTasks = mutableSetOf<Int>()
+ /**
+ * A map of task -> localeList to keep track of the language of app name that's currently
+ * cached in |taskToResourceCache|.
+ */
+ @VisibleForTesting
+ val localeListOnCache = ConcurrentHashMap<Int, LocaleList>()
+
init {
shellInit.addInitCallback(this::onInit, this)
}
@@ -99,11 +107,14 @@ class WindowDecorTaskResourceLoader(
fun getName(taskInfo: RunningTaskInfo): CharSequence {
checkWindowDecorExists(taskInfo)
val cachedResources = taskToResourceCache[taskInfo.taskId]
- if (cachedResources != null) {
+ val localeListActiveOnCacheTime = localeListOnCache[taskInfo.taskId]
+ if (cachedResources != null &&
+ taskInfo.getConfiguration().getLocales().equals(localeListActiveOnCacheTime)) {
return cachedResources.appName
}
val resources = loadAppResources(taskInfo)
taskToResourceCache[taskInfo.taskId] = resources
+ localeListOnCache[taskInfo.taskId] = taskInfo.getConfiguration().getLocales()
return resources.appName
}
@@ -117,6 +128,7 @@ class WindowDecorTaskResourceLoader(
}
val resources = loadAppResources(taskInfo)
taskToResourceCache[taskInfo.taskId] = resources
+ localeListOnCache[taskInfo.taskId] = taskInfo.getConfiguration().getLocales()
return resources.appIcon
}
@@ -130,6 +142,7 @@ class WindowDecorTaskResourceLoader(
}
val resources = loadAppResources(taskInfo)
taskToResourceCache[taskInfo.taskId] = resources
+ localeListOnCache[taskInfo.taskId] = taskInfo.getConfiguration().getLocales()
return resources.veilIcon
}
@@ -142,6 +155,7 @@ class WindowDecorTaskResourceLoader(
fun onWindowDecorClosed(taskInfo: RunningTaskInfo) {
existingTasks.remove(taskInfo.taskId)
taskToResourceCache.remove(taskInfo.taskId)
+ localeListOnCache.remove(taskInfo.taskId)
}
private fun checkWindowDecorExists(taskInfo: RunningTaskInfo) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
index c3f5b3f20f4a..30712b55bdfa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
@@ -466,11 +466,7 @@ class AppHeaderViewHolder(
override fun onHandleMenuOpened() {}
- override fun onHandleMenuClosed() {
- openMenuButton.post {
- openMenuButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
- }
- }
+ override fun onHandleMenuClosed() {}
fun onMaximizeWindowHoverExit() {
maximizeButtonView.cancelHoverAnimation()
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/UnminimizeAppFromTaskbar.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/UnminimizeAppFromTaskbar.kt
index 3e98e4342b3f..7d9f2bf8fdf6 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/UnminimizeAppFromTaskbar.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/UnminimizeAppFromTaskbar.kt
@@ -19,7 +19,7 @@ package com.android.wm.shell.scenarios
import android.app.Instrumentation
import android.tools.NavBar
import android.tools.Rotation
-import android.tools.device.apphelpers.BrowserAppHelper
+import android.tools.device.apphelpers.GmailAppHelper
import android.tools.flicker.rules.ChangeDisplayOrientationRule
import android.tools.traces.parsers.WindowManagerStateHelper
import androidx.test.platform.app.InstrumentationRegistry
@@ -44,8 +44,8 @@ abstract class UnminimizeAppFromTaskbar(val rotation: Rotation = Rotation.ROTATI
private val wmHelper = WindowManagerStateHelper(instrumentation)
private val device = UiDevice.getInstance(instrumentation)
private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation))
- private val browserHelper = BrowserAppHelper(instrumentation)
- private val browserApp = DesktopModeAppHelper(browserHelper)
+ private val gmailHelper = GmailAppHelper(instrumentation)
+ private val gmailApp = DesktopModeAppHelper(gmailHelper)
@Rule
@JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation)
@@ -59,20 +59,20 @@ abstract class UnminimizeAppFromTaskbar(val rotation: Rotation = Rotation.ROTATI
ChangeDisplayOrientationRule.setRotation(rotation)
testApp.enterDesktopMode(wmHelper, device)
tapl.showTaskbarIfHidden()
- browserApp.launchViaIntent(wmHelper)
- browserApp.minimizeDesktopApp(wmHelper, device)
+ gmailApp.launchViaIntent(wmHelper)
+ gmailApp.minimizeDesktopApp(wmHelper, device)
}
@Test
open fun unminimizeApp() {
tapl.launchedAppState.taskbar
- .getAppIcon(browserHelper.appName)
- .launch(browserHelper.packageName)
+ .getAppIcon(gmailHelper.appName)
+ .launch(gmailHelper.packageName)
}
@After
fun teardown() {
testApp.exit(wmHelper)
- browserApp.exit(wmHelper)
+ gmailApp.exit(wmHelper)
}
} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml
index 1de47df78853..e51447f5cfcb 100644
--- a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml
@@ -50,6 +50,8 @@
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
<!-- Disable AOD -->
<option name="run-command" value="settings put secure doze_always_on 0"/>
+ <!-- Disable explore hub mode -->
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml
index 34d001c858f6..7659ec903480 100644
--- a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml
@@ -50,6 +50,8 @@
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
<!-- Disable AOD -->
<option name="run-command" value="settings put secure doze_always_on 0"/>
+ <!-- Disable explore hub mode -->
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml
index 34d001c858f6..7659ec903480 100644
--- a/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml
@@ -50,6 +50,8 @@
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
<!-- Disable AOD -->
<option name="run-command" value="settings put secure doze_always_on 0"/>
+ <!-- Disable explore hub mode -->
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml
index 9c1a8f17aeee..a4ecac9dfeb0 100644
--- a/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml
@@ -50,6 +50,8 @@
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
<!-- Disable AOD -->
<option name="run-command" value="settings put secure doze_always_on 0"/>
+ <!-- Disable explore hub mode -->
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml
index ae73dae99d6f..75ffdc69c73b 100644
--- a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml
@@ -50,6 +50,8 @@
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
<!-- Disable AOD -->
<option name="run-command" value="settings put secure doze_always_on 0"/>
+ <!-- Disable explore hub mode -->
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml
index a136936c0838..8003cbaada50 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml
@@ -50,6 +50,8 @@
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
<!-- Disable AOD -->
<option name="run-command" value="settings put secure doze_always_on 0"/>
+ <!-- Disable explore hub mode -->
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java
index 3c79ea7be39f..925ca0f1638d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java
@@ -221,8 +221,8 @@ public class BubbleTransitionsTest extends ShellTestCase {
PointF dragPosition = new PointF(10f, 20f);
BubbleTransitions.DragData dragData = new BubbleTransitions.DragData(
- /* releasedOnLeft= */ false, /* taskScale= */ 0.5f, dragPosition,
- pendingWct);
+ /* releasedOnLeft= */ false, /* taskScale= */ 0.5f, /* cornerRadius= */ 10f,
+ dragPosition, pendingWct);
final BubbleTransitions.BubbleTransition bt = mBubbleTransitions.startConvertToBubble(
mBubble, taskInfo, mExpandedViewManager, mTaskViewFactory, mBubblePositioner,
@@ -253,6 +253,8 @@ public class BubbleTransitionsTest extends ShellTestCase {
verify(startT).setPosition(snapshot, dragPosition.x, dragPosition.y);
// Snapshot has the scale of the dragged task
verify(startT).setScale(snapshot, dragData.getTaskScale(), dragData.getTaskScale());
+ // Snapshot has dragged task corner radius
+ verify(startT).setCornerRadius(snapshot, dragData.getCornerRadius());
}
@Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsStateTest.java
index 01b76edd9b25..1066276becc7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsStateTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsStateTest.java
@@ -19,6 +19,8 @@ package com.android.wm.shell.common.pip;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -128,6 +130,31 @@ public class PipBoundsStateTest extends ShellTestCase {
}
@Test
+ public void setLastPipComponentName_notChanged_doesNotCallbackComponentChangedListener() {
+ mPipBoundsState.setLastPipComponentName(mTestComponentName1);
+ PipBoundsState.OnPipComponentChangedListener mockListener =
+ mock(PipBoundsState.OnPipComponentChangedListener.class);
+
+ mPipBoundsState.addOnPipComponentChangedListener(mockListener);
+ mPipBoundsState.setLastPipComponentName(mTestComponentName1);
+
+ verify(mockListener, never()).onPipComponentChanged(any(), any());
+ }
+
+ @Test
+ public void setLastPipComponentName_changed_callbackComponentChangedListener() {
+ mPipBoundsState.setLastPipComponentName(mTestComponentName1);
+ PipBoundsState.OnPipComponentChangedListener mockListener =
+ mock(PipBoundsState.OnPipComponentChangedListener.class);
+
+ mPipBoundsState.addOnPipComponentChangedListener(mockListener);
+ mPipBoundsState.setLastPipComponentName(mTestComponentName2);
+
+ verify(mockListener).onPipComponentChanged(
+ eq(mTestComponentName1), eq(mTestComponentName2));
+ }
+
+ @Test
public void testSetLastPipComponentName_notChanged_doesNotClearReentryState() {
mPipBoundsState.setLastPipComponentName(mTestComponentName1);
mPipBoundsState.saveReentryState(DEFAULT_SNAP_FRACTION);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
index 598a101b8bcd..597e4a55ed0e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
@@ -59,6 +59,7 @@ import com.android.wm.shell.common.DockStateReader;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.compatui.api.CompatUIInfo;
+import com.android.wm.shell.compatui.impl.CompatUIRequests;
import com.android.wm.shell.desktopmode.DesktopRepository;
import com.android.wm.shell.desktopmode.DesktopUserRepositories;
import com.android.wm.shell.sysui.ShellController;
@@ -738,6 +739,22 @@ public class CompatUIControllerTest extends ShellTestCase {
verify(mController, never()).removeLayouts(taskInfo.taskId);
}
+ @Test
+ @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
+ public void testSendCompatUIRequest_createRestartDialog() {
+ TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ false);
+ doReturn(true).when(mMockRestartDialogLayout)
+ .needsToBeRecreated(any(TaskInfo.class),
+ any(ShellTaskOrganizer.TaskListener.class));
+ doReturn(true).when(mCompatUIConfiguration).isRestartDialogEnabled();
+ doReturn(true).when(mCompatUIConfiguration).shouldShowRestartDialogAgain(eq(taskInfo));
+
+ mController.sendCompatUIRequest(new CompatUIRequests.DisplayCompatShowRestartDialog(
+ taskInfo, mMockTaskListener));
+ verify(mController).createRestartDialogWindowManager(any(), eq(taskInfo),
+ eq(mMockTaskListener));
+ }
+
private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat) {
return createTaskInfo(displayId, taskId, hasSizeCompat, /* isVisible */ false,
/* isFocused */ false, /* isTopActivityTransparent */ false);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/crashhandling/ShellCrashHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/crashhandling/ShellCrashHandlerTest.kt
new file mode 100644
index 000000000000..5c77f78a7dfa
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/crashhandling/ShellCrashHandlerTest.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.crashhandling
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.PendingIntent
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.platform.test.annotations.DisableFlags
+import android.view.Display.DEFAULT_DISPLAY
+import android.window.IWindowContainerToken
+import android.window.WindowContainerToken
+import android.window.WindowContainerTransaction
+import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_PENDING_INTENT
+import com.android.modules.utils.testing.ExtendedMockitoRule
+import com.android.window.flags.Flags
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.common.HomeIntentProvider
+import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.android.wm.shell.sysui.ShellInit
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import org.junit.Before
+import org.junit.Rule
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+class ShellCrashHandlerTest : ShellTestCase() {
+ @JvmField
+ @Rule
+ val extendedMockitoRule =
+ ExtendedMockitoRule.Builder(this)
+ .mockStatic(DesktopModeStatus::class.java)
+ .mockStatic(PendingIntent::class.java)
+ .build()!!
+
+ private val testExecutor = mock<ShellExecutor>()
+ private val context = mock<Context>()
+ private val shellTaskOrganizer = mock<ShellTaskOrganizer>()
+
+ private lateinit var homeIntentProvider: HomeIntentProvider
+ private lateinit var crashHandler: ShellCrashHandler
+ private lateinit var shellInit: ShellInit
+
+
+ @Before
+ fun setup() {
+ whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true)
+ whenever(PendingIntent.getActivity(any(), any(), any(), any(), any())).thenReturn(mock())
+
+ shellInit = spy(ShellInit(testExecutor))
+
+ homeIntentProvider = HomeIntentProvider(context)
+ crashHandler = ShellCrashHandler(context, shellTaskOrganizer, homeIntentProvider, shellInit)
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun init_freeformTaskExists_sendsHomeIntent() {
+ val wctCaptor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ whenever(shellTaskOrganizer.getRunningTasks()).thenReturn(arrayListOf(createTaskInfo(1)))
+
+ shellInit.init()
+
+ verify(shellTaskOrganizer).applyTransaction(
+ wctCaptor.capture()
+ )
+ wctCaptor.value.assertPendingIntentAt(0, launchHomeIntent(DEFAULT_DISPLAY))
+ }
+
+ private fun launchHomeIntent(displayId: Int): Intent {
+ return Intent(Intent.ACTION_MAIN).apply {
+ if (displayId != DEFAULT_DISPLAY) {
+ addCategory(Intent.CATEGORY_SECONDARY_HOME)
+ } else {
+ addCategory(Intent.CATEGORY_HOME)
+ }
+ }
+ }
+
+ private fun createTaskInfo(id: Int, windowingMode: Int = WINDOWING_MODE_FREEFORM) =
+ RunningTaskInfo().apply {
+ taskId = id
+ displayId = DEFAULT_DISPLAY
+ configuration.windowConfiguration.windowingMode = windowingMode
+ token = WindowContainerToken(Mockito.mock(IWindowContainerToken::class.java))
+ baseIntent = Intent().apply { component = ComponentName("package", "component.name") }
+ }
+
+ private fun WindowContainerTransaction.assertPendingIntentAt(index: Int, intent: Intent) {
+ val op = hierarchyOps[index]
+ assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_PENDING_INTENT)
+ assertThat(op.activityIntent?.component).isEqualTo(intent.component)
+ assertThat(op.activityIntent?.categories).isEqualTo(intent.categories)
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt
index 8ad54f5a0bb4..275d7b73a112 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt
@@ -28,14 +28,22 @@ import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener
import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.sysui.ShellInit
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.TestScope
+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
import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.whenever
@@ -46,15 +54,18 @@ import org.mockito.quality.Strictness
*
* Usage: atest WMShellUnitTests:DesktopDisplayEventHandlerTest
*/
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidTestingRunner::class)
class DesktopDisplayEventHandlerTest : ShellTestCase() {
@Mock lateinit var testExecutor: ShellExecutor
@Mock lateinit var displayController: DisplayController
@Mock private lateinit var mockDesktopUserRepositories: DesktopUserRepositories
+ @Mock private lateinit var mockDesktopRepositoryInitializer: DesktopRepositoryInitializer
@Mock private lateinit var mockDesktopRepository: DesktopRepository
@Mock private lateinit var mockDesktopTasksController: DesktopTasksController
@Mock private lateinit var desktopDisplayModeController: DesktopDisplayModeController
+ private val testScope = TestScope()
private lateinit var mockitoSession: StaticMockitoSession
private lateinit var shellInit: ShellInit
@@ -77,7 +88,9 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() {
DesktopDisplayEventHandler(
context,
shellInit,
+ testScope.backgroundScope,
displayController,
+ mockDesktopRepositoryInitializer,
mockDesktopUserRepositories,
mockDesktopTasksController,
desktopDisplayModeController,
@@ -89,17 +102,66 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() {
@After
fun tearDown() {
+ testScope.cancel()
mockitoSession.finishMocking()
}
@Test
- fun testDisplayAdded_supportsDesks_createsDesk() {
- whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
+ fun testDisplayAdded_supportsDesks_desktopRepositoryInitialized_createsDesk() =
+ testScope.runTest {
+ val stateFlow = MutableStateFlow(false)
+ whenever(mockDesktopRepositoryInitializer.isInitialized).thenReturn(stateFlow)
+ whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
- onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY)
+ onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY)
+ stateFlow.emit(true)
+ runCurrent()
- verify(mockDesktopTasksController).createDesk(DEFAULT_DISPLAY)
- }
+ verify(mockDesktopTasksController).createDesk(DEFAULT_DISPLAY)
+ }
+
+ @Test
+ fun testDisplayAdded_supportsDesks_desktopRepositoryNotInitialized_doesNotCreateDesk() =
+ testScope.runTest {
+ val stateFlow = MutableStateFlow(false)
+ whenever(mockDesktopRepositoryInitializer.isInitialized).thenReturn(stateFlow)
+ whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
+
+ onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY)
+ runCurrent()
+
+ verify(mockDesktopTasksController, never()).createDesk(DEFAULT_DISPLAY)
+ }
+
+ @Test
+ fun testDisplayAdded_supportsDesks_desktopRepositoryInitializedTwice_createsDeskOnce() =
+ testScope.runTest {
+ val stateFlow = MutableStateFlow(false)
+ whenever(mockDesktopRepositoryInitializer.isInitialized).thenReturn(stateFlow)
+ whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
+
+ onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY)
+ stateFlow.emit(true)
+ stateFlow.emit(true)
+ runCurrent()
+
+ verify(mockDesktopTasksController, times(1)).createDesk(DEFAULT_DISPLAY)
+ }
+
+ @Test
+ fun testDisplayAdded_supportsDesks_desktopRepositoryInitialized_deskExists_doesNotCreateDesk() =
+ testScope.runTest {
+ val stateFlow = MutableStateFlow(false)
+ whenever(mockDesktopRepositoryInitializer.isInitialized).thenReturn(stateFlow)
+ whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
+ whenever(mockDesktopRepository.getNumberOfDesks(DEFAULT_DISPLAY)).thenReturn(1)
+
+ onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY)
+ stateFlow.emit(true)
+ runCurrent()
+
+ verify(mockDesktopTasksController, never()).createDesk(DEFAULT_DISPLAY)
+ }
@Test
fun testDisplayAdded_cannotEnterDesktopMode_doesNotCreateDesk() {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt
index cc37c440f650..450989dd334d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt
@@ -21,9 +21,12 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
import android.content.ContentResolver
+import android.hardware.input.InputManager
import android.os.Binder
+import android.os.Handler
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.FlagsParameterization
import android.provider.Settings
import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS
import android.view.Display.DEFAULT_DISPLAY
@@ -44,6 +47,7 @@ import com.android.wm.shell.transition.Transitions
import com.google.common.truth.Truth.assertThat
import com.google.testing.junit.testparameterinjector.TestParameter
import com.google.testing.junit.testparameterinjector.TestParameterInjector
+import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -64,13 +68,18 @@ import org.mockito.kotlin.whenever
*/
@SmallTest
@RunWith(TestParameterInjector::class)
-class DesktopDisplayModeControllerTest : ShellTestCase() {
+class DesktopDisplayModeControllerTest(
+ @TestParameter(valuesProvider = FlagsParameterizationProvider::class)
+ flags: FlagsParameterization
+) : ShellTestCase() {
private val transitions = mock<Transitions>()
private val rootTaskDisplayAreaOrganizer = mock<RootTaskDisplayAreaOrganizer>()
private val mockWindowManager = mock<IWindowManager>()
private val shellTaskOrganizer = mock<ShellTaskOrganizer>()
private val desktopWallpaperActivityTokenProvider =
mock<DesktopWallpaperActivityTokenProvider>()
+ private val inputManager = mock<InputManager>()
+ private val mainHandler = mock<Handler>()
private lateinit var controller: DesktopDisplayModeController
@@ -82,6 +91,10 @@ class DesktopDisplayModeControllerTest : ShellTestCase() {
private val defaultTDA = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0)
private val wallpaperToken = MockToken().token()
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
+ }
+
@Before
fun setUp() {
whenever(transitions.startTransition(anyInt(), any(), isNull())).thenReturn(Binder())
@@ -95,27 +108,20 @@ class DesktopDisplayModeControllerTest : ShellTestCase() {
mockWindowManager,
shellTaskOrganizer,
desktopWallpaperActivityTokenProvider,
+ inputManager,
+ mainHandler,
)
runningTasks.add(freeformTask)
runningTasks.add(fullscreenTask)
whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(ArrayList(runningTasks))
whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(wallpaperToken)
+ setTabletModeStatus(SwitchState.UNKNOWN)
}
- private fun testDisplayWindowingModeSwitch(
- defaultWindowingMode: Int,
- extendedDisplayEnabled: Boolean,
- expectToSwitch: Boolean,
- ) {
- defaultTDA.configuration.windowConfiguration.windowingMode = defaultWindowingMode
- whenever(mockWindowManager.getWindowingMode(anyInt())).thenReturn(defaultWindowingMode)
- val settingsSession =
- ExtendedDisplaySettingsSession(
- context.contentResolver,
- if (extendedDisplayEnabled) 1 else 0,
- )
-
- settingsSession.use {
+ private fun testDisplayWindowingModeSwitchOnDisplayConnected(expectToSwitch: Boolean) {
+ defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+ whenever(mockWindowManager.getWindowingMode(anyInt())).thenReturn(WINDOWING_MODE_FULLSCREEN)
+ ExtendedDisplaySettingsSession(context.contentResolver, 1).use {
connectExternalDisplay()
if (expectToSwitch) {
// Assumes [connectExternalDisplay] properly triggered the switching transition.
@@ -133,7 +139,7 @@ class DesktopDisplayModeControllerTest : ShellTestCase() {
assertThat(arg.firstValue.changes[wallpaperToken.asBinder()]?.windowingMode)
.isEqualTo(WINDOWING_MODE_FULLSCREEN)
assertThat(arg.secondValue.changes[defaultTDA.token.asBinder()]?.windowingMode)
- .isEqualTo(defaultWindowingMode)
+ .isEqualTo(WINDOWING_MODE_FULLSCREEN)
assertThat(arg.secondValue.changes[wallpaperToken.asBinder()]?.windowingMode)
.isEqualTo(WINDOWING_MODE_FULLSCREEN)
} else {
@@ -144,25 +150,64 @@ class DesktopDisplayModeControllerTest : ShellTestCase() {
@Test
@DisableFlags(Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING)
- fun displayWindowingModeSwitchOnDisplayConnected_flagDisabled(
- @TestParameter param: ModeSwitchTestCase
- ) {
- testDisplayWindowingModeSwitch(
- param.defaultWindowingMode,
- param.extendedDisplayEnabled,
- // When the flag is disabled, never switch.
- expectToSwitch = false,
- )
+ fun displayWindowingModeSwitchOnDisplayConnected_flagDisabled() {
+ // When the flag is disabled, never switch.
+ testDisplayWindowingModeSwitchOnDisplayConnected(/* expectToSwitch= */ false)
}
@Test
@EnableFlags(Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING)
- fun displayWindowingModeSwitchOnDisplayConnected(@TestParameter param: ModeSwitchTestCase) {
- testDisplayWindowingModeSwitch(
- param.defaultWindowingMode,
- param.extendedDisplayEnabled,
- param.expectToSwitchByDefault,
- )
+ fun displayWindowingModeSwitchOnDisplayConnected() {
+ testDisplayWindowingModeSwitchOnDisplayConnected(/* expectToSwitch= */ true)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING)
+ @DisableFlags(Flags.FLAG_FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH)
+ fun testTargetWindowingMode_formfactorDisabled(
+ @TestParameter param: ExternalDisplayBasedTargetModeTestCase,
+ @TestParameter tabletModeStatus: SwitchState,
+ ) {
+ whenever(mockWindowManager.getWindowingMode(anyInt()))
+ .thenReturn(param.defaultWindowingMode)
+ if (param.hasExternalDisplay) {
+ connectExternalDisplay()
+ } else {
+ disconnectExternalDisplay()
+ }
+ setTabletModeStatus(tabletModeStatus)
+
+ ExtendedDisplaySettingsSession(
+ context.contentResolver,
+ if (param.extendedDisplayEnabled) 1 else 0,
+ )
+ .use {
+ assertThat(controller.getTargetWindowingModeForDefaultDisplay())
+ .isEqualTo(param.expectedWindowingMode)
+ }
+ }
+
+ @Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING,
+ Flags.FLAG_FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH,
+ )
+ fun testTargetWindowingMode(@TestParameter param: FormFactorBasedTargetModeTestCase) {
+ if (param.hasExternalDisplay) {
+ connectExternalDisplay()
+ } else {
+ disconnectExternalDisplay()
+ }
+ setTabletModeStatus(param.tabletModeStatus)
+
+ ExtendedDisplaySettingsSession(
+ context.contentResolver,
+ if (param.extendedDisplayEnabled) 1 else 0,
+ )
+ .use {
+ assertThat(controller.getTargetWindowingModeForDefaultDisplay())
+ .isEqualTo(param.expectedWindowingMode)
+ }
}
@Test
@@ -217,6 +262,10 @@ class DesktopDisplayModeControllerTest : ShellTestCase() {
controller.refreshDisplayWindowingMode()
}
+ private fun setTabletModeStatus(status: SwitchState) {
+ whenever(inputManager.isInTabletMode()).thenReturn(status.value)
+ }
+
private class ExtendedDisplaySettingsSession(
private val contentResolver: ContentResolver,
private val overrideValue: Int,
@@ -233,33 +282,158 @@ class DesktopDisplayModeControllerTest : ShellTestCase() {
}
}
+ private class FlagsParameterizationProvider : TestParameterValuesProvider() {
+ override fun provideValues(
+ context: TestParameterValuesProvider.Context
+ ): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf(
+ Flags.FLAG_FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH
+ )
+ }
+ }
+
companion object {
const val EXTERNAL_DISPLAY_ID = 100
- enum class ModeSwitchTestCase(
+ enum class SwitchState(val value: Int) {
+ UNKNOWN(InputManager.SWITCH_STATE_UNKNOWN),
+ ON(InputManager.SWITCH_STATE_ON),
+ OFF(InputManager.SWITCH_STATE_OFF),
+ }
+
+ enum class ExternalDisplayBasedTargetModeTestCase(
val defaultWindowingMode: Int,
+ val hasExternalDisplay: Boolean,
val extendedDisplayEnabled: Boolean,
- val expectToSwitchByDefault: Boolean,
+ val expectedWindowingMode: Int,
) {
- FULLSCREEN_DISPLAY(
+ FREEFORM_EXTERNAL_EXTENDED(
+ defaultWindowingMode = WINDOWING_MODE_FREEFORM,
+ hasExternalDisplay = true,
+ extendedDisplayEnabled = true,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ ),
+ FULLSCREEN_EXTERNAL_EXTENDED(
defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ hasExternalDisplay = true,
+ extendedDisplayEnabled = true,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ ),
+ FREEFORM_NO_EXTERNAL_EXTENDED(
+ defaultWindowingMode = WINDOWING_MODE_FREEFORM,
+ hasExternalDisplay = false,
extendedDisplayEnabled = true,
- expectToSwitchByDefault = true,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ ),
+ FULLSCREEN_NO_EXTERNAL_EXTENDED(
+ defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ hasExternalDisplay = false,
+ extendedDisplayEnabled = true,
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ ),
+ FREEFORM_EXTERNAL_MIRROR(
+ defaultWindowingMode = WINDOWING_MODE_FREEFORM,
+ hasExternalDisplay = true,
+ extendedDisplayEnabled = false,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
),
- FULLSCREEN_DISPLAY_MIRRORING(
+ FULLSCREEN_EXTERNAL_MIRROR(
defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ hasExternalDisplay = true,
extendedDisplayEnabled = false,
- expectToSwitchByDefault = false,
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
),
- FREEFORM_DISPLAY(
+ FREEFORM_NO_EXTERNAL_MIRROR(
defaultWindowingMode = WINDOWING_MODE_FREEFORM,
+ hasExternalDisplay = false,
+ extendedDisplayEnabled = false,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ ),
+ FULLSCREEN_NO_EXTERNAL_MIRROR(
+ defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ hasExternalDisplay = false,
+ extendedDisplayEnabled = false,
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ ),
+ }
+
+ enum class FormFactorBasedTargetModeTestCase(
+ val hasExternalDisplay: Boolean,
+ val extendedDisplayEnabled: Boolean,
+ val tabletModeStatus: SwitchState,
+ val expectedWindowingMode: Int,
+ ) {
+ EXTERNAL_EXTENDED_TABLET(
+ hasExternalDisplay = true,
extendedDisplayEnabled = true,
- expectToSwitchByDefault = false,
+ tabletModeStatus = SwitchState.ON,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
),
- FREEFORM_DISPLAY_MIRRORING(
- defaultWindowingMode = WINDOWING_MODE_FREEFORM,
+ NO_EXTERNAL_EXTENDED_TABLET(
+ hasExternalDisplay = false,
+ extendedDisplayEnabled = true,
+ tabletModeStatus = SwitchState.ON,
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ ),
+ EXTERNAL_MIRROR_TABLET(
+ hasExternalDisplay = true,
+ extendedDisplayEnabled = false,
+ tabletModeStatus = SwitchState.ON,
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ ),
+ NO_EXTERNAL_MIRROR_TABLET(
+ hasExternalDisplay = false,
+ extendedDisplayEnabled = false,
+ tabletModeStatus = SwitchState.ON,
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ ),
+ EXTERNAL_EXTENDED_CLAMSHELL(
+ hasExternalDisplay = true,
+ extendedDisplayEnabled = true,
+ tabletModeStatus = SwitchState.OFF,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ ),
+ NO_EXTERNAL_EXTENDED_CLAMSHELL(
+ hasExternalDisplay = false,
+ extendedDisplayEnabled = true,
+ tabletModeStatus = SwitchState.OFF,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ ),
+ EXTERNAL_MIRROR_CLAMSHELL(
+ hasExternalDisplay = true,
+ extendedDisplayEnabled = false,
+ tabletModeStatus = SwitchState.OFF,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ ),
+ NO_EXTERNAL_MIRROR_CLAMSHELL(
+ hasExternalDisplay = false,
+ extendedDisplayEnabled = false,
+ tabletModeStatus = SwitchState.OFF,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ ),
+ EXTERNAL_EXTENDED_UNKNOWN(
+ hasExternalDisplay = true,
+ extendedDisplayEnabled = true,
+ tabletModeStatus = SwitchState.UNKNOWN,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ ),
+ NO_EXTERNAL_EXTENDED_UNKNOWN(
+ hasExternalDisplay = false,
+ extendedDisplayEnabled = true,
+ tabletModeStatus = SwitchState.UNKNOWN,
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ ),
+ EXTERNAL_MIRROR_UNKNOWN(
+ hasExternalDisplay = true,
+ extendedDisplayEnabled = false,
+ tabletModeStatus = SwitchState.UNKNOWN,
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ ),
+ NO_EXTERNAL_MIRROR_UNKNOWN(
+ hasExternalDisplay = false,
extendedDisplayEnabled = false,
- expectToSwitchByDefault = false,
+ tabletModeStatus = SwitchState.UNKNOWN,
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
),
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt
index 006c3cae121c..4c18ee1500b7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt
@@ -114,6 +114,8 @@ class DesktopImmersiveControllerTest : ShellTestCase() {
transactionSupplier = transactionSupplier,
)
desktopRepository = userRepositories.current
+ desktopRepository.addDesk(DEFAULT_DISPLAY, DEFAULT_DESK_ID)
+ desktopRepository.setActiveDesk(DEFAULT_DISPLAY, DEFAULT_DESK_ID)
}
@Test
@@ -835,5 +837,6 @@ class DesktopImmersiveControllerTest : ShellTestCase() {
companion object {
private val STABLE_BOUNDS = Rect(0, 100, 2000, 1900)
private val DISPLAY_BOUNDS = Rect(0, 0, 2000, 2000)
+ private const val DEFAULT_DESK_ID = 0
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
index e9f92cfd7c56..0c585b3e843a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
@@ -431,6 +431,38 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() {
}
@Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX,
+ Flags.FLAG_ENABLE_DESKTOP_OPENING_DEEPLINK_MINIMIZE_ANIMATION_BUGFIX,
+ )
+ fun startAndAnimateLaunchTransition_withMinimizeChange_wrongTaskId_reparentsMinimizeChange() {
+ val wct = WindowContainerTransaction()
+ val launchingTask = createTask(WINDOWING_MODE_FREEFORM)
+ val minimizingTask = createTask(WINDOWING_MODE_FREEFORM)
+ val launchTaskChange = createChange(launchingTask, mode = TRANSIT_OPEN)
+ val minimizeChange = createChange(minimizingTask)
+ val transition = Binder()
+ whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull()))
+ .thenReturn(transition)
+
+ mixedHandler.startLaunchTransition(
+ transitionType = TRANSIT_OPEN,
+ wct = wct,
+ taskId = Int.MAX_VALUE,
+ minimizingTaskId = minimizingTask.taskId,
+ )
+ mixedHandler.startAnimation(
+ transition,
+ createCloseTransitionInfo(TRANSIT_OPEN, listOf(launchTaskChange, minimizeChange)),
+ SurfaceControl.Transaction(),
+ SurfaceControl.Transaction(),
+ ) {}
+
+ verify(rootTaskDisplayAreaOrganizer)
+ .reparentToDisplayArea(anyInt(), eq(minimizeChange.leash), any())
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX)
fun startAnimation_pendingTransition_noLaunchChange_returnsFalse() {
val wct = WindowContainerTransaction()
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandlerTest.kt
index fbc940663d19..6a99d4770728 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandlerTest.kt
@@ -16,8 +16,10 @@
package com.android.wm.shell.desktopmode
+import android.graphics.Rect
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
+import android.view.SurfaceControl
import android.view.WindowManager
import android.window.TransitionInfo
import androidx.test.filters.SmallTest
@@ -30,6 +32,8 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
@SmallTest
@RunWithLooper
@@ -55,7 +59,9 @@ class DesktopModeMoveToDisplayTransitionHandlerTest : ShellTestCase() {
info =
TransitionInfo(WindowManager.TRANSIT_CHANGE, /* flags= */ 0).apply {
addChange(
- TransitionInfo.Change(mock(), mock()).apply { setDisplayId(1, 1) }
+ TransitionInfo.Change(mock(), mock()).apply {
+ setDisplayId(/* start= */ 1, /* end= */ 1)
+ }
)
},
startTransaction = StubTransaction(),
@@ -74,7 +80,9 @@ class DesktopModeMoveToDisplayTransitionHandlerTest : ShellTestCase() {
info =
TransitionInfo(WindowManager.TRANSIT_CHANGE, /* flags= */ 0).apply {
addChange(
- TransitionInfo.Change(mock(), mock()).apply { setDisplayId(1, 2) }
+ TransitionInfo.Change(mock(), mock()).apply {
+ setDisplayId(/* start= */ 1, /* end= */ 2)
+ }
)
},
startTransaction = StubTransaction(),
@@ -84,4 +92,77 @@ class DesktopModeMoveToDisplayTransitionHandlerTest : ShellTestCase() {
assertTrue("Should animate display change transition", animates)
}
+
+ @Test
+ fun startAnimation_movingActivityEmbedding_shouldSetCorrectBounds() {
+ val leashLeft = mock<SurfaceControl>()
+ val leashRight = mock<SurfaceControl>()
+ val leashContainer = mock<SurfaceControl>()
+ val startTransaction = spy(StubTransaction())
+
+ handler.startAnimation(
+ transition = mock(),
+ info =
+ TransitionInfo(WindowManager.TRANSIT_CHANGE, /* flags= */ 0).apply {
+ addChange(
+ TransitionInfo.Change(mock(), mock()).apply {
+ flags = TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY
+ leash = leashLeft
+ setDisplayId(/* start= */ 1, /* end= */ 2)
+ setEndAbsBounds(
+ Rect(
+ /* left= */ 100,
+ /* top= */ 100,
+ /* right= */ 500,
+ /* bottom= */ 700,
+ )
+ )
+ setEndRelOffset(/* left= */ 0, /* top= */ 0)
+ }
+ )
+ addChange(
+ TransitionInfo.Change(mock(), mock()).apply {
+ flags = TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY
+ leash = leashRight
+ setDisplayId(1, 2)
+ setEndAbsBounds(
+ Rect(
+ /* left= */ 500,
+ /* top= */ 100,
+ /* right= */ 900,
+ /* bottom= */ 700,
+ )
+ )
+ setEndRelOffset(/* left= */ 400, /* top= */ 0)
+ }
+ )
+ addChange(
+ TransitionInfo.Change(mock(), mock()).apply {
+ flags = TransitionInfo.FLAG_TRANSLUCENT
+ leash = leashContainer
+ setDisplayId(/* start= */ 1, /* end= */ 2)
+ setEndAbsBounds(
+ Rect(
+ /* left= */ 100,
+ /* top= */ 100,
+ /* right= */ 900,
+ /* bottom= */ 700,
+ )
+ )
+ setEndRelOffset(/* left= */ 100, /* top= */ 100)
+ }
+ )
+ },
+ startTransaction = startTransaction,
+ finishTransaction = StubTransaction(),
+ finishCallback = mock(),
+ )
+
+ verify(startTransaction).setPosition(leashLeft, 0f, 0f)
+ verify(startTransaction).setPosition(leashRight, 400f, 0f)
+ verify(startTransaction).setPosition(leashContainer, 100f, 100f)
+ verify(startTransaction).setWindowCrop(leashLeft, 400, 600)
+ verify(startTransaction).setWindowCrop(leashRight, 400, 600)
+ verify(startTransaction).setWindowCrop(leashContainer, 800, 600)
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt
index fe1dc29181b9..b859a00c6df4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt
@@ -24,7 +24,6 @@ import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
-import android.view.Display
import android.view.SurfaceControl
import androidx.test.filters.SmallTest
import com.android.internal.policy.SystemBarUtils
@@ -67,7 +66,6 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() {
private lateinit var taskInfo: RunningTaskInfo
@Mock private lateinit var syncQueue: SyncTransactionQueue
@Mock private lateinit var displayController: DisplayController
- @Mock private lateinit var display: Display
@Mock private lateinit var taskSurface: SurfaceControl
@Mock private lateinit var taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
@Mock private lateinit var displayLayout: DisplayLayout
@@ -83,12 +81,20 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() {
whenever(displayLayout.width()).thenReturn(DISPLAY_BOUNDS.width())
whenever(displayLayout.height()).thenReturn(DISPLAY_BOUNDS.height())
whenever(displayLayout.stableInsets()).thenReturn(STABLE_INSETS)
- whenever(displayController.getDisplay(anyInt())).thenReturn(display)
whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout)
whenever(displayController.getDisplay(anyInt())).thenReturn(mContext.display)
whenever(bubbleBoundsProvider.getBubbleBarExpandedViewDropTargetBounds(any()))
.thenReturn(Rect())
taskInfo = DesktopTestHelpers.createFullscreenTask()
+
+ mContext.orCreateTestableResources.addOverride(
+ com.android.internal.R.bool.config_isDesktopModeSupported,
+ true,
+ )
+ mContext.orCreateTestableResources.addOverride(
+ com.android.internal.R.bool.config_canInternalDisplayHostDesktops,
+ true,
+ )
}
@Test
@@ -260,14 +266,9 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() {
)
fun testDefaultIndicatorWithNoDesktop() {
mContext.orCreateTestableResources.addOverride(
- com.android.internal.R.bool.config_isDesktopModeSupported,
+ com.android.internal.R.bool.config_canInternalDisplayHostDesktops,
false,
)
- mContext.orCreateTestableResources.addOverride(
- com.android.internal.R.bool.config_isDesktopModeDevOptionSupported,
- false,
- )
-
// Fullscreen to center, no desktop indicator
createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN)
var result = visualIndicator.updateIndicatorType(PointF(500f, 500f))
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
index de92d391645a..f84a1a38bdfc 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
@@ -1203,6 +1203,17 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() {
}
@Test
+ @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE)
+ fun removeDesk_removesFromPersistence() =
+ runTest(StandardTestDispatcher()) {
+ repo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 2)
+
+ repo.removeDesk(deskId = 2)
+
+ verify(persistentRepository).removeDesktop(DEFAULT_USER_ID, 2)
+ }
+
+ @Test
fun getTaskInFullImmersiveState_byDisplay() {
repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
repo.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 8f499c959759..34c5ebd6d94d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -22,6 +22,7 @@ import android.app.ActivityOptions
import android.app.KeyguardManager
import android.app.PendingIntent
import android.app.PictureInPictureParams
+import android.app.WindowConfiguration
import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
@@ -31,6 +32,7 @@ import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
import android.content.ComponentName
import android.content.Context
import android.content.Intent
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.pm.ActivityInfo
import android.content.pm.ActivityInfo.CONFIG_DENSITY
import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
@@ -101,6 +103,7 @@ import com.android.wm.shell.TestShellExecutor
import com.android.wm.shell.bubbles.BubbleController
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayLayout
+import com.android.wm.shell.common.HomeIntentProvider
import com.android.wm.shell.common.MultiInstanceHelper
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.common.SyncTransactionQueue
@@ -275,6 +278,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
private lateinit var testScope: CoroutineScope
private lateinit var desktopModeCompatPolicy: DesktopModeCompatPolicy
private lateinit var spyContext: TestableContext
+ private lateinit var homeIntentProvider: HomeIntentProvider
private val shellExecutor = TestShellExecutor()
private val bgExecutor = TestShellExecutor()
@@ -294,6 +298,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
private val wallpaperToken = MockToken().token()
private val homeComponentName = ComponentName(HOME_LAUNCHER_PACKAGE_NAME, /* class */ "")
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
+ }
+
@Before
fun setUp() {
Dispatchers.setMain(StandardTestDispatcher())
@@ -323,12 +331,14 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
transitions,
userRepositories,
shellTaskOrganizer,
+ desksOrganizer,
MAX_TASK_LIMIT,
mockInteractionJankMonitor,
mContext,
mockHandler,
)
desktopModeCompatPolicy = spy(DesktopModeCompatPolicy(spyContext))
+ homeIntentProvider = HomeIntentProvider(context)
whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks }
whenever(transitions.startTransition(anyInt(), any(), anyOrNull())).thenAnswer { Binder() }
@@ -428,6 +438,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
dragToDesktopTransitionHandler,
mMockDesktopImmersiveController,
userRepositories,
+ repositoryInitializer,
recentsTransitionHandler,
multiInstanceHelper,
shellExecutor,
@@ -448,6 +459,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
desktopModeCompatPolicy,
dragToDisplayTransitionHandler,
moveToDisplayTransitionHandler,
+ homeIntentProvider,
)
@After
@@ -621,11 +633,13 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun isDesktopModeShowing_noTasks_returnsFalse() {
assertThat(controller.isDesktopModeShowing(displayId = 0)).isFalse()
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun isDesktopModeShowing_noTasksVisible_returnsFalse() {
val task1 = setUpFreeformTask()
val task2 = setUpFreeformTask()
@@ -636,6 +650,15 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun isDesktopModeShowing_noActiveDesk_returnsFalse() {
+ taskRepository.setDeskInactive(deskId = 0)
+
+ assertThat(controller.isDesktopModeShowing(displayId = 0)).isFalse()
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun isDesktopModeShowing_tasksActiveAndVisible_returnsTrue() {
val task1 = setUpFreeformTask()
val task2 = setUpFreeformTask()
@@ -650,6 +673,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY,
Flags.FLAG_INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC,
)
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun isDesktopModeShowing_topTransparentFullscreenTask_returnsTrue() {
val topTransparentTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
taskRepository.setTopTransparentFullscreenTaskId(DEFAULT_DISPLAY, topTransparentTask.taskId)
@@ -659,6 +683,20 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY,
+ Flags.FLAG_INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun isDesktopModeShowing_deskInactive_topTransparentFullscreenTask_returnsTrue() {
+ val topTransparentTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
+ taskRepository.setTopTransparentFullscreenTaskId(DEFAULT_DISPLAY, topTransparentTask.taskId)
+ taskRepository.setDeskInactive(deskId = 0)
+
+ assertThat(controller.isDesktopModeShowing(displayId = DEFAULT_DISPLAY)).isTrue()
+ }
+
+ @Test
+ @EnableFlags(
Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY,
)
@@ -1047,11 +1085,29 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun isAnyDeskActive_noTasks_returnsFalse() {
assertThat(controller.isAnyDeskActive(DEFAULT_DISPLAY)).isFalse()
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun isAnyDeskActive_noActiveDesk_returnsFalse() {
+ taskRepository.setDeskInactive(deskId = 0)
+
+ assertThat(controller.isAnyDeskActive(DEFAULT_DISPLAY)).isFalse()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun isAnyDeskActive_withActiveDesk_returnsTrue() {
+ taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+
+ assertThat(controller.isAnyDeskActive(DEFAULT_DISPLAY)).isTrue()
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun isAnyDeskActive_twoTasks_bothVisible_returnsTrue() {
setUpHomeTask()
@@ -1062,6 +1118,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun isInDesktop_twoTasks_oneVisible_returnsTrue() {
setUpHomeTask()
@@ -1072,6 +1129,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun isAnyDeskActive_twoTasksVisibleOnDifferentDisplays_returnsTrue() {
taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
taskRepository.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
@@ -1091,7 +1149,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
controller.addMoveToDeskTaskChanges(wct, task, deskId = 0)
val finalBounds = findBoundsChange(wct, task)
- assertThat(finalBounds).isEqualTo(Rect())
+ assertThat(finalBounds).isNull()
}
@Test
@@ -1102,7 +1160,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
controller.addMoveToDeskTaskChanges(wct, task, deskId = 0)
val finalBounds = findBoundsChange(wct, task)
- assertThat(finalBounds).isEqualTo(Rect())
+ assertThat(finalBounds).isNull()
}
@Test
@@ -1113,7 +1171,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
controller.addMoveToDeskTaskChanges(wct, task, deskId = 0)
val finalBounds = findBoundsChange(wct, task)
- assertThat(finalBounds).isEqualTo(Rect())
+ assertThat(finalBounds).isNull()
}
@Test
@@ -1124,7 +1182,55 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
controller.addMoveToDeskTaskChanges(wct, task, deskId = 0)
val finalBounds = findBoundsChange(wct, task)
- assertThat(finalBounds).isEqualTo(Rect())
+ assertThat(finalBounds).isNull()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES)
+ fun addMoveToDeskTaskChanges_newTaskInstance_inheritsClosingInstanceBounds() {
+ // Setup existing task.
+ val existingTask = setUpFreeformTask(active = true)
+ val testComponent = ComponentName(/* package */ "test.package", /* class */ "test.class")
+ existingTask.topActivity = testComponent
+ existingTask.configuration.windowConfiguration.setBounds(Rect(0, 0, 500, 500))
+ // Set up new instance of already existing task.
+ val launchingTask = setUpFullscreenTask()
+ launchingTask.topActivity = testComponent
+ launchingTask.baseIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
+
+ // Move new instance to desktop. By default multi instance is not supported so first
+ // instance will close.
+ val wct = WindowContainerTransaction()
+ controller.addMoveToDeskTaskChanges(wct, launchingTask, deskId = 0)
+
+ // New instance should inherit task bounds of old instance.
+ assertThat(findBoundsChange(wct, launchingTask))
+ .isEqualTo(existingTask.configuration.windowConfiguration.bounds)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES)
+ fun handleRequest_newTaskInstance_inheritsClosingInstanceBounds() {
+ setUpLandscapeDisplay()
+ // Setup existing task.
+ val existingTask = setUpFreeformTask(active = true)
+ val testComponent = ComponentName(/* package */ "test.package", /* class */ "test.class")
+ existingTask.topActivity = testComponent
+ existingTask.configuration.windowConfiguration.setBounds(Rect(0, 0, 500, 500))
+ // Set up new instance of already existing task.
+ val launchingTask = setUpFreeformTask(active = false)
+ taskRepository.removeTask(launchingTask.displayId, launchingTask.taskId)
+ launchingTask.topActivity = testComponent
+ launchingTask.baseIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
+
+ // Move new instance to desktop. By default multi instance is not supported so first
+ // instance will close.
+ val wct = controller.handleRequest(Binder(), createTransition(launchingTask))
+
+ assertNotNull(wct, "should handle request")
+ val finalBounds = findBoundsChange(wct, launchingTask)
+ // New instance should inherit task bounds of old instance.
+ assertThat(finalBounds).isEqualTo(existingTask.configuration.windowConfiguration.bounds)
}
@Test
@@ -1808,6 +1914,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
val wallpaperToken = MockToken().token()
whenever(desktopWallpaperActivityTokenProvider.getToken(SECOND_DISPLAY))
.thenReturn(wallpaperToken)
+ taskRepository.addDesk(SECOND_DISPLAY, deskId = 2)
val task = setUpFreeformTask(displayId = SECOND_DISPLAY, deskId = 2, background = true)
controller.moveTaskToDefaultDeskAndActivate(
@@ -2363,6 +2470,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun moveTaskToFront_postsWctWithReorderOp() {
val task1 = setUpFreeformTask()
setUpFreeformTask()
@@ -2385,9 +2493,34 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- fun moveTaskToFront_bringsTasksOverLimit_minimizesBackTask() {
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveTaskToFront_reordersToFront() {
+ val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = 0)
+ setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = 0)
+ whenever(
+ desktopMixedTransitionHandler.startLaunchTransition(
+ eq(TRANSIT_TO_FRONT),
+ any(),
+ eq(task1.taskId),
+ anyOrNull(),
+ anyOrNull(),
+ )
+ )
+ .thenReturn(Binder())
+
+ controller.moveTaskToFront(task1, remoteTransition = null)
+
+ verify(desksOrganizer).reorderTaskToFront(any(), eq(0), eq(task1))
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveTaskToFront_bringsTasksOverLimit_multiDesksDisabled_minimizesBackTask() {
setUpHomeTask()
- val freeformTasks = (1..MAX_TASK_LIMIT + 1).map { _ -> setUpFreeformTask() }
+ val freeformTasks =
+ (1..MAX_TASK_LIMIT + 1).map { _ ->
+ setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = 0)
+ }
whenever(
desktopMixedTransitionHandler.startLaunchTransition(
eq(TRANSIT_TO_FRONT),
@@ -2408,6 +2541,32 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveTaskToFront_bringsTasksOverLimit_multiDesksEnabled_minimizesBackTask() {
+ val deskId = 0
+ setUpHomeTask()
+ val freeformTasks =
+ (1..MAX_TASK_LIMIT + 1).map { _ ->
+ setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ }
+ whenever(
+ desktopMixedTransitionHandler.startLaunchTransition(
+ eq(TRANSIT_TO_FRONT),
+ any(),
+ eq(freeformTasks[0].taskId),
+ anyOrNull(),
+ anyOrNull(),
+ )
+ )
+ .thenReturn(Binder())
+
+ controller.moveTaskToFront(freeformTasks[0], remoteTransition = null)
+
+ val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_TO_FRONT)
+ verify(desksOrganizer).minimizeTask(wct, deskId, freeformTasks[1])
+ }
+
+ @Test
fun moveTaskToFront_minimizedTask_marksTaskAsUnminimized() {
val transition = Binder()
val freeformTask = setUpFreeformTask()
@@ -2496,8 +2655,13 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- fun moveTaskToFront_backgroundTaskBringsTasksOverLimit_minimizesBackTask() {
- val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveTaskToFront_backgroundTaskBringsTasksOverLimit_multiDesksDisabled_minimizesBackTask() {
+ val deskId = 0
+ val freeformTasks =
+ (1..MAX_TASK_LIMIT).map { _ ->
+ setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ }
val task = createRecentTaskInfo(1001)
whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(null)
whenever(
@@ -2520,6 +2684,33 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveTaskToFront_backgroundTaskBringsTasksOverLimit_multiDesksEnabled_minimizesBackTask() {
+ val deskId = 0
+ val freeformTasks =
+ (1..MAX_TASK_LIMIT).map { _ ->
+ setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ }
+ val task = createRecentTaskInfo(1001)
+ whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(null)
+ whenever(
+ desktopMixedTransitionHandler.startLaunchTransition(
+ eq(TRANSIT_OPEN),
+ any(),
+ eq(task.taskId),
+ anyOrNull(),
+ anyOrNull(),
+ )
+ )
+ .thenReturn(Binder())
+
+ controller.moveTaskToFront(task.taskId, unminimizeReason = UnminimizeReason.UNKNOWN)
+
+ val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_OPEN)
+ verify(desksOrganizer).minimizeTask(wct, deskId, freeformTasks[0])
+ }
+
+ @Test
fun moveToNextDisplay_noOtherDisplays() {
whenever(rootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(DEFAULT_DISPLAY))
val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
@@ -2528,7 +2719,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- fun moveToNextDisplay_moveFromFirstToSecondDisplay() {
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveToNextDisplay_moveFromFirstToSecondDisplay_multiDesksDisabled() {
// Set up two display ids
taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
whenever(rootTaskDisplayAreaOrganizer.displayIds)
@@ -2554,7 +2746,27 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- fun moveToNextDisplay_moveFromSecondToFirstDisplay() {
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveToNextDisplay_moveFromFirstToSecondDisplay_multiDesksEnabled() {
+ // Set up two display ids
+ val targetDeskId = 2
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = targetDeskId)
+ whenever(rootTaskDisplayAreaOrganizer.displayIds)
+ .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
+ // Create a mock for the target display area: second display
+ val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0)
+ whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY))
+ .thenReturn(secondDisplayArea)
+
+ val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ controller.moveToNextDisplay(task.taskId)
+
+ verify(desksOrganizer).moveTaskToDesk(any(), eq(targetDeskId), eq(task))
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveToNextDisplay_moveFromSecondToFirstDisplay_multiDesksDisabled() {
// Set up two display ids
whenever(rootTaskDisplayAreaOrganizer.displayIds)
.thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
@@ -2580,6 +2792,25 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveToNextDisplay_moveFromSecondToFirstDisplay_multiDesksEnabled() {
+ // Set up two display ids
+ val targetDeskId = 0
+ whenever(rootTaskDisplayAreaOrganizer.displayIds)
+ .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
+ // Create a mock for the target display area: default display
+ val defaultDisplayArea = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0)
+ whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
+ .thenReturn(defaultDisplayArea)
+
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
+ val task = setUpFreeformTask(displayId = SECOND_DISPLAY)
+ controller.moveToNextDisplay(task.taskId)
+
+ verify(desksOrganizer).moveTaskToDesk(any(), eq(targetDeskId), eq(task))
+ }
+
+ @Test
@EnableFlags(
FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY,
Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER,
@@ -2643,6 +2874,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@EnableFlags(FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT)
fun moveToNextDisplay_sizeInDpPreserved() {
// Set up two display ids
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = 2)
whenever(rootTaskDisplayAreaOrganizer.displayIds)
.thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
// Create a mock for the target display area: second display
@@ -2684,6 +2916,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@EnableFlags(FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT)
fun moveToNextDisplay_shiftWithinDestinationDisplayBounds() {
// Set up two display ids
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = 2)
whenever(rootTaskDisplayAreaOrganizer.displayIds)
.thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
// Create a mock for the target display area: second display
@@ -2725,6 +2958,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
// Set up two display ids
whenever(rootTaskDisplayAreaOrganizer.displayIds)
.thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = 2)
// Create a mock for the target display area: second display
val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0)
whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY))
@@ -2833,7 +3067,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- fun moveToNextDisplay_toDesktopInOtherDisplay_bringsExistingTasksToFront() {
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveToNextDisplay_toDesktopInOtherDisplay_multiDesksDisabled_bringsExistingTasksToFront() {
val transition = Binder()
val sourceDeskId = 0
val targetDeskId = 2
@@ -2859,6 +3094,34 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveToNextDisplay_toDesktopInOtherDisplay_multiDesksEnabled_bringsExistingTasksToFront() {
+ val transition = Binder()
+ val sourceDeskId = 0
+ val targetDeskId = 2
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = targetDeskId)
+ taskRepository.setDeskInactive(deskId = targetDeskId)
+ // Set up two display ids
+ whenever(rootTaskDisplayAreaOrganizer.displayIds)
+ .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
+ // Create a mock for the target display area: second display
+ val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0)
+ whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY))
+ .thenReturn(secondDisplayArea)
+ whenever(transitions.startTransition(eq(TRANSIT_CHANGE), any(), anyOrNull()))
+ .thenReturn(transition)
+ val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = sourceDeskId)
+ val task2 = setUpFreeformTask(displayId = SECOND_DISPLAY, deskId = targetDeskId)
+
+ controller.moveToNextDisplay(task1.taskId)
+
+ // Existing desktop task in the target display is moved to front.
+ val wct = getLatestTransition()
+ assertNotNull(wct)
+ verify(desksOrganizer).reorderTaskToFront(wct, targetDeskId, task2)
+ }
+
+ @Test
@EnableFlags(
Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY,
@@ -2972,7 +3235,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
fun moveToNextDisplay_movingToDesktop_sendsTaskbarRoundingUpdate() {
val transition = Binder()
- val sourceDeskId = 1
+ val sourceDeskId = 0
val targetDeskId = 2
taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = targetDeskId)
taskRepository.setDeskInactive(deskId = targetDeskId)
@@ -3489,7 +3752,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- fun handleRequest_fullscreenTask_freeformVisible_returnSwitchToFreeformWCT() {
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_fullscreenTask_freeformVisible_multiDesksDisabled_returnSwitchToFreeformWCT() {
val homeTask = setUpHomeTask()
val freeformTask = setUpFreeformTask()
markTaskVisible(freeformTask)
@@ -3505,7 +3769,22 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- fun handleRequest_fullscreenTaskWithTaskOnHome_freeformVisible_returnSwitchToFreeformWCT() {
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_fullscreenTask_deskActive_multiDesksEnabled_movesToDesk() {
+ val deskId = 0
+ taskRepository.setActiveDesk(DEFAULT_DISPLAY, deskId = deskId)
+ setUpHomeTask()
+ val fullscreenTask = createFullscreenTask()
+
+ val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
+
+ assertNotNull(wct, "should handle request")
+ verify(desksOrganizer).moveTaskToDesk(wct, deskId, fullscreenTask)
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_fullscreenTaskWithTaskOnHome_freeformVisible_multiDesksDisabled_returnSwitchToFreeformWCT() {
val homeTask = setUpHomeTask()
val freeformTask = setUpFreeformTask()
markTaskVisible(freeformTask)
@@ -3530,7 +3809,23 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- fun handleRequest_fullscreenTaskToFreeform_underTaskLimit_dontMinimize() {
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_fullscreenTaskWithTaskOnHome_activeDesk_multiDesksEnabled_movesToDesk() {
+ val deskId = 0
+ setUpHomeTask()
+ setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ val fullscreenTask = createFullscreenTask()
+ fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME)
+
+ val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
+
+ assertNotNull(wct, "should handle request")
+ verify(desksOrganizer).moveTaskToDesk(wct, deskId, fullscreenTask)
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_fullscreenTaskToDesk_underTaskLimit_multiDesksDisabled_dontMinimize() {
val freeformTask = setUpFreeformTask()
markTaskVisible(freeformTask)
val fullscreenTask = createFullscreenTask()
@@ -3543,7 +3838,29 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- fun handleRequest_fullscreenTaskToFreeform_bringsTasksOverLimit_otherTaskIsMinimized() {
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_fullscreenTaskToDesk_underTaskLimit_multiDesksEnabled_dontMinimize() {
+ val deskId = 5
+ taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ val freeformTask =
+ setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId).also {
+ markTaskVisible(it)
+ }
+
+ // Launch a fullscreen task while in the desk.
+ val fullscreenTask = createFullscreenTask()
+ val transition = Binder()
+ val wct = controller.handleRequest(transition, createTransition(fullscreenTask))
+
+ assertNotNull(wct)
+ verify(desksOrganizer, never()).minimizeTask(eq(wct), eq(deskId), any())
+ assertNull(desktopTasksLimiter.getMinimizingTask(transition))
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_fullscreenTaskToDesk_bringsTasksOverLimit_multiDesksDisabled_otherTaskIsMinimized() {
val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
freeformTasks.forEach { markTaskVisible(it) }
val fullscreenTask = createFullscreenTask()
@@ -3557,7 +3874,34 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- fun handleRequest_fullscreenTaskWithTaskOnHome_bringsTasksOverLimit_otherTaskIsMinimized() {
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_fullscreenTaskToDesk_bringsTasksOverLimit_multiDesksEnabled_otherTaskIsMinimized() {
+ val deskId = 5
+ taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ val freeformTasks =
+ (1..MAX_TASK_LIMIT).map { _ ->
+ setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId).also {
+ markTaskVisible(it)
+ }
+ }
+
+ // Launch a fullscreen task while in the desk.
+ setUpHomeTask()
+ val fullscreenTask = createFullscreenTask()
+ val transition = Binder()
+ val wct = controller.handleRequest(transition, createTransition(fullscreenTask))
+
+ assertNotNull(wct)
+ verify(desksOrganizer).minimizeTask(wct, deskId, freeformTasks[0])
+ val minimizingTask =
+ assertNotNull(desktopTasksLimiter.getMinimizingTask(transition)?.taskId)
+ assertThat(minimizingTask).isEqualTo(freeformTasks[0].taskId)
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_fullscreenTaskWithTaskOnHome_bringsTasksOverLimit_multiDesksDisabled_otherTaskIsMinimized() {
val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
freeformTasks.forEach { markTaskVisible(it) }
val fullscreenTask = createFullscreenTask()
@@ -3578,7 +3922,31 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- fun handleRequest_fullscreenTaskWithTaskOnHome_beyondLimit_existingAndNewTasksAreMinimized() {
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_fullscreenTaskWithTaskOnHome_bringsTasksOverLimit_multiDesksEnabled_otherTaskIsMinimized() {
+ val deskId = 5
+ taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ val freeformTasks =
+ (1..MAX_TASK_LIMIT).map { _ ->
+ setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ }
+ freeformTasks.forEach { markTaskVisible(it) }
+ val fullscreenTask = createFullscreenTask()
+ fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME)
+
+ val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
+
+ assertNotNull(wct)
+ // The launching task is moved to the desk.
+ verify(desksOrganizer).moveTaskToDesk(wct, deskId, fullscreenTask)
+ // The bottom-most task is minimized.
+ verify(desksOrganizer).minimizeTask(wct, deskId, freeformTasks[0])
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_fullscreenTaskWithTaskOnHome_beyondLimit_multiDesksDisabled_existingAndNewTasksAreMinimized() {
val minimizedTask = setUpFreeformTask()
taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = minimizedTask.taskId)
val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
@@ -3604,7 +3972,90 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_fullscreenTaskWithTaskOnHome_beyondLimit_multiDesksEnabled_existingAndNewTasksAreMinimized() {
+ // A desk with a minimized tasks, and non-minimized tasks already at the task limit.
+ val deskId = 5
+ taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ val minimizedTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ taskRepository.minimizeTaskInDesk(
+ displayId = DEFAULT_DISPLAY,
+ deskId = deskId,
+ taskId = minimizedTask.taskId,
+ )
+ val freeformTasks =
+ (1..MAX_TASK_LIMIT).map { _ ->
+ setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId).also {
+ markTaskVisible(it)
+ }
+ }
+
+ // Launch a fullscreen task that brings Home to front with it.
+ setUpHomeTask()
+ val fullscreenTask = createFullscreenTask()
+ fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME)
+ val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
+
+ assertNotNull(wct)
+ verify(desksOrganizer).minimizeTask(wct, deskId, freeformTasks[0])
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_fullscreenTaskWithTaskOnHome_taskAddedToDesk() {
+ // A desk with a minimized tasks, and non-minimized tasks already at the task limit.
+ val deskId = 5
+ taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = deskId)
+
+ // Launch a fullscreen task that brings Home to front with it.
+ setUpHomeTask()
+ val fullscreenTask = createFullscreenTask()
+ fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME)
+ val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
+
+ assertNotNull(wct)
+ verify(desksOrganizer).moveTaskToDesk(wct, deskId, fullscreenTask)
+ }
+
+ @Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+ Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY,
+ Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER,
+ )
+ fun handleRequest_fullscreenTaskWithTaskOnHome_activatesDesk() {
+ val deskId = 5
+ taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = deskId)
+
+ // Launch a fullscreen task that brings Home to front with it.
+ val homeTask = setUpHomeTask()
+ val fullscreenTask = createFullscreenTask()
+ fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME)
+ val transition = Binder()
+ val wct = controller.handleRequest(transition, createTransition(fullscreenTask))
+
+ assertNotNull(wct)
+ wct.assertReorder(homeTask, toTop = true)
+ wct.assertReorder(wallpaperToken, toTop = true)
+ verify(desksOrganizer).activateDesk(wct, deskId)
+ verify(desksTransitionsObserver)
+ .addPendingTransition(
+ DeskTransition.ActiveDeskWithTask(
+ token = transition,
+ displayId = DEFAULT_DISPLAY,
+ deskId = deskId,
+ enterTaskId = fullscreenTask.taskId,
+ )
+ )
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun handleRequest_fullscreenTask_noTasks_enforceDesktop_freeformDisplay_returnFreeformWCT() {
whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
@@ -3627,7 +4078,28 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- fun handleRequest_fullscreenTask_noTasks_enforceDesktop_fullscreenDisplay_returnNull() {
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun handleRequest_fullscreenTask_noInDesk_enforceDesktop_freeformDisplay_movesToDesk() {
+ val deskId = 0
+ taskRepository.setDeskInactive(deskId)
+ whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
+ whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
+ val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+ tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+
+ val fullscreenTask = createFullscreenTask()
+ val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
+
+ assertNotNull(wct, "should handle request")
+ verify(desksOrganizer).moveTaskToDesk(wct, deskId, fullscreenTask)
+ }
+
+ @Test
+ fun handleRequest_fullscreenTask_notInDesk_enforceDesktop_fullscreenDisplay_returnNull() {
+ taskRepository.setDeskInactive(deskId = 0)
whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
@@ -3639,6 +4111,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun handleRequest_fullscreenTask_freeformNotVisible_returnNull() {
val freeformTask = setUpFreeformTask()
markTaskHidden(freeformTask)
@@ -3647,12 +4120,23 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun handleRequest_fullscreenTask_noOtherTasks_returnNull() {
val fullscreenTask = createFullscreenTask()
assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull()
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_fullscreenTask_notInDesk_returnNull() {
+ taskRepository.setDeskInactive(deskId = 0)
+ val fullscreenTask = createFullscreenTask()
+
+ assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull()
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun handleRequest_fullscreenTask_freeformTaskOnOtherDisplay_returnNull() {
val fullscreenTaskDefaultDisplay = createFullscreenTask(displayId = DEFAULT_DISPLAY)
createFreeformTask(displayId = SECOND_DISPLAY)
@@ -3663,8 +4147,27 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- fun handleRequest_freeformTask_freeformVisible_aboveTaskLimit_minimize() {
- val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_fullscreenTask_deskInOtherDisplayActive_returnNull() {
+ taskRepository.setDeskInactive(deskId = 0)
+ val fullscreenTaskDefaultDisplay = createFullscreenTask(displayId = DEFAULT_DISPLAY)
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = 2)
+ taskRepository.setActiveDesk(displayId = SECOND_DISPLAY, deskId = 2)
+
+ val result =
+ controller.handleRequest(Binder(), createTransition(fullscreenTaskDefaultDisplay))
+
+ assertThat(result).isNull()
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_freeformTask_freeformVisible_aboveTaskLimit_multiDesksDisabled_minimize() {
+ val deskId = 0
+ val freeformTasks =
+ (1..MAX_TASK_LIMIT).map { _ ->
+ setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ }
freeformTasks.forEach { markTaskVisible(it) }
val newFreeformTask = createFreeformTask()
@@ -3677,6 +4180,24 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_freeformTask_freeformVisible_aboveTaskLimit_multiDesksEnabled_minimize() {
+ val deskId = 0
+ val freeformTasks =
+ (1..MAX_TASK_LIMIT).map { _ ->
+ setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ }
+ freeformTasks.forEach { markTaskVisible(it) }
+ val newFreeformTask = createFreeformTask()
+
+ val wct =
+ controller.handleRequest(Binder(), createTransition(newFreeformTask, TRANSIT_OPEN))
+
+ assertNotNull(wct)
+ verify(desksOrganizer).minimizeTask(wct, deskId, freeformTasks[0])
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun handleRequest_freeformTaskFromInactiveDesk_tracksDeskDeactivation() {
val deskId = 0
val freeformTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId)
@@ -3691,7 +4212,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
fun handleRequest_freeformTask_relaunchActiveTask_taskBecomesUndefined() {
- val freeformTask = setUpFreeformTask()
+ taskRepository.setDeskInactive(deskId = 0)
+ val freeformTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = 0)
markTaskHidden(freeformTask)
val wct = controller.handleRequest(Binder(), createTransition(freeformTask))
@@ -3721,9 +4243,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
-
- val freeformTask = setUpFreeformTask()
+ taskRepository.setDeskInactive(deskId = 0)
+ val freeformTask = setUpFreeformTask(DEFAULT_DISPLAY, deskId = 0)
markTaskHidden(freeformTask)
+
val wct = controller.handleRequest(Binder(), createTransition(freeformTask))
assertNotNull(wct, "should handle request")
@@ -3732,7 +4255,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ @DisableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
fun handleRequest_freeformTask_desktopWallpaperDisabled_freeformNotVisible_reorderedToTop() {
val freeformTask1 = setUpFreeformTask()
val freeformTask2 = createFreeformTask()
@@ -3751,7 +4277,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun handleRequest_freeformTask_desktopWallpaperEnabled_freeformNotVisible_reorderedToTop() {
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_freeformTask_desktopWallpaperEnabled_freeformNotVisible_multiDesksDisabled_reorderedToTop() {
whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
val freeformTask1 = setUpFreeformTask()
val freeformTask2 = createFreeformTask()
@@ -3774,34 +4301,47 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun handleRequest_freeformTask_desktopWallpaperDisabled_noOtherTasks_reorderedToTop() {
- val task = createFreeformTask()
- val result = controller.handleRequest(Binder(), createTransition(task))
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun handleRequest_freeformTask_desktopWallpaperEnabled_notInDesk_reorderedToTop() {
+ whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
+ val deskId = 0
+ taskRepository.setDeskInactive(deskId)
+ val freeformTask1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ val freeformTask2 = createFreeformTask()
- assertNotNull(result, "Should handle request")
- assertThat(result.hierarchyOps?.size).isEqualTo(1)
- result.assertReorderAt(0, task, toTop = true)
+ val wct =
+ controller.handleRequest(
+ Binder(),
+ createTransition(freeformTask2, type = TRANSIT_TO_FRONT),
+ )
+
+ assertNotNull(wct, "Should handle request")
+ verify(desksOrganizer).reorderTaskToFront(wct, deskId, freeformTask1)
+ wct.assertReorder(freeformTask2, toTop = true)
}
@Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun handleRequest_freeformTask_desktopWallpaperEnabled_noOtherTasks_reorderedToTop() {
- whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
+ @DisableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun handleRequest_freeformTask_desktopWallpaperDisabled_noOtherTasks_reorderedToTop() {
val task = createFreeformTask()
-
val result = controller.handleRequest(Binder(), createTransition(task))
assertNotNull(result, "Should handle request")
- assertThat(result.hierarchyOps?.size).isEqualTo(2)
- // Add desktop wallpaper activity
- result.assertPendingIntentAt(0, desktopWallpaperIntent)
- // Bring new task to front
- result.assertReorderAt(1, task, toTop = true)
+ assertThat(result.hierarchyOps?.size).isEqualTo(1)
+ result.assertReorderAt(0, task, toTop = true)
}
@Test
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ @DisableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
fun handleRequest_freeformTask_dskWallpaperDisabled_freeformOnOtherDisplayOnly_reorderedToTop() {
val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY)
// Second display task
@@ -3817,10 +4357,13 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
fun handleRequest_freeformTask_dskWallpaperEnabled_freeformOnOtherDisplayOnly_reorderedToTop() {
+ taskRepository.setDeskInactive(deskId = 0)
whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY)
// Second display task
- createFreeformTask(displayId = SECOND_DISPLAY)
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = 2)
+ taskRepository.setActiveDesk(displayId = SECOND_DISPLAY, deskId = 2)
+ setUpFreeformTask(displayId = SECOND_DISPLAY, deskId = 2)
val result = controller.handleRequest(Binder(), createTransition(taskDefaultDisplay))
@@ -3956,7 +4499,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
- fun handleRequest_topActivityTransparentWithoutDisplay_returnSwitchToFreeformWCT() {
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_topActivityTransparentWithoutDisplay_multiDesksDisabled_returnSwitchToFreeformWCT() {
val freeformTask = setUpFreeformTask()
markTaskVisible(freeformTask)
@@ -3973,6 +4517,29 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun handleRequest_topActivityTransparentWithoutDisplay_multiDesksEnabled_returnSwitchToFreeformWCT() {
+ val deskId = 0
+ val freeformTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ markTaskVisible(freeformTask)
+
+ val task =
+ setUpFullscreenTask().apply {
+ isActivityStackTransparent = true
+ isTopActivityNoDisplay = true
+ numActivities = 1
+ }
+
+ val wct = controller.handleRequest(Binder(), createTransition(task))
+
+ assertNotNull(wct)
+ verify(desksOrganizer).moveTaskToDesk(wct, deskId, task)
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
@DisableFlags(Flags.FLAG_ENABLE_MODALS_FULLSCREEN_WITH_PERMISSION)
fun handleRequest_topActivityTransparentWithDisplay_returnSwitchToFullscreenWCT() {
@@ -4031,7 +4598,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY,
Flags.FLAG_INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC,
)
- fun handleRequest_onlyTopTransparentFullscreenTask_returnSwitchToFreeformWCT() {
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_onlyTopTransparentFullscreenTask_multiDesksDisabled_returnSwitchToFreeformWCT() {
val topTransparentTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
taskRepository.setTopTransparentFullscreenTaskId(DEFAULT_DISPLAY, topTransparentTask.taskId)
@@ -4043,8 +4611,28 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY,
+ Flags.FLAG_INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun handleRequest_onlyTopTransparentFullscreenTask_multiDesksEnabled_movesToDesktop() {
+ val deskId = 0
+ val topTransparentTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
+ taskRepository.setTopTransparentFullscreenTaskId(DEFAULT_DISPLAY, topTransparentTask.taskId)
+ taskRepository.setDeskInactive(deskId = deskId)
+
+ val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
+
+ val wct = controller.handleRequest(Binder(), createTransition(task))
+ assertNotNull(wct)
+ verify(desksOrganizer).moveTaskToDesk(wct, deskId, task)
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
fun handleRequest_desktopNotShowing_topTransparentFullscreenTask_returnNull() {
+ taskRepository.setDeskInactive(deskId = 0)
val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull()
@@ -4098,7 +4686,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
- fun handleRequest_systemUIActivityWithoutDisplay_returnSwitchToFreeformWCT() {
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_systemUIActivityWithoutDisplay_multiDesksDisabled_returnSwitchToFreeformWCT() {
val freeformTask = setUpFreeformTask()
markTaskVisible(freeformTask)
@@ -4117,6 +4706,32 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
.isEqualTo(WINDOWING_MODE_FREEFORM)
}
+ @Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun handleRequest_systemUIActivityWithoutDisplay_multiDesksEnabled_movesTaskToDesk() {
+ val deskId = 0
+ val freeformTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ markTaskVisible(freeformTask)
+
+ // Set task as systemUI package
+ val systemUIPackageName =
+ context.resources.getString(com.android.internal.R.string.config_systemUi)
+ val baseComponent = ComponentName(systemUIPackageName, /* cls= */ "")
+ val task =
+ setUpFullscreenTask(displayId = DEFAULT_DISPLAY).apply {
+ baseActivity = baseComponent
+ isTopActivityNoDisplay = true
+ }
+
+ val wct = controller.handleRequest(Binder(), createTransition(task))
+
+ assertNotNull(wct)
+ verify(desksOrganizer).moveTaskToDesk(wct, deskId, task)
+ }
+
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
fun handleRequest_defaultHomePackageWithDisplay_returnSwitchToFullscreenWCT() {
val freeformTask = setUpFreeformTask()
@@ -4159,6 +4774,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
fun handleRequest_systemUIActivityWithDisplay_returnSwitchToFullscreenWCT_enforcedDesktop() {
+ taskRepository.setDeskInactive(deskId = 0)
whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
@@ -4792,6 +5408,55 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION,
Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
)
+ fun activateDesk_hasNonRunningTask_startsTask() {
+ val deskId = 0
+ val nonRunningTask =
+ setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = 0, background = true)
+
+ val transition = Binder()
+ val deskChange = mock(TransitionInfo.Change::class.java)
+ whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull()))
+ .thenReturn(transition)
+ whenever(desksOrganizer.isDeskActiveAtEnd(deskChange, deskId)).thenReturn(true)
+ // Make desk inactive by activating another desk.
+ taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 1)
+ taskRepository.setActiveDesk(DEFAULT_DISPLAY, deskId = 1)
+
+ controller.activateDesk(deskId, RemoteTransition(TestRemoteTransition()))
+
+ val wct = getLatestWct(TRANSIT_TO_FRONT, OneShotRemoteHandler::class.java)
+ assertNotNull(wct)
+ wct.assertLaunchTask(nonRunningTask.taskId, WINDOWING_MODE_FREEFORM)
+ }
+
+ @Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun activateDesk_hasRunningTask_reordersTask() {
+ val deskId = 0
+ val runningTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = 0)
+
+ val transition = Binder()
+ val deskChange = mock(TransitionInfo.Change::class.java)
+ whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull()))
+ .thenReturn(transition)
+ whenever(desksOrganizer.isDeskActiveAtEnd(deskChange, deskId)).thenReturn(true)
+ // Make desk inactive by activating another desk.
+ taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 1)
+ taskRepository.setActiveDesk(DEFAULT_DISPLAY, deskId = 1)
+
+ controller.activateDesk(deskId, RemoteTransition(TestRemoteTransition()))
+
+ verify(desksOrganizer).reorderTaskToFront(any(), eq(deskId), eq(runningTask))
+ }
+
+ @Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
fun moveTaskToDesk_multipleDesks_addsPendingTransition() {
val transition = Binder()
whenever(enterDesktopTransitionHandler.moveToDesktop(any(), any())).thenReturn(transition)
@@ -5578,7 +6243,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
- fun openInstance_fromFreeformAddsNewWindow() {
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun openInstance_fromFreeformAddsNewWindow_multiDesksDisabled() {
setUpLandscapeDisplay()
val task = setUpFreeformTask()
val taskToRequest = setUpFreeformTask()
@@ -5592,7 +6258,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
)
)
.thenReturn(Binder())
+
runOpenInstance(task, taskToRequest.taskId)
+
verify(desktopMixedTransitionHandler)
.startLaunchTransition(anyInt(), any(), anyInt(), anyOrNull(), anyOrNull())
val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_TO_FRONT)
@@ -5601,10 +6269,42 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun openInstance_fromFreeformAddsNewWindow_multiDesksEnabled() {
+ setUpLandscapeDisplay()
+ val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = 0)
+ val taskToRequest = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = 0)
+ whenever(
+ desktopMixedTransitionHandler.startLaunchTransition(
+ eq(TRANSIT_TO_FRONT),
+ any(),
+ eq(taskToRequest.taskId),
+ anyOrNull(),
+ anyOrNull(),
+ )
+ )
+ .thenReturn(Binder())
+
+ runOpenInstance(task, taskToRequest.taskId)
+
+ verify(desktopMixedTransitionHandler)
+ .startLaunchTransition(anyInt(), any(), anyInt(), anyOrNull(), anyOrNull())
+ verify(desksOrganizer).reorderTaskToFront(any(), eq(0), eq(taskToRequest))
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
- fun openInstance_fromFreeform_minimizesIfNeeded() {
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun openInstance_fromFreeform_multiDesksDisabled_minimizesIfNeeded() {
setUpLandscapeDisplay()
- val freeformTasks = (1..MAX_TASK_LIMIT + 1).map { _ -> setUpFreeformTask() }
+ val deskId = 0
+ val freeformTasks =
+ (1..MAX_TASK_LIMIT + 1).map { _ ->
+ setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ }
val oldestTask = freeformTasks.first()
val newestTask = freeformTasks.last()
@@ -5630,6 +6330,40 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun openInstance_fromFreeform_multiDesksEnabled_minimizesIfNeeded() {
+ setUpLandscapeDisplay()
+ val deskId = 0
+ val freeformTasks =
+ (1..MAX_TASK_LIMIT + 1).map { _ ->
+ setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ }
+ val oldestTask = freeformTasks.first()
+ val newestTask = freeformTasks.last()
+
+ val transition = Binder()
+ val wctCaptor = argumentCaptor<WindowContainerTransaction>()
+ whenever(
+ desktopMixedTransitionHandler.startLaunchTransition(
+ anyInt(),
+ wctCaptor.capture(),
+ anyInt(),
+ anyOrNull(),
+ anyOrNull(),
+ )
+ )
+ .thenReturn(transition)
+
+ runOpenInstance(newestTask, freeformTasks[1].taskId)
+
+ val wct = wctCaptor.firstValue
+ verify(desksOrganizer).minimizeTask(wct, deskId, oldestTask)
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
fun openInstance_fromFreeform_exitsImmersiveIfNeeded() {
setUpLandscapeDisplay()
@@ -6447,6 +7181,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
fun shouldPlayDesktopAnimation_notShowingDesktop_doesNotPlay() {
+ taskRepository.setDeskInactive(deskId = 0)
val triggerTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
taskRepository.setTaskInFullImmersiveState(
displayId = triggerTask.displayId,
@@ -6523,6 +7258,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
fun shouldPlayDesktopAnimation_fullscreenStaysFullscreen_doesNotPlay() {
val triggerTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
+ taskRepository.setDeskInactive(deskId = 0)
assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isFalse()
assertThat(
@@ -6558,6 +7294,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
fun shouldPlayDesktopAnimation_freeformExitsDesktop_doesNotPlay() {
val triggerTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, active = false)
+ taskRepository.setDeskInactive(deskId = 0)
assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isFalse()
assertThat(
@@ -6587,7 +7324,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER,
)
fun startLaunchTransition_desktopNotShowing_movesWallpaperToFront() {
- val launchingTask = createFreeformTask()
+ taskRepository.setDeskInactive(deskId = 0)
+ val launchingTask = createFreeformTask(displayId = DEFAULT_DISPLAY)
val wct = WindowContainerTransaction()
wct.reorder(launchingTask.token, /* onTop= */ true)
whenever(
@@ -6601,7 +7339,13 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
)
.thenReturn(Binder())
- controller.startLaunchTransition(TRANSIT_OPEN, wct, launchingTaskId = null)
+ controller.startLaunchTransition(
+ transitionType = TRANSIT_OPEN,
+ wct = wct,
+ launchingTaskId = null,
+ deskId = 0,
+ displayId = DEFAULT_DISPLAY,
+ )
val latestWct = getLatestDesktopMixedTaskWct(type = TRANSIT_OPEN)
val launchingTaskReorderIndex = latestWct.indexOfReorder(launchingTask, toTop = true)
@@ -6625,6 +7369,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
transitionType = TRANSIT_OPEN,
wct = WindowContainerTransaction(),
launchingTaskId = null,
+ deskId = 0,
+ displayId = DEFAULT_DISPLAY,
)
verify(desktopModeEnterExitTransitionListener).onEnterDesktopModeTransitionStarted(any())
@@ -6634,6 +7380,51 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@EnableFlags(
Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun startLaunchTransition_launchingTaskFromInactiveDesk_otherDeskActive_activatesDesk() {
+ val activeDeskId = 4
+ taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = activeDeskId)
+ taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = activeDeskId)
+ val inactiveDesk = 5
+ taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = inactiveDesk)
+ val launchingTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = inactiveDesk)
+ val transition = Binder()
+ whenever(
+ desktopMixedTransitionHandler.startLaunchTransition(
+ eq(TRANSIT_OPEN),
+ any(),
+ eq(launchingTask.taskId),
+ anyOrNull(),
+ anyOrNull(),
+ )
+ )
+ .thenReturn(transition)
+
+ val wct = WindowContainerTransaction()
+ controller.startLaunchTransition(
+ transitionType = TRANSIT_OPEN,
+ wct = wct,
+ launchingTaskId = launchingTask.taskId,
+ deskId = inactiveDesk,
+ displayId = DEFAULT_DISPLAY,
+ )
+
+ verify(desksOrganizer).activateDesk(any(), eq(inactiveDesk))
+ verify(desksTransitionsObserver)
+ .addPendingTransition(
+ DeskTransition.ActivateDesk(
+ token = transition,
+ displayId = DEFAULT_DISPLAY,
+ deskId = inactiveDesk,
+ )
+ )
+ }
+
+ @Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+ Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER,
)
fun startLaunchTransition_desktopShowing_doesNotReorderWallpaper() {
val wct = WindowContainerTransaction()
@@ -6648,8 +7439,14 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
)
.thenReturn(Binder())
- setUpFreeformTask()
- controller.startLaunchTransition(TRANSIT_OPEN, wct, launchingTaskId = null)
+ setUpFreeformTask(deskId = 0, displayId = DEFAULT_DISPLAY)
+ controller.startLaunchTransition(
+ transitionType = TRANSIT_OPEN,
+ wct = wct,
+ launchingTaskId = null,
+ deskId = 0,
+ displayId = DEFAULT_DISPLAY,
+ )
val latestWct = getLatestDesktopMixedTaskWct(type = TRANSIT_OPEN)
assertNull(latestWct.hierarchyOps.find { op -> op.container == wallpaperToken.asBinder() })
@@ -6997,7 +7794,15 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
private fun findBoundsChange(wct: WindowContainerTransaction, task: RunningTaskInfo): Rect? =
- wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds
+ wct.changes.entries
+ .find { (token, change) ->
+ token == task.token.asBinder() &&
+ (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0
+ }
+ ?.value
+ ?.configuration
+ ?.windowConfiguration
+ ?.bounds
private fun verifyWCTNotExecuted() {
verify(transitions, never()).startTransition(anyInt(), any(), isNull())
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
index 76103640c029..eeecb00b5b08 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
@@ -39,12 +39,14 @@ import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
import com.android.dx.mockito.inline.extended.StaticMockitoSession
import com.android.internal.jank.InteractionJankMonitor
import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
+import com.android.window.flags.Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.MinimizeReason
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.UnminimizeReason
import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask
+import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer
import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository
import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
@@ -67,10 +69,13 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
-import org.mockito.Mockito.any
import org.mockito.Mockito.mock
import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.never
import org.mockito.quality.Strictness
/**
@@ -84,6 +89,7 @@ import org.mockito.quality.Strictness
class DesktopTasksLimiterTest : ShellTestCase() {
@Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer
+ @Mock lateinit var desksOrganizer: DesksOrganizer
@Mock lateinit var transitions: Transitions
@Mock lateinit var interactionJankMonitor: InteractionJankMonitor
@Mock lateinit var handler: Handler
@@ -128,6 +134,7 @@ class DesktopTasksLimiterTest : ShellTestCase() {
transitions,
userRepositories,
shellTaskOrganizer,
+ desksOrganizer,
MAX_TASK_LIMIT,
interactionJankMonitor,
mContext,
@@ -148,6 +155,7 @@ class DesktopTasksLimiterTest : ShellTestCase() {
transitions,
userRepositories,
shellTaskOrganizer,
+ desksOrganizer,
0,
interactionJankMonitor,
mContext,
@@ -163,6 +171,7 @@ class DesktopTasksLimiterTest : ShellTestCase() {
transitions,
userRepositories,
shellTaskOrganizer,
+ desksOrganizer,
-5,
interactionJankMonitor,
mContext,
@@ -178,6 +187,7 @@ class DesktopTasksLimiterTest : ShellTestCase() {
transitions,
userRepositories,
shellTaskOrganizer,
+ desksOrganizer,
maxTasksLimit = null,
interactionJankMonitor,
mContext,
@@ -394,7 +404,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
}
@Test
- fun addAndGetMinimizeTaskChanges_tasksWithinLimit_noTaskMinimized() {
+ @DisableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun addAndGetMinimizeTaskChanges_tasksWithinLimit_multiDesksDisabled_noTaskMinimized() {
desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
(1..<MAX_TASK_LIMIT).forEach { _ -> setUpFreeformTask() }
@@ -402,7 +413,7 @@ class DesktopTasksLimiterTest : ShellTestCase() {
val wct = WindowContainerTransaction()
val minimizedTaskId =
desktopTasksLimiter.addAndGetMinimizeTaskChanges(
- displayId = DEFAULT_DISPLAY,
+ deskId = 0,
wct = wct,
newFrontTaskId = setUpFreeformTask().taskId,
)
@@ -412,7 +423,27 @@ class DesktopTasksLimiterTest : ShellTestCase() {
}
@Test
- fun addAndGetMinimizeTaskChanges_tasksAboveLimit_backTaskMinimized() {
+ @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun addAndGetMinimizeTaskChanges_tasksWithinLimit_multiDesksEnabled_noTaskMinimized() {
+ desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ (1..<MAX_TASK_LIMIT).forEach { _ -> setUpFreeformTask() }
+
+ val wct = WindowContainerTransaction()
+ val minimizedTaskId =
+ desktopTasksLimiter.addAndGetMinimizeTaskChanges(
+ deskId = 0,
+ wct = wct,
+ newFrontTaskId = setUpFreeformTask().taskId,
+ )
+
+ assertThat(minimizedTaskId).isNull()
+ verify(desksOrganizer, never()).minimizeTask(eq(wct), eq(0), any())
+ }
+
+ @Test
+ @DisableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun addAndGetMinimizeTaskChanges_tasksAboveLimit_multiDesksDisabled_backTaskMinimized() {
desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
// The following list will be ordered bottom -> top, as the last task is moved to top last.
@@ -421,7 +452,7 @@ class DesktopTasksLimiterTest : ShellTestCase() {
val wct = WindowContainerTransaction()
val minimizedTaskId =
desktopTasksLimiter.addAndGetMinimizeTaskChanges(
- displayId = DEFAULT_DISPLAY,
+ deskId = DEFAULT_DISPLAY,
wct = wct,
newFrontTaskId = setUpFreeformTask().taskId,
)
@@ -433,7 +464,28 @@ class DesktopTasksLimiterTest : ShellTestCase() {
}
@Test
- fun addAndGetMinimizeTaskChanges_nonMinimizedTasksWithinLimit_noTaskMinimized() {
+ @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun addAndGetMinimizeTaskChanges_tasksAboveLimit_multiDesksEnabled_backTaskMinimized() {
+ desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ // The following list will be ordered bottom -> top, as the last task is moved to top last.
+ val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() }
+
+ val wct = WindowContainerTransaction()
+ val minimizedTaskId =
+ desktopTasksLimiter.addAndGetMinimizeTaskChanges(
+ deskId = DEFAULT_DISPLAY,
+ wct = wct,
+ newFrontTaskId = setUpFreeformTask().taskId,
+ )
+
+ assertThat(minimizedTaskId).isEqualTo(tasks.first().taskId)
+ verify(desksOrganizer).minimizeTask(wct, deskId = 0, tasks.first())
+ }
+
+ @Test
+ @DisableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun addAndGetMinimizeTaskChanges_nonMinimizedTasksWithinLimit_multiDesksDisabled_noTaskMinimized() {
desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() }
@@ -442,7 +494,7 @@ class DesktopTasksLimiterTest : ShellTestCase() {
val wct = WindowContainerTransaction()
val minimizedTaskId =
desktopTasksLimiter.addAndGetMinimizeTaskChanges(
- displayId = 0,
+ deskId = 0,
wct = wct,
newFrontTaskId = setUpFreeformTask().taskId,
)
@@ -452,6 +504,26 @@ class DesktopTasksLimiterTest : ShellTestCase() {
}
@Test
+ @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun addAndGetMinimizeTaskChanges_nonMinimizedTasksWithinLimit_multiDesksEnabled_noTaskMinimized() {
+ desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() }
+ desktopTaskRepo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = tasks[0].taskId)
+
+ val wct = WindowContainerTransaction()
+ val minimizedTaskId =
+ desktopTasksLimiter.addAndGetMinimizeTaskChanges(
+ deskId = 0,
+ wct = wct,
+ newFrontTaskId = setUpFreeformTask().taskId,
+ )
+
+ assertThat(minimizedTaskId).isNull()
+ verify(desksOrganizer, never()).minimizeTask(eq(wct), eq(0), any())
+ }
+
+ @Test
fun getTaskToMinimize_tasksWithinLimit_returnsNull() {
desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
@@ -485,6 +557,7 @@ class DesktopTasksLimiterTest : ShellTestCase() {
transitions,
userRepositories,
shellTaskOrganizer,
+ desksOrganizer,
MAX_TASK_LIMIT2,
interactionJankMonitor,
mContext,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
index fd8842b6d99b..a7dc706eb6c9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
@@ -48,13 +48,11 @@ import com.android.wm.shell.back.BackAnimationController
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG
import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider
-import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP
import com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP
-import com.android.wm.shell.util.StubTransaction
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import org.junit.Before
@@ -95,7 +93,6 @@ class DesktopTasksTransitionObserverTest {
private val backAnimationController = mock<BackAnimationController>()
private val desktopWallpaperActivityTokenProvider =
mock<DesktopWallpaperActivityTokenProvider>()
- private val desksTransitionObserver = mock<DesksTransitionObserver>()
private val wallpaperToken = MockToken().token()
private lateinit var transitionObserver: DesktopTasksTransitionObserver
@@ -119,7 +116,6 @@ class DesktopTasksTransitionObserverTest {
mixedHandler,
backAnimationController,
desktopWallpaperActivityTokenProvider,
- desksTransitionObserver,
shellInit,
)
}
@@ -403,21 +399,6 @@ class DesktopTasksTransitionObserverTest {
verify(taskRepository).setTaskInPip(task.displayId, task.taskId, enterPip = false)
}
- @Test
- fun onTransitionReady_forwardsToDesksTransitionObserver() {
- val transition = Binder()
- val info = TransitionInfo(TRANSIT_CLOSE, /* flags= */ 0)
-
- transitionObserver.onTransitionReady(
- transition = transition,
- info = info,
- StubTransaction(),
- StubTransaction(),
- )
-
- verify(desksTransitionObserver).onTransitionReady(transition, info)
- }
-
private fun createBackNavigationTransition(
task: RunningTaskInfo?,
type: Int = TRANSIT_TO_BACK,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt
index c00083b0607f..c40a04c47b9b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt
@@ -41,6 +41,7 @@ object DesktopTestHelpers {
.setActivityType(ACTIVITY_TYPE_STANDARD)
.setWindowingMode(WINDOWING_MODE_FREEFORM)
.setLastActiveTime(100)
+ .setUserId(DEFAULT_USER_ID)
.apply { bounds?.let { setBounds(it) } }
.build()
@@ -50,6 +51,7 @@ object DesktopTestHelpers {
.setToken(MockToken().token())
.setActivityType(ACTIVITY_TYPE_STANDARD)
.setWindowingMode(WINDOWING_MODE_FULLSCREEN)
+ .setUserId(DEFAULT_USER_ID)
.setLastActiveTime(100)
/** Create a task that has windowing mode set to [WINDOWING_MODE_FULLSCREEN] */
@@ -63,6 +65,7 @@ object DesktopTestHelpers {
.setToken(MockToken().token())
.setActivityType(ACTIVITY_TYPE_STANDARD)
.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW)
+ .setUserId(DEFAULT_USER_ID)
.setLastActiveTime(100)
.build()
@@ -72,6 +75,7 @@ object DesktopTestHelpers {
.setToken(MockToken().token())
.setActivityType(ACTIVITY_TYPE_HOME)
.setWindowingMode(WINDOWING_MODE_FULLSCREEN)
+ .setUserId(DEFAULT_USER_ID)
.setLastActiveTime(100)
.build()
@@ -91,4 +95,6 @@ object DesktopTestHelpers {
createSystemModalTask().apply {
baseActivity = ComponentName("com.test.dummypackage", "TestClass")
}
+
+ const val DEFAULT_USER_ID = 10
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
index 0871d38ceb46..6e7adf368155 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
@@ -26,6 +26,7 @@ import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD
import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE
import com.android.internal.jank.InteractionJankMonitor
import com.android.window.flags.Flags
+import com.android.window.flags.Flags.FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.TestRunningTaskInfoBuilder
@@ -34,6 +35,7 @@ import com.android.wm.shell.bubbles.BubbleTransitions
import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP
import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP
import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP
+import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.CancelState
import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.Companion.DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS
import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT
import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT
@@ -56,6 +58,7 @@ import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.MockitoSession
+import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.argThat
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
@@ -118,6 +121,14 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
.strictness(Strictness.LENIENT)
.mockStatic(SystemProperties::class.java)
.startMocking()
+ whenever(
+ transitions.startTransition(
+ eq(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP),
+ /* wct= */ any(),
+ eq(defaultHandler),
+ )
+ )
+ .thenReturn(mock<IBinder>())
}
@After
@@ -679,17 +690,11 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
val startTransition = startDrag(defaultHandler, task)
val endTransition = mock<IBinder>()
defaultHandler.onTaskResizeAnimationListener = mock()
- defaultHandler.mergeAnimation(
+ mergeAnimation(
transition = endTransition,
- info =
- createTransitionInfo(
- type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP,
- draggedTask = task,
- ),
- startT = mock<SurfaceControl.Transaction>(),
- finishT = mock<SurfaceControl.Transaction>(),
+ type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP,
+ task = task,
mergeTarget = startTransition,
- finishCallback = mock<Transitions.TransitionFinishCallback>(),
)
defaultHandler.onTransitionConsumed(endTransition, aborted = true, mock())
@@ -701,6 +706,123 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
}
@Test
+ @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX)
+ fun mergeOtherTransition_cancelAndEndNotYetRequested_doesntInterruptsStartDrag() {
+ val finishCallback = mock<Transitions.TransitionFinishCallback>()
+ val task = createTask()
+ defaultHandler.onTaskResizeAnimationListener = mock()
+ val startTransition = startDrag(defaultHandler, task, finishCallback = finishCallback)
+
+ mergeInterruptingTransition(mergeTarget = startTransition)
+
+ verify(finishCallback, never()).onTransitionFinished(anyOrNull())
+ verify(dragAnimator, never()).cancelAnimator()
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX)
+ fun mergeOtherTransition_endDragAlreadyMerged_doesNotInterruptStartDrag() {
+ val startDragFinishCallback = mock<Transitions.TransitionFinishCallback>()
+ val task = createTask()
+ val startTransition =
+ startDrag(defaultHandler, task, finishCallback = startDragFinishCallback)
+ defaultHandler.onTaskResizeAnimationListener = mock()
+ mergeAnimation(
+ type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP,
+ task = task,
+ mergeTarget = startTransition,
+ )
+
+ mergeInterruptingTransition(mergeTarget = startTransition)
+
+ verify(startDragFinishCallback, never()).onTransitionFinished(anyOrNull())
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX)
+ fun startEndAnimation_otherTransitionInterruptedStartAfterEndRequest_finishImmediately() {
+ val task1 = createTask()
+ val startTransition = startDrag(defaultHandler, task1)
+ val endTransition =
+ defaultHandler.finishDragToDesktopTransition(WindowContainerTransaction())
+ val startTransaction = mock<SurfaceControl.Transaction>()
+ val endDragFinishCallback = mock<Transitions.TransitionFinishCallback>()
+ defaultHandler.onTaskResizeAnimationListener = mock()
+ mergeInterruptingTransition(mergeTarget = startTransition)
+
+ val didAnimate =
+ defaultHandler.startAnimation(
+ transition = requireNotNull(endTransition),
+ info =
+ createTransitionInfo(
+ type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP,
+ draggedTask = task1,
+ ),
+ startTransaction = startTransaction,
+ finishTransaction = mock(),
+ finishCallback = endDragFinishCallback,
+ )
+
+ assertThat(didAnimate).isTrue()
+ verify(startTransaction).apply()
+ verify(endDragFinishCallback).onTransitionFinished(anyOrNull())
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX)
+ fun startDrag_otherTransitionInterruptedStartAfterEndRequested_animatesDragWhenReady() {
+ val task1 = createTask()
+ val startTransition = startDrag(defaultHandler, task1)
+ verify(dragAnimator).startAnimation()
+ val endTransition =
+ defaultHandler.finishDragToDesktopTransition(WindowContainerTransaction())
+ defaultHandler.onTaskResizeAnimationListener = mock()
+ mergeInterruptingTransition(mergeTarget = startTransition)
+ defaultHandler.startAnimation(
+ transition = requireNotNull(endTransition),
+ info =
+ createTransitionInfo(
+ type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP,
+ draggedTask = task1,
+ ),
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ finishCallback = mock(),
+ )
+
+ startDrag(defaultHandler, createTask())
+
+ verify(dragAnimator, times(2)).startAnimation()
+ }
+
+ private fun mergeInterruptingTransition(mergeTarget: IBinder) {
+ defaultHandler.mergeAnimation(
+ transition = mock<IBinder>(),
+ info = createTransitionInfo(type = TRANSIT_OPEN, draggedTask = createTask()),
+ startT = mock(),
+ finishT = mock(),
+ mergeTarget = mergeTarget,
+ finishCallback = mock(),
+ )
+ }
+
+ private fun mergeAnimation(
+ transition: IBinder = mock(),
+ type: Int,
+ mergeTarget: IBinder,
+ task: RunningTaskInfo,
+ ) {
+ defaultHandler.mergeAnimation(
+ transition = transition,
+ info = createTransitionInfo(type = type, draggedTask = task),
+ startT = mock(),
+ finishT = mock(),
+ mergeTarget = mergeTarget,
+ finishCallback = mock(),
+ )
+ }
+
+ @Test
fun getAnimationFraction_returnsFraction() {
val fraction =
SpringDragToDesktopTransitionHandler.getAnimationFraction(
@@ -785,6 +907,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
finishTransaction: SurfaceControl.Transaction = mock(),
homeChange: TransitionInfo.Change? = createHomeChange(),
transitionRootLeash: SurfaceControl = mock(),
+ finishCallback: Transitions.TransitionFinishCallback = mock(),
): IBinder {
whenever(dragAnimator.position).thenReturn(PointF())
// Simulate transition is started and is ready to animate.
@@ -800,7 +923,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
),
startTransaction = startTransaction,
finishTransaction = finishTransaction,
- finishCallback = {},
+ finishCallback = finishCallback,
)
return transition
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt
index 96b85ad2729e..9af504797182 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt
@@ -15,20 +15,25 @@
*/
package com.android.wm.shell.desktopmode.multidesks
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
import android.testing.AndroidTestingRunner
import android.view.Display
import android.view.SurfaceControl
import android.view.WindowManager.TRANSIT_TO_FRONT
import android.window.TransitionInfo
+import android.window.WindowContainerToken
import android.window.WindowContainerTransaction
import android.window.WindowContainerTransaction.Change
import android.window.WindowContainerTransaction.HierarchyOp
+import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER
import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT
import androidx.test.filters.SmallTest
import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.ShellTaskOrganizer.TaskListener
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.TestShellExecutor
+import com.android.wm.shell.common.LaunchAdjacentController
import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask
import com.android.wm.shell.desktopmode.multidesks.RootTaskDesksOrganizer.DeskMinimizationRoot
import com.android.wm.shell.desktopmode.multidesks.RootTaskDesksOrganizer.DeskRoot
@@ -36,14 +41,17 @@ import com.android.wm.shell.sysui.ShellCommandHandler
import com.android.wm.shell.sysui.ShellInit
import com.google.common.truth.Truth.assertThat
import kotlin.test.assertNotNull
+import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mockito
import org.mockito.Mockito.verify
import org.mockito.kotlin.argThat
import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
/**
* Tests for [RootTaskDesksOrganizer].
@@ -58,57 +66,32 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
private val testShellInit = ShellInit(testExecutor)
private val mockShellCommandHandler = mock<ShellCommandHandler>()
private val mockShellTaskOrganizer = mock<ShellTaskOrganizer>()
+ private val launchAdjacentController = LaunchAdjacentController(mock())
private lateinit var organizer: RootTaskDesksOrganizer
@Before
fun setUp() {
organizer =
- RootTaskDesksOrganizer(testShellInit, mockShellCommandHandler, mockShellTaskOrganizer)
- }
-
- @Test
- fun testCreateDesk_callsBack() {
- val callback = FakeOnCreateCallback()
- organizer.createDesk(Display.DEFAULT_DISPLAY, callback)
-
- val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
- organizer.onTaskAppeared(freeformRoot, SurfaceControl())
-
- assertThat(callback.created).isTrue()
- assertEquals(freeformRoot.taskId, callback.deskId)
+ RootTaskDesksOrganizer(
+ testShellInit,
+ mockShellCommandHandler,
+ mockShellTaskOrganizer,
+ launchAdjacentController,
+ )
}
- @Test
- fun testCreateDesk_createsMinimizationRoot() {
- val callback = FakeOnCreateCallback()
- organizer.createDesk(Display.DEFAULT_DISPLAY, callback)
- val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
- organizer.onTaskAppeared(freeformRoot, SurfaceControl())
-
- val minimizationRootTask = createFreeformTask().apply { parentTaskId = -1 }
- organizer.onTaskAppeared(minimizationRootTask, SurfaceControl())
-
- val minimizationRoot = organizer.deskMinimizationRootsByDeskId[freeformRoot.taskId]
- assertNotNull(minimizationRoot)
- assertThat(minimizationRoot.deskId).isEqualTo(freeformRoot.taskId)
- assertThat(minimizationRoot.rootId).isEqualTo(minimizationRootTask.taskId)
- }
+ @Test fun testCreateDesk_createsDeskAndMinimizationRoots() = runTest { createDesk() }
@Test
- fun testCreateMinimizationRoot_marksHidden() {
- organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
- val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
- organizer.onTaskAppeared(freeformRoot, SurfaceControl())
-
- val minimizationRootTask = createFreeformTask().apply { parentTaskId = -1 }
- organizer.onTaskAppeared(minimizationRootTask, SurfaceControl())
+ fun testCreateMinimizationRoot_marksHidden() = runTest {
+ val desk = createDesk()
verify(mockShellTaskOrganizer)
.applyTransaction(
argThat { wct ->
wct.changes.any { change ->
- change.key == minimizationRootTask.token.asBinder() &&
+ change.key == desk.minimizationRoot.token.asBinder() &&
(change.value.changeMask and Change.CHANGE_HIDDEN != 0) &&
change.value.hidden
}
@@ -117,7 +100,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testOnTaskAppeared_withoutRequest_throws() {
+ fun testOnTaskAppeared_withoutRequest_throws() = runTest {
val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
assertThrows(Exception::class.java) {
@@ -126,41 +109,25 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testOnTaskAppeared_withRequestOnlyInAnotherDisplay_throws() {
- organizer.createDesk(displayId = 2, FakeOnCreateCallback())
- val freeformRoot = createFreeformTask(Display.DEFAULT_DISPLAY).apply { parentTaskId = -1 }
-
- assertThrows(Exception::class.java) {
- organizer.onTaskAppeared(freeformRoot, SurfaceControl())
- }
- }
-
- @Test
- fun testOnTaskAppeared_duplicateRoot_throws() {
- organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
- val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
- organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+ fun testOnTaskAppeared_duplicateRoot_throws() = runTest {
+ val desk = createDesk()
assertThrows(Exception::class.java) {
- organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+ organizer.onTaskAppeared(desk.deskRoot.taskInfo, SurfaceControl())
}
}
@Test
- fun testOnTaskAppeared_duplicateMinimizedRoot_throws() {
- organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
- val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
- val minimizationRootTask = createFreeformTask().apply { parentTaskId = -1 }
- organizer.onTaskAppeared(freeformRoot, SurfaceControl())
- organizer.onTaskAppeared(minimizationRootTask, SurfaceControl())
+ fun testOnTaskAppeared_duplicateMinimizedRoot_throws() = runTest {
+ val desk = createDesk()
assertThrows(Exception::class.java) {
- organizer.onTaskAppeared(minimizationRootTask, SurfaceControl())
+ organizer.onTaskAppeared(desk.minimizationRoot.taskInfo, SurfaceControl())
}
}
@Test
- fun testOnTaskVanished_removesRoot() {
+ fun testOnTaskVanished_removesRoot() = runTest {
val desk = createDesk()
organizer.onTaskVanished(desk.deskRoot.taskInfo)
@@ -169,7 +136,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testOnTaskVanished_removesMinimizedRoot() {
+ fun testOnTaskVanished_removesMinimizedRoot() = runTest {
val desk = createDesk()
organizer.onTaskVanished(desk.deskRoot.taskInfo)
@@ -179,7 +146,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testDesktopWindowAppearsInDesk() {
+ fun testDesktopWindowAppearsInDesk() = runTest {
val desk = createDesk()
val child = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
@@ -189,7 +156,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testDesktopWindowAppearsInDeskMinimizationRoot() {
+ fun testDesktopWindowAppearsInDeskMinimizationRoot() = runTest {
val desk = createDesk()
val child = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId }
@@ -199,7 +166,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testDesktopWindowMovesToMinimizationRoot() {
+ fun testDesktopWindowMovesToMinimizationRoot() = runTest {
val desk = createDesk()
val child = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
organizer.onTaskAppeared(child, SurfaceControl())
@@ -212,7 +179,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testDesktopWindowDisappearsFromDesk() {
+ fun testDesktopWindowDisappearsFromDesk() = runTest {
val desk = createDesk()
val child = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
@@ -223,7 +190,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testDesktopWindowDisappearsFromDeskMinimizationRoot() {
+ fun testDesktopWindowDisappearsFromDeskMinimizationRoot() = runTest {
val desk = createDesk()
val child = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId }
@@ -234,7 +201,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testRemoveDesk_removesDeskRoot() {
+ fun testRemoveDesk_removesDeskRoot() = runTest {
val desk = createDesk()
val wct = WindowContainerTransaction()
@@ -250,7 +217,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testRemoveDesk_removesMinimizationRoot() {
+ fun testRemoveDesk_removesMinimizationRoot() = runTest {
val desk = createDesk()
val wct = WindowContainerTransaction()
@@ -266,7 +233,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testActivateDesk() {
+ fun testActivateDesk() = runTest {
val desk = createDesk()
val wct = WindowContainerTransaction()
@@ -290,7 +257,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testActivateDesk_didNotExist_throws() {
+ fun testActivateDesk_didNotExist_throws() = runTest {
val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
val wct = WindowContainerTransaction()
@@ -298,7 +265,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testMoveTaskToDesk() {
+ fun testMoveTaskToDesk() = runTest {
val desk = createDesk()
val desktopTask = createFreeformTask().apply { parentTaskId = -1 }
@@ -324,7 +291,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testMoveTaskToDesk_didNotExist_throws() {
+ fun testMoveTaskToDesk_didNotExist_throws() = runTest {
val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
val desktopTask = createFreeformTask().apply { parentTaskId = -1 }
@@ -335,7 +302,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testGetDeskAtEnd() {
+ fun testGetDeskAtEnd() = runTest {
val desk = createDesk()
val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
@@ -348,7 +315,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testGetDeskAtEnd_inMinimizationRoot() {
+ fun testGetDeskAtEnd_inMinimizationRoot() = runTest {
val desk = createDesk()
val task = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId }
@@ -361,27 +328,24 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testIsDeskActiveAtEnd() {
- organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
- val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
- freeformRoot.isVisibleRequested = true
- organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+ fun testIsDeskActiveAtEnd() = runTest {
+ val desk = createDesk()
val isActive =
organizer.isDeskActiveAtEnd(
change =
- TransitionInfo.Change(freeformRoot.token, SurfaceControl()).apply {
- taskInfo = freeformRoot
+ TransitionInfo.Change(desk.deskRoot.token, SurfaceControl()).apply {
+ taskInfo = desk.deskRoot.taskInfo
mode = TRANSIT_TO_FRONT
},
- deskId = freeformRoot.taskId,
+ deskId = desk.deskRoot.deskId,
)
assertThat(isActive).isTrue()
}
@Test
- fun deactivateDesk_clearsLaunchRoot() {
+ fun deactivateDesk_clearsLaunchRoot() = runTest {
val wct = WindowContainerTransaction()
val desk = createDesk()
organizer.activateDesk(wct, desk.deskRoot.deskId)
@@ -400,7 +364,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun isDeskChange_forDeskId() {
+ fun isDeskChange_forDeskId() = runTest {
val desk = createDesk()
assertThat(
@@ -415,7 +379,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun isDeskChange_forDeskId_inMinimizationRoot() {
+ fun isDeskChange_forDeskId_inMinimizationRoot() = runTest {
val desk = createDesk()
assertThat(
@@ -433,7 +397,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun isDeskChange_anyDesk() {
+ fun isDeskChange_anyDesk() = runTest {
val desk = createDesk()
assertThat(
@@ -447,7 +411,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun isDeskChange_anyDesk_inMinimizationRoot() {
+ fun isDeskChange_anyDesk_inMinimizationRoot() = runTest {
val desk = createDesk()
assertThat(
@@ -464,7 +428,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun minimizeTask() {
+ fun minimizeTask() = runTest {
val desk = createDesk()
val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
val wct = WindowContainerTransaction()
@@ -473,18 +437,11 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
organizer.minimizeTask(wct, deskId = desk.deskRoot.deskId, task)
- assertThat(
- wct.hierarchyOps.any { hop ->
- hop.isReparent &&
- hop.container == task.token.asBinder() &&
- hop.newParent == desk.minimizationRoot.token.asBinder()
- }
- )
- .isTrue()
+ assertThat(wct.hasMinimizationHops(desk, task.token)).isTrue()
}
@Test
- fun minimizeTask_alreadyMinimized_noOp() {
+ fun minimizeTask_alreadyMinimized_noOp() = runTest {
val desk = createDesk()
val task = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId }
val wct = WindowContainerTransaction()
@@ -496,7 +453,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun minimizeTask_inDifferentDesk_noOp() {
+ fun minimizeTask_inDifferentDesk_noOp() = runTest {
val desk = createDesk()
val otherDesk = createDesk()
val task = createFreeformTask().apply { parentTaskId = otherDesk.deskRoot.deskId }
@@ -508,30 +465,251 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
assertThat(wct.isEmpty).isTrue()
}
+ @Test
+ fun unminimizeTask() = runTest {
+ val desk = createDesk()
+ val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
+ val wct = WindowContainerTransaction()
+ organizer.moveTaskToDesk(wct, desk.deskRoot.deskId, task)
+ organizer.onTaskAppeared(task, SurfaceControl())
+ organizer.minimizeTask(wct, deskId = desk.deskRoot.deskId, task)
+ task.parentTaskId = desk.minimizationRoot.rootId
+ organizer.onTaskInfoChanged(task)
+
+ wct.clear()
+ organizer.unminimizeTask(wct, deskId = desk.deskRoot.deskId, task)
+
+ assertThat(wct.hasUnminimizationHops(desk, task.token)).isTrue()
+ }
+
+ @Test
+ fun unminimizeTask_alreadyUnminimized_noOp() = runTest {
+ val desk = createDesk()
+ val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
+ val wct = WindowContainerTransaction()
+ organizer.moveTaskToDesk(wct, desk.deskRoot.deskId, task)
+ organizer.onTaskAppeared(task, SurfaceControl())
+
+ wct.clear()
+ organizer.unminimizeTask(wct, deskId = desk.deskRoot.deskId, task)
+
+ assertThat(wct.hasUnminimizationHops(desk, task.token)).isFalse()
+ }
+
+ @Test
+ fun unminimizeTask_notInDesk_noOp() = runTest {
+ val desk = createDesk()
+ val task = createFreeformTask()
+ val wct = WindowContainerTransaction()
+
+ organizer.unminimizeTask(wct, deskId = desk.deskRoot.deskId, task)
+
+ assertThat(wct.hasUnminimizationHops(desk, task.token)).isFalse()
+ }
+
+ @Test
+ fun reorderTaskToFront() = runTest {
+ val desk = createDesk()
+ val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
+ val wct = WindowContainerTransaction()
+ organizer.onTaskAppeared(task, SurfaceControl())
+
+ organizer.reorderTaskToFront(wct, desk.deskRoot.deskId, task)
+
+ assertThat(
+ wct.hierarchyOps.singleOrNull { hop ->
+ hop.container == task.token.asBinder() &&
+ hop.type == HIERARCHY_OP_TYPE_REORDER &&
+ hop.toTop &&
+ hop.includingParents()
+ }
+ )
+ .isNotNull()
+ }
+
+ @Test
+ fun reorderTaskToFront_notInDesk_noOp() = runTest {
+ val desk = createDesk()
+ val task = createFreeformTask()
+ val wct = WindowContainerTransaction()
+
+ organizer.reorderTaskToFront(wct, desk.deskRoot.deskId, task)
+
+ assertThat(
+ wct.hierarchyOps.singleOrNull { hop ->
+ hop.container == task.token.asBinder() &&
+ hop.type == HIERARCHY_OP_TYPE_REORDER &&
+ hop.toTop &&
+ hop.includingParents()
+ }
+ )
+ .isNull()
+ }
+
+ @Test
+ fun reorderTaskToFront_minimized_unminimizesAndReorders() = runTest {
+ val desk = createDesk()
+ val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
+ val wct = WindowContainerTransaction()
+ organizer.onTaskAppeared(task, SurfaceControl())
+ task.parentTaskId = desk.minimizationRoot.rootId
+ organizer.onTaskInfoChanged(task)
+
+ organizer.reorderTaskToFront(wct, desk.deskRoot.deskId, task)
+
+ assertThat(wct.hasUnminimizationHops(desk, task.token)).isTrue()
+ assertThat(
+ wct.hierarchyOps.singleOrNull { hop ->
+ hop.container == task.token.asBinder() &&
+ hop.type == HIERARCHY_OP_TYPE_REORDER &&
+ hop.toTop &&
+ hop.includingParents()
+ }
+ )
+ .isNotNull()
+ }
+
+ @Test
+ fun onTaskAppeared_visibleDesk_onlyDesk_disablesLaunchAdjacent() = runTest {
+ launchAdjacentController.launchAdjacentEnabled = true
+
+ createDesk(visible = true)
+
+ assertThat(launchAdjacentController.launchAdjacentEnabled).isFalse()
+ }
+
+ @Test
+ fun onTaskAppeared_invisibleDesk_onlyDesk_enablesLaunchAdjacent() = runTest {
+ launchAdjacentController.launchAdjacentEnabled = false
+
+ createDesk(visible = false)
+
+ assertThat(launchAdjacentController.launchAdjacentEnabled).isTrue()
+ }
+
+ @Test
+ fun onTaskAppeared_invisibleDesk_otherVisibleDesk_disablesLaunchAdjacent() = runTest {
+ launchAdjacentController.launchAdjacentEnabled = true
+
+ createDesk(visible = true)
+ createDesk(visible = false)
+
+ assertThat(launchAdjacentController.launchAdjacentEnabled).isFalse()
+ }
+
+ @Test
+ fun onTaskInfoChanged_deskBecomesVisible_onlyDesk_disablesLaunchAdjacent() = runTest {
+ launchAdjacentController.launchAdjacentEnabled = true
+
+ val desk = createDesk(visible = false)
+ desk.deskRoot.taskInfo.isVisible = true
+ organizer.onTaskInfoChanged(desk.deskRoot.taskInfo)
+
+ assertThat(launchAdjacentController.launchAdjacentEnabled).isFalse()
+ }
+
+ @Test
+ fun onTaskInfoChanged_deskBecomesInvisible_onlyDesk_enablesLaunchAdjacent() = runTest {
+ launchAdjacentController.launchAdjacentEnabled = false
+
+ val desk = createDesk(visible = true)
+ desk.deskRoot.taskInfo.isVisible = false
+ organizer.onTaskInfoChanged(desk.deskRoot.taskInfo)
+
+ assertThat(launchAdjacentController.launchAdjacentEnabled).isTrue()
+ }
+
+ @Test
+ fun onTaskInfoChanged_deskBecomesInvisible_otherVisibleDesk_disablesLaunchAdjacent() = runTest {
+ launchAdjacentController.launchAdjacentEnabled = true
+
+ createDesk(visible = true)
+ val desk = createDesk(visible = true)
+ desk.deskRoot.taskInfo.isVisible = false
+ organizer.onTaskInfoChanged(desk.deskRoot.taskInfo)
+
+ assertThat(launchAdjacentController.launchAdjacentEnabled).isFalse()
+ }
+
+ @Test
+ fun onTaskVanished_visibleDeskDisappears_onlyDesk_enablesLaunchAdjacent() = runTest {
+ launchAdjacentController.launchAdjacentEnabled = false
+
+ val desk = createDesk(visible = true)
+ organizer.onTaskVanished(desk.deskRoot.taskInfo)
+
+ assertThat(launchAdjacentController.launchAdjacentEnabled).isTrue()
+ }
+
+ @Test
+ fun onTaskVanished_visibleDeskDisappears_otherDeskVisible_disablesLaunchAdjacent() = runTest {
+ launchAdjacentController.launchAdjacentEnabled = true
+
+ createDesk(visible = true)
+ val desk = createDesk(visible = true)
+ organizer.onTaskVanished(desk.deskRoot.taskInfo)
+
+ assertThat(launchAdjacentController.launchAdjacentEnabled).isFalse()
+ }
+
private data class DeskRoots(
val deskRoot: DeskRoot,
val minimizationRoot: DeskMinimizationRoot,
)
- private fun createDesk(): DeskRoots {
- organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
- val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
- organizer.onTaskAppeared(freeformRoot, SurfaceControl())
- val minimizationRoot = createFreeformTask().apply { parentTaskId = -1 }
- organizer.onTaskAppeared(minimizationRoot, SurfaceControl())
- return DeskRoots(
- organizer.deskRootsByDeskId[freeformRoot.taskId],
- checkNotNull(organizer.deskMinimizationRootsByDeskId[freeformRoot.taskId]),
- )
- }
-
- private class FakeOnCreateCallback : DesksOrganizer.OnCreateCallback {
- var deskId: Int? = null
- val created: Boolean
- get() = deskId != null
-
- override fun onCreated(deskId: Int) {
- this.deskId = deskId
- }
+ private suspend fun createDesk(visible: Boolean = true): DeskRoots {
+ val freeformRootTask =
+ createFreeformTask().apply {
+ parentTaskId = -1
+ isVisible = visible
+ isVisibleRequested = visible
+ }
+ val minimizationRootTask = createFreeformTask().apply { parentTaskId = -1 }
+ Mockito.reset(mockShellTaskOrganizer)
+ whenever(
+ mockShellTaskOrganizer.createRootTask(
+ Display.DEFAULT_DISPLAY,
+ WINDOWING_MODE_FREEFORM,
+ organizer,
+ true,
+ )
+ )
+ .thenAnswer { invocation ->
+ val listener = (invocation.arguments[2] as TaskListener)
+ listener.onTaskAppeared(freeformRootTask, SurfaceControl())
+ }
+ .thenAnswer { invocation ->
+ val listener = (invocation.arguments[2] as TaskListener)
+ listener.onTaskAppeared(minimizationRootTask, SurfaceControl())
+ }
+ val deskId = organizer.createDesk(Display.DEFAULT_DISPLAY)
+ assertEquals(freeformRootTask.taskId, deskId)
+ val deskRoot = assertNotNull(organizer.deskRootsByDeskId.get(freeformRootTask.taskId))
+ val minimizationRoot =
+ assertNotNull(organizer.deskMinimizationRootsByDeskId[freeformRootTask.taskId])
+ assertThat(minimizationRoot.deskId).isEqualTo(freeformRootTask.taskId)
+ assertThat(minimizationRoot.rootId).isEqualTo(minimizationRootTask.taskId)
+ return DeskRoots(deskRoot, minimizationRoot)
}
+
+ private fun WindowContainerTransaction.hasMinimizationHops(
+ desk: DeskRoots,
+ task: WindowContainerToken,
+ ): Boolean =
+ hierarchyOps.any { hop ->
+ hop.isReparent &&
+ hop.container == task.asBinder() &&
+ hop.newParent == desk.minimizationRoot.token.asBinder()
+ }
+
+ private fun WindowContainerTransaction.hasUnminimizationHops(
+ desk: DeskRoots,
+ task: WindowContainerToken,
+ ): Boolean =
+ hierarchyOps.any { hop ->
+ hop.isReparent &&
+ hop.container == task.asBinder() &&
+ hop.newParent == desk.deskRoot.token.asBinder() &&
+ hop.toTop
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt
index dd9e6ca0deae..4440d4e801fe 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt
@@ -17,7 +17,6 @@
package com.android.wm.shell.desktopmode.persistence
import android.os.UserManager
-import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.testing.AndroidTestingRunner
import android.view.Display.DEFAULT_DISPLAY
@@ -82,10 +81,27 @@ class DesktopRepositoryInitializerTest : ShellTestCase() {
}
@Test
- @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE, FLAG_ENABLE_DESKTOP_WINDOWING_HSUM)
- /** TODO: b/362720497 - add multi-desk version when implemented. */
- @DisableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
- fun initWithPersistence_multipleUsers_addedCorrectly_multiDesksDisabled() =
+ fun init_updatesFlow() =
+ runTest(StandardTestDispatcher()) {
+ whenever(persistentRepository.getUserDesktopRepositoryMap())
+ .thenReturn(mapOf(USER_ID_1 to desktopRepositoryState1))
+ whenever(persistentRepository.getDesktopRepositoryState(USER_ID_1))
+ .thenReturn(desktopRepositoryState1)
+ whenever(persistentRepository.readDesktop(USER_ID_1, DESKTOP_ID_1)).thenReturn(desktop1)
+ whenever(persistentRepository.readDesktop(USER_ID_1, DESKTOP_ID_2)).thenReturn(desktop2)
+
+ repositoryInitializer.initialize(desktopUserRepositories)
+
+ assertThat(repositoryInitializer.isInitialized.value).isTrue()
+ }
+
+ @Test
+ @EnableFlags(
+ FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE,
+ FLAG_ENABLE_DESKTOP_WINDOWING_HSUM,
+ FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun initWithPersistence_multipleUsers_addedCorrectly() =
runTest(StandardTestDispatcher()) {
whenever(persistentRepository.getUserDesktopRepositoryMap())
.thenReturn(
@@ -104,50 +120,74 @@ class DesktopRepositoryInitializerTest : ShellTestCase() {
repositoryInitializer.initialize(desktopUserRepositories)
- // Desktop Repository currently returns all tasks across desktops for a specific user
- // since the repository currently doesn't handle desktops. This test logic should be
- // updated
- // once the repository handles multiple desktops.
assertThat(
- desktopUserRepositories.getProfile(USER_ID_1).getActiveTasks(DEFAULT_DISPLAY)
+ desktopUserRepositories
+ .getProfile(USER_ID_1)
+ .getActiveTaskIdsInDesk(DESKTOP_ID_1)
)
- .containsExactly(1, 3, 4, 5)
+ .containsExactly(1, 3)
.inOrder()
assertThat(
desktopUserRepositories
.getProfile(USER_ID_1)
- .getExpandedTasksOrdered(DEFAULT_DISPLAY)
+ .getActiveTaskIdsInDesk(DESKTOP_ID_2)
)
- .containsExactly(5, 1)
+ .containsExactly(4, 5)
.inOrder()
assertThat(
- desktopUserRepositories.getProfile(USER_ID_1).getMinimizedTasks(DEFAULT_DISPLAY)
+ desktopUserRepositories
+ .getProfile(USER_ID_2)
+ .getActiveTaskIdsInDesk(DESKTOP_ID_3)
)
- .containsExactly(3, 4)
+ .containsExactly(7, 8)
+ .inOrder()
+ assertThat(
+ desktopUserRepositories
+ .getProfile(USER_ID_1)
+ .getExpandedTasksIdsInDeskOrdered(DESKTOP_ID_1)
+ )
+ .containsExactly(1)
.inOrder()
-
assertThat(
- desktopUserRepositories.getProfile(USER_ID_2).getActiveTasks(DEFAULT_DISPLAY)
+ desktopUserRepositories
+ .getProfile(USER_ID_1)
+ .getExpandedTasksIdsInDeskOrdered(DESKTOP_ID_2)
)
- .containsExactly(7, 8)
+ .containsExactly(5)
.inOrder()
assertThat(
desktopUserRepositories
.getProfile(USER_ID_2)
- .getExpandedTasksOrdered(DEFAULT_DISPLAY)
+ .getExpandedTasksIdsInDeskOrdered(DESKTOP_ID_3)
+ )
+ .containsExactly(7)
+ .inOrder()
+ assertThat(
+ desktopUserRepositories
+ .getProfile(USER_ID_1)
+ .getMinimizedTaskIdsInDesk(DESKTOP_ID_1)
+ )
+ .containsExactly(3)
+ .inOrder()
+ assertThat(
+ desktopUserRepositories
+ .getProfile(USER_ID_1)
+ .getMinimizedTaskIdsInDesk(DESKTOP_ID_2)
)
- .contains(7)
+ .containsExactly(4)
+ .inOrder()
assertThat(
- desktopUserRepositories.getProfile(USER_ID_2).getMinimizedTasks(DEFAULT_DISPLAY)
+ desktopUserRepositories
+ .getProfile(USER_ID_2)
+ .getMinimizedTaskIdsInDesk(DESKTOP_ID_3)
)
.containsExactly(8)
+ .inOrder()
}
@Test
- @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE)
- /** TODO: b/362720497 - add multi-desk version when implemented. */
- @DisableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
- fun initWithPersistence_singleUser_addedCorrectly_multiDesksDisabled() =
+ @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE, FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun initWithPersistence_singleUser_addedCorrectly() =
runTest(StandardTestDispatcher()) {
whenever(persistentRepository.getUserDesktopRepositoryMap())
.thenReturn(mapOf(USER_ID_1 to desktopRepositoryState1))
@@ -161,23 +201,44 @@ class DesktopRepositoryInitializerTest : ShellTestCase() {
assertThat(
desktopUserRepositories
.getProfile(USER_ID_1)
- .getActiveTaskIdsInDesk(deskId = DEFAULT_DISPLAY)
+ .getActiveTaskIdsInDesk(DESKTOP_ID_1)
+ )
+ .containsExactly(1, 3)
+ .inOrder()
+ assertThat(
+ desktopUserRepositories
+ .getProfile(USER_ID_1)
+ .getActiveTaskIdsInDesk(DESKTOP_ID_2)
+ )
+ .containsExactly(4, 5)
+ .inOrder()
+ assertThat(
+ desktopUserRepositories
+ .getProfile(USER_ID_1)
+ .getExpandedTasksIdsInDeskOrdered(DESKTOP_ID_1)
+ )
+ .containsExactly(1)
+ .inOrder()
+ assertThat(
+ desktopUserRepositories
+ .getProfile(USER_ID_1)
+ .getExpandedTasksIdsInDeskOrdered(DESKTOP_ID_2)
)
- .containsExactly(1, 3, 4, 5)
+ .containsExactly(5)
.inOrder()
assertThat(
desktopUserRepositories
.getProfile(USER_ID_1)
- .getExpandedTasksIdsInDeskOrdered(deskId = DEFAULT_DISPLAY)
+ .getMinimizedTaskIdsInDesk(DESKTOP_ID_1)
)
- .containsExactly(5, 1)
+ .containsExactly(3)
.inOrder()
assertThat(
desktopUserRepositories
.getProfile(USER_ID_1)
- .getMinimizedTaskIdsInDesk(deskId = DEFAULT_DISPLAY)
+ .getMinimizedTaskIdsInDesk(DESKTOP_ID_2)
)
- .containsExactly(3, 4)
+ .containsExactly(4)
.inOrder()
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
index 9509aaf20c9b..52c5ad1f8e49 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
@@ -41,6 +41,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.dx.mockito.inline.extended.StaticMockitoSession;
+import com.android.window.flags.Flags;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestRunningTaskInfoBuilder;
@@ -198,6 +199,7 @@ public final class FreeformTaskListenerTests extends ShellTestCase {
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
public void visibilityTaskChanged_visible_setLaunchAdjacentDisabled() {
ActivityManager.RunningTaskInfo task =
new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build();
@@ -209,6 +211,7 @@ public final class FreeformTaskListenerTests extends ShellTestCase {
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
public void visibilityTaskChanged_notVisible_setLaunchAdjacentEnabled() {
ActivityManager.RunningTaskInfo task =
new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
index bc918450a3cf..714e5f486285 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
@@ -44,10 +44,12 @@ import androidx.test.filters.SmallTest;
import com.android.window.flags.Flags;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.desktopmode.DesktopImmersiveController;
+import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.FocusTransitionObserver;
import com.android.wm.shell.transition.TransitionInfoBuilder;
import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.util.StubTransaction;
import com.android.wm.shell.windowdecor.WindowDecorViewModel;
import org.junit.Before;
@@ -68,6 +70,7 @@ public class FreeformTaskTransitionObserverTest extends ShellTestCase {
@Mock private WindowDecorViewModel mWindowDecorViewModel;
@Mock private TaskChangeListener mTaskChangeListener;
@Mock private FocusTransitionObserver mFocusTransitionObserver;
+ @Mock private DesksTransitionObserver mDesksTransitionObserver;
private FreeformTaskTransitionObserver mTransitionObserver;
@@ -88,7 +91,8 @@ public class FreeformTaskTransitionObserverTest extends ShellTestCase {
Optional.of(mDesktopImmersiveController),
mWindowDecorViewModel,
Optional.of(mTaskChangeListener),
- mFocusTransitionObserver);
+ mFocusTransitionObserver,
+ Optional.of(mDesksTransitionObserver));
final ArgumentCaptor<Runnable> initRunnableCaptor = ArgumentCaptor.forClass(Runnable.class);
verify(mShellInit).addInitCallback(initRunnableCaptor.capture(), same(mTransitionObserver));
@@ -357,6 +361,18 @@ public class FreeformTaskTransitionObserverTest extends ShellTestCase {
verify(mDesktopImmersiveController).onTransitionFinished(transition, /* aborted= */ false);
}
+ @Test
+ public void onTransitionReady_forwardsToDesksTransitionObserver() {
+ final IBinder transition = mock(IBinder.class);
+ final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CLOSE, /* flags= */ 0)
+ .build();
+
+ mTransitionObserver.onTransitionReady(transition, info, new StubTransaction(),
+ new StubTransaction());
+
+ verify(mDesksTransitionObserver).onTransitionReady(transition, info);
+ }
+
private static TransitionInfo.Change createChange(int mode, int taskId, int windowingMode) {
final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
taskInfo.taskId = taskId;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java
index 275e4882a79d..42f65dd71f16 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java
@@ -17,6 +17,7 @@
package com.android.wm.shell.pip2.phone;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -86,6 +87,7 @@ public class PipSchedulerTest {
@Mock private SurfaceControl.Transaction mMockTransaction;
@Mock private PipAlphaAnimator mMockAlphaAnimator;
@Mock private SplitScreenController mMockSplitScreenController;
+ @Mock private SurfaceControl mMockLeash;
@Captor private ArgumentCaptor<Runnable> mRunnableArgumentCaptor;
@Captor private ArgumentCaptor<WindowContainerTransaction> mWctArgumentCaptor;
@@ -315,6 +317,30 @@ public class PipSchedulerTest {
verify(mMockAlphaAnimator, never()).start();
}
+ @Test
+ public void onPipTransitionStateChanged_exiting_endAnimation() {
+ mPipScheduler.setOverlayFadeoutAnimator(mMockAlphaAnimator);
+ when(mMockAlphaAnimator.isStarted()).thenReturn(true);
+ mPipScheduler.onPipTransitionStateChanged(PipTransitionState.ENTERED_PIP,
+ PipTransitionState.EXITING_PIP, null);
+
+ verify(mMockAlphaAnimator, times(1)).end();
+ assertNull("mOverlayFadeoutAnimator should be reset to null",
+ mPipScheduler.getOverlayFadeoutAnimator());
+ }
+
+ @Test
+ public void onPipTransitionStateChanged_scheduledBoundsChange_endAnimation() {
+ mPipScheduler.setOverlayFadeoutAnimator(mMockAlphaAnimator);
+ when(mMockAlphaAnimator.isStarted()).thenReturn(true);
+ mPipScheduler.onPipTransitionStateChanged(PipTransitionState.ENTERED_PIP,
+ PipTransitionState.SCHEDULED_BOUNDS_CHANGE, null);
+
+ verify(mMockAlphaAnimator, times(1)).end();
+ assertNull("mOverlayFadeoutAnimator should be reset to null",
+ mPipScheduler.getOverlayFadeoutAnimator());
+ }
+
private void setNullPipTaskToken() {
when(mMockPipTransitionState.getPipTaskToken()).thenReturn(null);
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTaskListenerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTaskListenerTest.java
index 333569a7206e..5029371c3419 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTaskListenerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTaskListenerTest.java
@@ -24,6 +24,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.kotlin.MatchersKt.eq;
import static org.mockito.kotlin.VerificationKt.clearInvocations;
@@ -35,7 +36,9 @@ import android.app.ActivityManager;
import android.app.PendingIntent;
import android.app.PictureInPictureParams;
import android.app.RemoteAction;
+import android.content.ComponentName;
import android.content.Context;
+import android.content.res.Resources;
import android.graphics.Rect;
import android.graphics.drawable.Icon;
import android.os.Bundle;
@@ -48,8 +51,10 @@ import androidx.test.filters.SmallTest;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.pip.PhoneSizeSpecSource;
import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
import com.android.wm.shell.common.pip.PipBoundsState;
+import com.android.wm.shell.common.pip.PipDisplayLayoutState;
import com.android.wm.shell.pip2.animation.PipResizeAnimator;
import org.junit.Before;
@@ -107,6 +112,16 @@ public class PipTaskListenerTest {
}
@Test
+ public void constructor_addOnPipComponentChangedListener() {
+ mPipTaskListener = new PipTaskListener(mMockContext, mMockShellTaskOrganizer,
+ mMockPipTransitionState, mMockPipScheduler, mMockPipBoundsState,
+ mMockPipBoundsAlgorithm, mMockShellExecutor);
+
+ verify(mMockPipBoundsState).addOnPipComponentChangedListener(
+ any(PipBoundsState.OnPipComponentChangedListener.class));
+ }
+
+ @Test
public void setPictureInPictureParams_updatePictureInPictureParams() {
mPipTaskListener = new PipTaskListener(mMockContext, mMockShellTaskOrganizer,
mMockPipTransitionState, mMockPipScheduler, mMockPipBoundsState,
@@ -359,6 +374,26 @@ public class PipTaskListenerTest {
verify(mMockPipResizeAnimator, times(0)).start();
}
+ @Test
+ public void onPipComponentChanged_clearPictureInPictureParams() {
+ when(mMockContext.getResources()).thenReturn(mock(Resources.class));
+ PipBoundsState pipBoundsState = new PipBoundsState(mMockContext,
+ mock(PhoneSizeSpecSource.class), mock(PipDisplayLayoutState.class));
+ pipBoundsState.setLastPipComponentName(new ComponentName("org.test", "test1"));
+
+ mPipTaskListener = new PipTaskListener(mMockContext, mMockShellTaskOrganizer,
+ mMockPipTransitionState, mMockPipScheduler, pipBoundsState,
+ mMockPipBoundsAlgorithm, mMockShellExecutor);
+ Rational aspectRatio = new Rational(4, 3);
+ String action1 = "action1";
+ mPipTaskListener.setPictureInPictureParams(getPictureInPictureParams(
+ aspectRatio, action1));
+
+ pipBoundsState.setLastPipComponentName(new ComponentName("org.test", "test2"));
+
+ assertTrue(mPipTaskListener.getPictureInPictureParams().empty());
+ }
+
private PictureInPictureParams getPictureInPictureParams(Rational aspectRatio,
String... actions) {
final PictureInPictureParams.Builder builder = new PictureInPictureParams.Builder();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt
index 6ecebd76a951..75f6bda4d750 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt
@@ -203,7 +203,7 @@ class GroupedTaskInfoTest : ShellTestCase() {
assertThat(taskInfoFromParcel.taskInfoList).hasSize(3)
// Only compare task ids
val taskIdComparator = Correspondence.transforming<TaskInfo, Int>(
- { it?.taskId }, "has taskId of"
+ { it.taskId }, "has taskId of"
)
assertThat(taskInfoFromParcel.taskInfoList).comparingElementsUsing(taskIdComparator)
.containsExactly(1, 2, 3).inOrder()
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt
index fd22a84dee5d..2b39262d9f00 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt
@@ -53,7 +53,8 @@ class DragZoneFactoryTest {
tabletPortrait.copy(windowBounds = Rect(0, 0, 800, 900), isSmallTablet = true)
private val foldableLandscape =
foldablePortrait.copy(windowBounds = Rect(0, 0, 900, 800), isLandscape = true)
- private val splitScreenModeChecker = SplitScreenModeChecker { SplitScreenMode.NONE }
+ private var splitScreenMode = SplitScreenMode.NONE
+ private val splitScreenModeChecker = SplitScreenModeChecker { splitScreenMode }
private var isDesktopWindowModeSupported = true
private val desktopWindowModeChecker = DesktopWindowModeChecker { isDesktopWindowModeSupported }
@@ -283,7 +284,7 @@ class DragZoneFactoryTest {
}
@Test
- fun dragZonesForBubble_tablet_desktopModeDisabled() {
+ fun dragZonesForBubble_desktopModeDisabled() {
isDesktopWindowModeSupported = false
dragZoneFactory =
DragZoneFactory(
@@ -298,7 +299,7 @@ class DragZoneFactoryTest {
}
@Test
- fun dragZonesForExpandedView_tablet_desktopModeDisabled() {
+ fun dragZonesForExpandedView_desktopModeDisabled() {
isDesktopWindowModeSupported = false
dragZoneFactory =
DragZoneFactory(
@@ -314,6 +315,38 @@ class DragZoneFactoryTest {
assertThat(dragZones.filterIsInstance<DragZone.DesktopWindow>()).isEmpty()
}
+ @Test
+ fun dragZonesForBubble_splitScreenModeUnsupported() {
+ splitScreenMode = SplitScreenMode.UNSUPPORTED
+ dragZoneFactory =
+ DragZoneFactory(
+ context,
+ foldableLandscape,
+ splitScreenModeChecker,
+ desktopWindowModeChecker
+ )
+ val dragZones =
+ dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT))
+ assertThat(dragZones.filterIsInstance<DragZone.Split>()).isEmpty()
+ }
+
+ @Test
+ fun dragZonesForExpandedView_splitScreenModeUnsupported() {
+ splitScreenMode = SplitScreenMode.UNSUPPORTED
+ dragZoneFactory =
+ DragZoneFactory(
+ context,
+ foldableLandscape,
+ splitScreenModeChecker,
+ desktopWindowModeChecker
+ )
+ val dragZones =
+ dragZoneFactory.createSortedDragZones(
+ DraggedObject.ExpandedView(BubbleBarLocation.LEFT)
+ )
+ assertThat(dragZones.filterIsInstance<DragZone.Split>()).isEmpty()
+ }
+
private inline fun <reified T> verifyInstance(): DragZoneVerifier = { dragZone ->
assertThat(dragZone).isInstanceOf(T::class.java)
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt
index 257bbb5603a7..b07b6c1a3a87 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt
@@ -22,6 +22,7 @@ import android.platform.test.annotations.EnableFlags
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.SurfaceControl
+import android.window.TaskSnapshot
import androidx.test.filters.SmallTest
import com.android.window.flags.Flags
import com.android.wm.shell.MockToken
@@ -33,6 +34,7 @@ import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
import com.google.common.truth.Truth.assertThat
import org.junit.After
+import org.junit.Assert.fail
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -84,7 +86,29 @@ class DesktopHeaderManageWindowsMenuTest : ShellTestCase() {
assertThat(menu.menuViewContainer).isInstanceOf(AdditionalSystemViewContainer::class.java)
}
- private fun createMenu(task: RunningTaskInfo) = DesktopHeaderManageWindowsMenu(
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun testShow_nullSnapshotDoesNotCauseNPE() {
+ val task = createFreeformTask()
+ val snapshotList = listOf(Pair(/* index = */ 1, /* snapshot = */ null))
+ // Set as immersive so that menu is created as system view container (simpler of the
+ // options)
+ userRepositories.getProfile(DEFAULT_USER_ID).setTaskInFullImmersiveState(
+ displayId = task.displayId,
+ taskId = task.taskId,
+ immersive = true
+ )
+ try {
+ menu = createMenu(task, snapshotList)
+ } catch (e: NullPointerException) {
+ fail("Null snapshot should not have thrown null pointer exception")
+ }
+ }
+
+ private fun createMenu(
+ task: RunningTaskInfo,
+ snapshotList: List<Pair<Int, TaskSnapshot?>> = emptyList()
+ ) = DesktopHeaderManageWindowsMenu(
callerTaskInfo = task,
x = 0,
y = 0,
@@ -94,7 +118,7 @@ class DesktopHeaderManageWindowsMenuTest : ShellTestCase() {
desktopUserRepositories = userRepositories,
surfaceControlBuilderSupplier = { SurfaceControl.Builder() },
surfaceControlTransactionSupplier = { SurfaceControl.Transaction() },
- snapshotList = emptyList(),
+ snapshotList = snapshotList,
onIconClickListener = {},
onOutsideClickListener = {},
)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index c4f70ac2297f..1371b38a579f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -167,6 +167,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
private static final boolean DEFAULT_IS_STATUSBAR_VISIBLE = true;
private static final boolean DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED = false;
private static final boolean DEFAULT_IS_IN_FULL_IMMERSIVE_MODE = false;
+ private static final boolean DEFAULT_IS_DRAGGING = false;
private static final boolean DEFAULT_HAS_GLOBAL_FOCUS = true;
private static final boolean DEFAULT_SHOULD_IGNORE_CORNER_RADIUS = false;
private static final boolean DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS = false;
@@ -415,6 +416,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
DEFAULT_IS_STATUSBAR_VISIBLE,
DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED,
DEFAULT_IS_IN_FULL_IMMERSIVE_MODE,
+ DEFAULT_IS_DRAGGING,
new InsetsState(),
DEFAULT_HAS_GLOBAL_FOCUS,
mExclusionRegion,
@@ -616,6 +618,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
DEFAULT_IS_STATUSBAR_VISIBLE,
DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED,
DEFAULT_IS_IN_FULL_IMMERSIVE_MODE,
+ DEFAULT_IS_DRAGGING,
new InsetsState(),
DEFAULT_HAS_GLOBAL_FOCUS,
mExclusionRegion,
@@ -650,6 +653,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
DEFAULT_IS_STATUSBAR_VISIBLE,
DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED,
DEFAULT_IS_IN_FULL_IMMERSIVE_MODE,
+ DEFAULT_IS_DRAGGING,
new InsetsState(),
DEFAULT_HAS_GLOBAL_FOCUS,
mExclusionRegion,
@@ -728,6 +732,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
DEFAULT_IS_STATUSBAR_VISIBLE,
DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED,
/* inFullImmersiveMode */ true,
+ DEFAULT_IS_DRAGGING,
insetsState,
DEFAULT_HAS_GLOBAL_FOCUS,
mExclusionRegion,
@@ -755,6 +760,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
DEFAULT_IS_STATUSBAR_VISIBLE,
DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED,
/* inFullImmersiveMode */ true,
+ DEFAULT_IS_DRAGGING,
new InsetsState(),
DEFAULT_HAS_GLOBAL_FOCUS,
mExclusionRegion,
@@ -781,6 +787,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* isStatusBarVisible */ false,
DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED,
DEFAULT_IS_IN_FULL_IMMERSIVE_MODE,
+ DEFAULT_IS_DRAGGING,
new InsetsState(),
DEFAULT_HAS_GLOBAL_FOCUS,
mExclusionRegion,
@@ -807,6 +814,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
DEFAULT_IS_IN_FULL_IMMERSIVE_MODE,
+ DEFAULT_IS_DRAGGING,
new InsetsState(),
DEFAULT_HAS_GLOBAL_FOCUS,
mExclusionRegion,
@@ -832,6 +840,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* isStatusBarVisible */ false,
DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED,
DEFAULT_IS_IN_FULL_IMMERSIVE_MODE,
+ DEFAULT_IS_DRAGGING,
new InsetsState(),
DEFAULT_HAS_GLOBAL_FOCUS,
mExclusionRegion,
@@ -857,6 +866,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
DEFAULT_IS_STATUSBAR_VISIBLE,
/* isKeyguardVisibleAndOccluded */ true,
DEFAULT_IS_IN_FULL_IMMERSIVE_MODE,
+ DEFAULT_IS_DRAGGING,
new InsetsState(),
DEFAULT_HAS_GLOBAL_FOCUS,
mExclusionRegion,
@@ -883,6 +893,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* isStatusBarVisible */ true,
DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED,
/* inFullImmersiveMode */ true,
+ DEFAULT_IS_DRAGGING,
new InsetsState(),
DEFAULT_HAS_GLOBAL_FOCUS,
mExclusionRegion,
@@ -901,6 +912,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* isStatusBarVisible */ false,
DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED,
/* inFullImmersiveMode */ true,
+ DEFAULT_IS_DRAGGING,
new InsetsState(),
DEFAULT_HAS_GLOBAL_FOCUS,
mExclusionRegion,
@@ -911,6 +923,33 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_IMMERSIVE_DRAG_BUGFIX)
+ public void updateRelayoutParams_header_fullyImmersive_captionVisDuringDrag() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ final RelayoutParams relayoutParams = new RelayoutParams();
+
+ DesktopModeWindowDecoration.updateRelayoutParams(
+ relayoutParams,
+ mTestableContext,
+ taskInfo,
+ mMockSplitScreenController,
+ DEFAULT_APPLY_START_TRANSACTION_ON_DRAW,
+ DEFAULT_SHOULD_SET_TASK_POSITIONING_AND_CROP,
+ /* isStatusBarVisible */ false,
+ DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED,
+ /* inFullImmersiveMode */ true,
+ /* isDragging */ true,
+ new InsetsState(),
+ DEFAULT_HAS_GLOBAL_FOCUS,
+ mExclusionRegion,
+ DEFAULT_SHOULD_IGNORE_CORNER_RADIUS,
+ DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS);
+
+ assertThat(relayoutParams.mIsCaptionVisible).isTrue();
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
public void updateRelayoutParams_header_fullyImmersive_overKeyguard_captionNotVisible() {
final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
@@ -927,6 +966,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
DEFAULT_IS_STATUSBAR_VISIBLE,
/* isKeyguardVisibleAndOccluded */ true,
/* inFullImmersiveMode */ true,
+ DEFAULT_IS_DRAGGING,
new InsetsState(),
DEFAULT_HAS_GLOBAL_FOCUS,
mExclusionRegion,
@@ -1588,6 +1628,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
DEFAULT_IS_STATUSBAR_VISIBLE,
DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED,
DEFAULT_IS_IN_FULL_IMMERSIVE_MODE,
+ DEFAULT_IS_DRAGGING,
new InsetsState(),
DEFAULT_HAS_GLOBAL_FOCUS,
mExclusionRegion,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index a2927fa3527b..9a2e2fad50be 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -60,6 +60,7 @@ import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.Region;
import android.os.Handler;
+import android.os.LocaleList;
import android.testing.AndroidTestingRunner;
import android.util.DisplayMetrics;
import android.view.AttachedSurfaceControl;
@@ -97,6 +98,7 @@ import org.mockito.Mockito;
import java.util.ArrayList;
import java.util.List;
+import java.util.Locale;
import java.util.function.Supplier;
/**
@@ -475,6 +477,50 @@ public class WindowDecorationTests extends ShellTestCase {
}
@Test
+ public void testReinflateViewsOnLocaleListChange() {
+ final Display defaultDisplay = mock(Display.class);
+ doReturn(defaultDisplay).when(mMockDisplayController)
+ .getDisplay(Display.DEFAULT_DISPLAY);
+
+ final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+ .setVisible(true)
+ .setDisplayId(Display.DEFAULT_DISPLAY)
+ .build();
+ taskInfo.configuration.setLocales(new LocaleList(Locale.FRANCE, Locale.US));
+ final TestWindowDecoration windowDecor = spy(createWindowDecoration(taskInfo));
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, Region.obtain());
+ clearInvocations(windowDecor);
+
+ final ActivityManager.RunningTaskInfo taskInfo2 = new TestRunningTaskInfoBuilder()
+ .setVisible(true)
+ .setDisplayId(Display.DEFAULT_DISPLAY)
+ .build();
+ taskInfo2.configuration.setLocales(new LocaleList(Locale.US, Locale.FRANCE));
+ windowDecor.relayout(taskInfo2, true /* hasGlobalFocus */, Region.obtain());
+ // WindowDecoration#releaseViews should be called since the locale list has changed.
+ verify(windowDecor, times(1)).releaseViews(any());
+ }
+
+ @Test
+ public void testViewNotReinflatedWhenLocaleListNotChanged() {
+ final Display defaultDisplay = mock(Display.class);
+ doReturn(defaultDisplay).when(mMockDisplayController)
+ .getDisplay(Display.DEFAULT_DISPLAY);
+
+ final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+ .setVisible(true)
+ .setDisplayId(Display.DEFAULT_DISPLAY)
+ .build();
+ taskInfo.configuration.setLocales(new LocaleList(Locale.FRANCE, Locale.US));
+ final TestWindowDecoration windowDecor = spy(createWindowDecoration(taskInfo));
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, Region.obtain());
+ clearInvocations(windowDecor);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, Region.obtain());
+ // WindowDecoration#releaseViews should not be called since nothing has changed.
+ verify(windowDecor, never()).releaseViews(any());
+ }
+
+ @Test
public void testLayoutResultCalculation_fullWidthCaption() {
final Display defaultDisplay = mock(Display.class);
doReturn(defaultDisplay).when(mMockDisplayController)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt
index c61e0eb3b5af..c8ccac35d4c4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt
@@ -23,6 +23,7 @@ import android.content.pm.ActivityInfo
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
+import android.os.LocaleList
import android.os.UserHandle
import android.testing.AndroidTestingRunner
import android.testing.TestableContext
@@ -39,6 +40,7 @@ import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.sysui.UserChangeListener
import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader.AppResources
import com.google.common.truth.Truth.assertThat
+import java.util.Locale
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
@@ -116,8 +118,10 @@ class WindowDecorTaskResourceLoaderTest : ShellTestCase() {
@Test
fun testGetName_cached_returnsFromCache() {
val task = createTaskInfo(context.userId)
+ task.configuration.setLocales(LocaleList(Locale.US))
loader.onWindowDecorCreated(task)
loader.taskToResourceCache[task.taskId] = AppResources("App Name", mock(), mock())
+ loader.localeListOnCache[task.taskId] = LocaleList(Locale.US)
loader.getName(task)
@@ -130,6 +134,19 @@ class WindowDecorTaskResourceLoaderTest : ShellTestCase() {
}
@Test
+ fun testGetName_cached_localesChanged_loadsResourceAndCaches() {
+ val task = createTaskInfo(context.userId)
+ loader.onWindowDecorCreated(task)
+ loader.taskToResourceCache[task.taskId] = AppResources("App Name", mock(), mock())
+ loader.localeListOnCache[task.taskId] = LocaleList(Locale.US, Locale.FRANCE)
+ task.configuration.setLocales(LocaleList(Locale.FRANCE, Locale.US))
+ doReturn("App Name but in French").whenever(mockPackageManager).getApplicationLabel(any())
+
+ assertThat(loader.getName(task)).isEqualTo("App Name but in French")
+ assertThat(loader.taskToResourceCache[task.taskId]?.appName).isEqualTo("App Name but in French")
+ }
+
+ @Test
fun testGetHeaderIcon_notCached_loadsResourceAndCaches() {
val task = createTaskInfo(context.userId)
loader.onWindowDecorCreated(task)
diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp
index a18c5f5f92f6..8ecd6ba9b253 100644
--- a/libs/androidfw/ResourceTypes.cpp
+++ b/libs/androidfw/ResourceTypes.cpp
@@ -6520,41 +6520,79 @@ base::expected<StringPiece16, NullOrIOError> StringPoolRef::string16() const {
}
bool ResTable::getResourceFlags(uint32_t resID, uint32_t* outFlags) const {
- if (mError != NO_ERROR) {
- return false;
- }
+ if (mError != NO_ERROR) {
+ return false;
+ }
- const ssize_t p = getResourcePackageIndex(resID);
- const int t = Res_GETTYPE(resID);
- const int e = Res_GETENTRY(resID);
+ const ssize_t p = getResourcePackageIndex(resID);
+ const int t = Res_GETTYPE(resID);
+ const int e = Res_GETENTRY(resID);
- if (p < 0) {
- if (Res_GETPACKAGE(resID)+1 == 0) {
- ALOGW("No package identifier when getting flags for resource number 0x%08x", resID);
- } else {
- ALOGW("No known package when getting flags for resource number 0x%08x", resID);
- }
- return false;
- }
- if (t < 0) {
- ALOGW("No type identifier when getting flags for resource number 0x%08x", resID);
- return false;
+ if (p < 0) {
+ if (Res_GETPACKAGE(resID)+1 == 0) {
+ ALOGW("No package identifier when getting flags for resource number 0x%08x", resID);
+ } else {
+ ALOGW("No known package when getting flags for resource number 0x%08x", resID);
}
+ return false;
+ }
+ if (t < 0) {
+ ALOGW("No type identifier when getting flags for resource number 0x%08x", resID);
+ return false;
+ }
- const PackageGroup* const grp = mPackageGroups[p];
- if (grp == NULL) {
- ALOGW("Bad identifier when getting flags for resource number 0x%08x", resID);
- return false;
- }
+ const PackageGroup* const grp = mPackageGroups[p];
+ if (grp == NULL) {
+ ALOGW("Bad identifier when getting flags for resource number 0x%08x", resID);
+ return false;
+ }
- Entry entry;
- status_t err = getEntry(grp, t, e, NULL, &entry);
- if (err != NO_ERROR) {
- return false;
+ Entry entry;
+ status_t err = getEntry(grp, t, e, NULL, &entry);
+ if (err != NO_ERROR) {
+ return false;
+ }
+
+ *outFlags = entry.specFlags;
+ return true;
+}
+
+bool ResTable::getResourceEntryFlags(uint32_t resID, uint32_t* outFlags) const {
+ if (mError != NO_ERROR) {
+ return false;
+ }
+
+ const ssize_t p = getResourcePackageIndex(resID);
+ const int t = Res_GETTYPE(resID);
+ const int e = Res_GETENTRY(resID);
+
+ if (p < 0) {
+ if (Res_GETPACKAGE(resID)+1 == 0) {
+ ALOGW("No package identifier when getting flags for resource number 0x%08x", resID);
+ } else {
+ ALOGW("No known package when getting flags for resource number 0x%08x", resID);
}
+ return false;
+ }
+ if (t < 0) {
+ ALOGW("No type identifier when getting flags for resource number 0x%08x", resID);
+ return false;
+ }
- *outFlags = entry.specFlags;
- return true;
+ const PackageGroup* const grp = mPackageGroups[p];
+ if (grp == NULL) {
+ ALOGW("Bad identifier when getting flags for resource number 0x%08x", resID);
+ return false;
+ }
+
+ Entry entry;
+ status_t err = getEntry(grp, t, e, NULL, &entry);
+ if (err != NO_ERROR) {
+ return false;
+ }
+
+ *outFlags = entry.entry->flags();
+ return true;
}
bool ResTable::isPackageDynamic(uint8_t packageID) const {
diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h
index 30594dcfa939..63b28da075cd 100644
--- a/libs/androidfw/include/androidfw/ResourceTypes.h
+++ b/libs/androidfw/include/androidfw/ResourceTypes.h
@@ -1265,6 +1265,9 @@ struct ResTable_config
// Varies in length from 3 to 8 chars. Zero-filled value.
char localeNumberingSystem[8];
+ // Mark all padding explicitly so it's clear how much we can expand it.
+ char endPadding[3];
+
void copyFromDeviceNoSwap(const ResTable_config& o) {
const auto o_size = dtohl(o.size);
if (o_size >= sizeof(ResTable_config)) [[likely]] {
@@ -1422,6 +1425,13 @@ struct ResTable_config
void swapHtoD_slow();
};
+// Fix the struct size for backward compatibility
+static_assert(sizeof(ResTable_config) == 64);
+
+// Make sure there's no unaccounted padding in the structure.
+static_assert(offsetof(ResTable_config, endPadding) +
+ sizeof(ResTable_config::endPadding) == sizeof(ResTable_config));
+
/**
* A specification of the resources defined by a particular type.
*
@@ -1583,6 +1593,8 @@ union ResTable_entry
// If set, this is a compact entry with data type and value directly
// encoded in the this entry, see ResTable_entry::compact
FLAG_COMPACT = 0x0008,
+ // If set, this entry relies on read write android feature flags
+ FLAG_USES_FEATURE_FLAGS = 0x0010,
};
struct Full {
@@ -1612,6 +1624,7 @@ union ResTable_entry
uint16_t flags() const { return dtohs(full.flags); };
bool is_compact() const { return flags() & FLAG_COMPACT; }
bool is_complex() const { return flags() & FLAG_COMPLEX; }
+ bool uses_feature_flags() const { return flags() & FLAG_USES_FEATURE_FLAGS; }
size_t size() const {
return is_compact() ? sizeof(ResTable_entry) : dtohs(this->full.size);
@@ -2029,6 +2042,8 @@ public:
bool getResourceFlags(uint32_t resID, uint32_t* outFlags) const;
+ bool getResourceEntryFlags(uint32_t resID, uint32_t* outFlags) const;
+
/**
* Returns whether or not the package for the given resource has been dynamically assigned.
* If the resource can't be found, returns 'false'.
diff --git a/libs/hostgraphics/HostBufferQueue.cpp b/libs/hostgraphics/HostBufferQueue.cpp
index 7e14b88a47fa..ef5406250251 100644
--- a/libs/hostgraphics/HostBufferQueue.cpp
+++ b/libs/hostgraphics/HostBufferQueue.cpp
@@ -29,6 +29,7 @@ public:
}
virtual status_t detachBuffer(int slot) {
+ mBuffer.clear();
return OK;
}
diff --git a/libs/hwui/jni/Graphics.cpp b/libs/hwui/jni/Graphics.cpp
index a210ddf54b2e..7d227f793817 100644
--- a/libs/hwui/jni/Graphics.cpp
+++ b/libs/hwui/jni/Graphics.cpp
@@ -722,10 +722,15 @@ void RecyclingClippingPixelAllocator::copyIfNecessary() {
auto canvas = SkCanvas(recycledPixels->getSkBitmap());
SkRect destination = SkRect::Make(recycledPixels->info().bounds());
- destination.intersect(SkRect::Make(mSkiaBitmap->info().bounds()));
- canvas.drawImageRect(mSkiaBitmap->asImage(), *mDesiredSubset, destination,
- SkSamplingOptions(SkFilterMode::kLinear), nullptr,
- SkCanvas::kFast_SrcRectConstraint);
+ if (destination.intersect(SkRect::Make(mSkiaBitmap->info().bounds()))) {
+ canvas.drawImageRect(mSkiaBitmap->asImage(), *mDesiredSubset, destination,
+ SkSamplingOptions(SkFilterMode::kLinear), nullptr,
+ SkCanvas::kFast_SrcRectConstraint);
+ } else {
+ // The canvas would have discarded the draw operation automatically, but
+ // this case should have been detected before getting to this point.
+ ALOGE("Copy destination does not intersect image bounds");
+ }
} else {
void* dst = recycledPixels->pixels();
const size_t dstRowBytes = mRecycledBitmap->rowBytes();
diff --git a/location/java/android/location/flags/location.aconfig b/location/java/android/location/flags/location.aconfig
index 83b1778fd611..f8eb41826c6f 100644
--- a/location/java/android/location/flags/location.aconfig
+++ b/location/java/android/location/flags/location.aconfig
@@ -179,3 +179,10 @@ flag {
}
}
+flag {
+ name: "gnss_location_provider_overlay_2025_devices"
+ namespace: "location"
+ description: "Flag for GNSS location provider overlay for 2025 devices"
+ bug: "398254728"
+ is_fixed_read_only: true
+}
diff --git a/media/java/Android.bp b/media/java/Android.bp
index 6878f9d61f6d..28b9d3bbc167 100644
--- a/media/java/Android.bp
+++ b/media/java/Android.bp
@@ -15,6 +15,7 @@ filegroup {
],
exclude_srcs: [
":framework-media-tv-tunerresourcemanager-sources-aidl",
+ ":framework-media-quality-sources-aidl",
],
visibility: [
"//frameworks/base",
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index d082c7384fd1..32af7c6fca68 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -4857,7 +4857,7 @@ public class AudioManager {
focusReceiver = addClientIdToFocusReceiverLocked(clientFakeId);
}
- return handleExternalAudioPolicyWaitIfNeeded(clientFakeId, focusReceiver);
+ return handleExternalAudioPolicyWaitIfNeeded(clientFakeId, focusReceiver, afr);
}
/**
@@ -5070,7 +5070,7 @@ public class AudioManager {
focusReceiver = addClientIdToFocusReceiverLocked(clientId);
}
- return handleExternalAudioPolicyWaitIfNeeded(clientId, focusReceiver);
+ return handleExternalAudioPolicyWaitIfNeeded(clientId, focusReceiver, afr);
}
@GuardedBy("mFocusRequestsLock")
@@ -5086,11 +5086,20 @@ public class AudioManager {
}
private @FocusRequestResult int handleExternalAudioPolicyWaitIfNeeded(String clientId,
- BlockingFocusResultReceiver focusReceiver) {
+ BlockingFocusResultReceiver focusReceiver, @NonNull AudioFocusRequest afr) {
focusReceiver.waitForResult(EXT_FOCUS_POLICY_TIMEOUT_MS);
- if (DEBUG && !focusReceiver.receivedResult()) {
- Log.e(TAG, "handleExternalAudioPolicyWaitIfNeeded"
- + " response from ext policy timed out, denying request");
+ if (!focusReceiver.receivedResult()) {
+ if (DEBUG) {
+ Log.e(TAG, "handleExternalAudioPolicyWaitIfNeeded"
+ + " response from ext policy timed out, denying request");
+ }
+ try {
+ // To prevent from orphan focus holder, cleanup
+ abandonAudioFocus(afr.getOnAudioFocusChangeListener());
+ } catch (Exception e) {
+ Log.e(TAG, "handleExternalAudioPolicyWaitIfNeeded failed to abandon audio"
+ +" focus after time out, error: " + e.getMessage());
+ }
}
synchronized (mFocusRequestsLock) {
diff --git a/media/java/android/media/IMediaRouter2.aidl b/media/java/android/media/IMediaRouter2.aidl
index 85bc8efe2750..e9590d50d719 100644
--- a/media/java/android/media/IMediaRouter2.aidl
+++ b/media/java/android/media/IMediaRouter2.aidl
@@ -18,6 +18,7 @@ package android.media;
import android.media.MediaRoute2Info;
import android.media.RoutingSessionInfo;
+import android.media.SuggestedDeviceInfo;
import android.os.Bundle;
import android.os.UserHandle;
@@ -37,4 +38,6 @@ oneway interface IMediaRouter2 {
*/
void requestCreateSessionByManager(long uniqueRequestId, in RoutingSessionInfo oldSession,
in MediaRoute2Info route);
+ void notifyDeviceSuggestionsUpdated(String suggestingPackageName,
+ in List<SuggestedDeviceInfo> suggestions);
}
diff --git a/media/java/android/media/IMediaRouter2Manager.aidl b/media/java/android/media/IMediaRouter2Manager.aidl
index 21908b2ca2e0..1c399d6958bb 100644
--- a/media/java/android/media/IMediaRouter2Manager.aidl
+++ b/media/java/android/media/IMediaRouter2Manager.aidl
@@ -21,6 +21,7 @@ import android.media.MediaRoute2Info;
import android.media.RouteDiscoveryPreference;
import android.media.RouteListingPreference;
import android.media.RoutingSessionInfo;
+import android.media.SuggestedDeviceInfo;
/**
* {@hide}
@@ -33,6 +34,8 @@ oneway interface IMediaRouter2Manager {
in RouteDiscoveryPreference discoveryPreference);
void notifyRouteListingPreferenceChange(String packageName,
in @nullable RouteListingPreference routeListingPreference);
+ void notifyDeviceSuggestionsUpdated(String packageName, String suggestingPackageName,
+ in @nullable List<SuggestedDeviceInfo> suggestedDeviceInfo);
void notifyRoutesUpdated(in List<MediaRoute2Info> routes);
void notifyRequestFailed(int requestId, int reason);
void invalidateInstance();
diff --git a/media/java/android/media/IMediaRouterService.aidl b/media/java/android/media/IMediaRouterService.aidl
index 961962f6a010..60881f4bfc30 100644
--- a/media/java/android/media/IMediaRouterService.aidl
+++ b/media/java/android/media/IMediaRouterService.aidl
@@ -25,6 +25,7 @@ import android.media.MediaRouterClientState;
import android.media.RouteDiscoveryPreference;
import android.media.RouteListingPreference;
import android.media.RoutingSessionInfo;
+import android.media.SuggestedDeviceInfo;
import android.os.Bundle;
import android.os.UserHandle;
/**
@@ -72,6 +73,10 @@ interface IMediaRouterService {
in MediaRoute2Info route);
void setSessionVolumeWithRouter2(IMediaRouter2 router, String sessionId, int volume);
void releaseSessionWithRouter2(IMediaRouter2 router, String sessionId);
+ void setDeviceSuggestionsWithRouter2(IMediaRouter2 router,
+ in @nullable List<SuggestedDeviceInfo> suggestedDeviceInfo);
+ @nullable Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithRouter2(
+ IMediaRouter2 router);
// Methods for MediaRouter2Manager
List<RoutingSessionInfo> getRemoteSessions(IMediaRouter2Manager manager);
@@ -98,4 +103,8 @@ interface IMediaRouterService {
String sessionId, int volume);
void releaseSessionWithManager(IMediaRouter2Manager manager, int requestId, String sessionId);
boolean showMediaOutputSwitcherWithProxyRouter(IMediaRouter2Manager manager);
+ void setDeviceSuggestionsWithManager(IMediaRouter2Manager manager,
+ in @nullable List<SuggestedDeviceInfo> suggestedDeviceInfo);
+ @nullable Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithManager(
+ IMediaRouter2Manager manager);
}
diff --git a/media/java/android/media/MediaCodecInfo.java b/media/java/android/media/MediaCodecInfo.java
index 4e86eacea404..f3b21bfdaa3c 100644
--- a/media/java/android/media/MediaCodecInfo.java
+++ b/media/java/android/media/MediaCodecInfo.java
@@ -3836,6 +3836,151 @@ public final class MediaCodecInfo {
maxBlocks, maxBlocksPerSecond,
blockSize, blockSize,
1 /* widthAlignment */, 1 /* heightAlignment */);
+ } else if (GetFlag(() -> android.media.codec.Flags.apvSupport())
+ && mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_APV)) {
+ maxBlocksPerSecond = 11880;
+ maxBps = 7000000;
+
+ // Sample rate, and Bit rate for APV Codec,
+ // corresponding to the definitions in
+ // "10.1.4. Levels and bands"
+ // found at https://www.ietf.org/archive/id/draft-lim-apv-03.html
+ for (CodecProfileLevel profileLevel: profileLevels) {
+ long SR = 0; // luma sample rate
+ int BR = 0; // bit rate bps
+ switch (profileLevel.level) {
+ case CodecProfileLevel.APVLevel1Band0:
+ SR = 3041280; BR = 7000000; break;
+ case CodecProfileLevel.APVLevel1Band1:
+ SR = 3041280; BR = 11000000; break;
+ case CodecProfileLevel.APVLevel1Band2:
+ SR = 3041280; BR = 14000000; break;
+ case CodecProfileLevel.APVLevel1Band3:
+ SR = 3041280; BR = 21000000; break;
+ case CodecProfileLevel.APVLevel11Band0:
+ SR = 6082560; BR = 14000000; break;
+ case CodecProfileLevel.APVLevel11Band1:
+ SR = 6082560; BR = 21000000; break;
+ case CodecProfileLevel.APVLevel11Band2:
+ SR = 6082560; BR = 28000000; break;
+ case CodecProfileLevel.APVLevel11Band3:
+ SR = 6082560; BR = 42000000; break;
+ case CodecProfileLevel.APVLevel2Band0:
+ SR = 15667200; BR = 36000000; break;
+ case CodecProfileLevel.APVLevel2Band1:
+ SR = 15667200; BR = 53000000; break;
+ case CodecProfileLevel.APVLevel2Band2:
+ SR = 15667200; BR = 71000000; break;
+ case CodecProfileLevel.APVLevel2Band3:
+ SR = 15667200; BR = 106000000; break;
+ case CodecProfileLevel.APVLevel21Band0:
+ SR = 31334400; BR = 71000000; break;
+ case CodecProfileLevel.APVLevel21Band1:
+ SR = 31334400; BR = 106000000; break;
+ case CodecProfileLevel.APVLevel21Band2:
+ SR = 31334400; BR = 141000000; break;
+ case CodecProfileLevel.APVLevel21Band3:
+ SR = 31334400; BR = 212000000; break;
+ case CodecProfileLevel.APVLevel3Band0:
+ SR = 66846720; BR = 101000000; break;
+ case CodecProfileLevel.APVLevel3Band1:
+ SR = 66846720; BR = 151000000; break;
+ case CodecProfileLevel.APVLevel3Band2:
+ SR = 66846720; BR = 201000000; break;
+ case CodecProfileLevel.APVLevel3Band3:
+ SR = 66846720; BR = 301000000; break;
+ case CodecProfileLevel.APVLevel31Band0:
+ SR = 133693440; BR = 201000000; break;
+ case CodecProfileLevel.APVLevel31Band1:
+ SR = 133693440; BR = 301000000; break;
+ case CodecProfileLevel.APVLevel31Band2:
+ SR = 133693440; BR = 401000000; break;
+ case CodecProfileLevel.APVLevel31Band3:
+ SR = 133693440; BR = 602000000; break;
+ case CodecProfileLevel.APVLevel4Band0:
+ SR = 265420800; BR = 401000000; break;
+ case CodecProfileLevel.APVLevel4Band1:
+ SR = 265420800; BR = 602000000; break;
+ case CodecProfileLevel.APVLevel4Band2:
+ SR = 265420800; BR = 780000000; break;
+ case CodecProfileLevel.APVLevel4Band3:
+ SR = 265420800; BR = 1170000000; break;
+ case CodecProfileLevel.APVLevel41Band0:
+ SR = 530841600; BR = 780000000; break;
+ case CodecProfileLevel.APVLevel41Band1:
+ SR = 530841600; BR = 1170000000; break;
+ case CodecProfileLevel.APVLevel41Band2:
+ SR = 530841600; BR = 1560000000; break;
+ case CodecProfileLevel.APVLevel41Band3:
+ // Current API allows bitrates only up to Max Integer
+ // Hence we are limiting internal limits to Integer.MAX_VALUE
+ // even when actual Level/Band limits are higher
+ SR = 530841600; BR = Integer.MAX_VALUE; break;
+ case CodecProfileLevel.APVLevel5Band0:
+ SR = 1061683200; BR = 1560000000; break;
+ case CodecProfileLevel.APVLevel5Band1:
+ SR = 1061683200; BR = Integer.MAX_VALUE; break;
+ case CodecProfileLevel.APVLevel5Band2:
+ SR = 1061683200; BR = Integer.MAX_VALUE; break;
+ case CodecProfileLevel.APVLevel5Band3:
+ SR = 1061683200; BR = Integer.MAX_VALUE; break;
+ case CodecProfileLevel.APVLevel51Band0:
+ case CodecProfileLevel.APVLevel51Band1:
+ case CodecProfileLevel.APVLevel51Band2:
+ case CodecProfileLevel.APVLevel51Band3:
+ SR = 2123366400; BR = Integer.MAX_VALUE; break;
+ case CodecProfileLevel.APVLevel6Band0:
+ case CodecProfileLevel.APVLevel6Band1:
+ case CodecProfileLevel.APVLevel6Band2:
+ case CodecProfileLevel.APVLevel6Band3:
+ SR = 4777574400L; BR = Integer.MAX_VALUE; break;
+ case CodecProfileLevel.APVLevel61Band0:
+ case CodecProfileLevel.APVLevel61Band1:
+ case CodecProfileLevel.APVLevel61Band2:
+ case CodecProfileLevel.APVLevel61Band3:
+ SR = 8493465600L; BR = Integer.MAX_VALUE; break;
+ case CodecProfileLevel.APVLevel7Band0:
+ case CodecProfileLevel.APVLevel7Band1:
+ case CodecProfileLevel.APVLevel7Band2:
+ case CodecProfileLevel.APVLevel7Band3:
+ SR = 16986931200L; BR = Integer.MAX_VALUE; break;
+ case CodecProfileLevel.APVLevel71Band0:
+ case CodecProfileLevel.APVLevel71Band1:
+ case CodecProfileLevel.APVLevel71Band2:
+ case CodecProfileLevel.APVLevel71Band3:
+ SR = 33973862400L; BR = Integer.MAX_VALUE; break;
+ default:
+ Log.w(TAG, "Unrecognized level "
+ + profileLevel.level + " for " + mime);
+ errors |= ERROR_UNRECOGNIZED;
+ }
+ switch (profileLevel.profile) {
+ case CodecProfileLevel.APVProfile422_10:
+ case CodecProfileLevel.APVProfile422_10HDR10:
+ case CodecProfileLevel.APVProfile422_10HDR10Plus:
+ break;
+ default:
+ Log.w(TAG, "Unrecognized profile "
+ + profileLevel.profile + " for " + mime);
+ errors |= ERROR_UNRECOGNIZED;
+ }
+ errors &= ~ERROR_NONE_SUPPORTED;
+ maxBlocksPerSecond = Math.max(SR, maxBlocksPerSecond);
+ maxBps = Math.max(BR, maxBps);
+ }
+
+ final int blockSize = 16;
+ maxBlocks = Integer.MAX_VALUE;
+ maxBlocksPerSecond = Utils.divUp(maxBlocksPerSecond, blockSize * blockSize);
+ maxBlocks = (int) Math.min((long) maxBlocks, maxBlocksPerSecond);
+ // Max frame size in APV is 2^24
+ int maxLengthInBlocks = Utils.divUp((int) Math.pow(2, 24), blockSize);
+ maxLengthInBlocks = Math.min(maxLengthInBlocks, maxBlocks);
+ applyMacroBlockLimits(
+ maxLengthInBlocks, maxLengthInBlocks,
+ maxBlocks, maxBlocksPerSecond,
+ blockSize, blockSize,
+ 2 /* widthAlignment */, 1 /* heightAlignment */);
} else {
Log.w(TAG, "Unsupported mime " + mime);
// using minimal bitrate here. should be overriden by
diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java
index 3af36a404c30..db305effac3f 100644
--- a/media/java/android/media/MediaRouter2.java
+++ b/media/java/android/media/MediaRouter2.java
@@ -22,6 +22,7 @@ import static com.android.media.flags.Flags.FLAG_ENABLE_GET_TRANSFERABLE_ROUTES;
import static com.android.media.flags.Flags.FLAG_ENABLE_PRIVILEGED_ROUTING_FOR_MEDIA_ROUTING_CONTROL;
import static com.android.media.flags.Flags.FLAG_ENABLE_RLP_CALLBACKS_IN_MEDIA_ROUTER2;
import static com.android.media.flags.Flags.FLAG_ENABLE_SCREEN_OFF_SCANNING;
+import static com.android.media.flags.Flags.FLAG_ENABLE_SUGGESTED_DEVICE_API;
import android.Manifest;
import android.annotation.CallbackExecutor;
@@ -159,6 +160,8 @@ public final class MediaRouter2 {
new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<ControllerCallbackRecord> mControllerCallbackRecords =
new CopyOnWriteArrayList<>();
+ private final CopyOnWriteArrayList<DeviceSuggestionsCallbackRecord>
+ mDeviceSuggestionsCallbackRecords = new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<ControllerCreationRequest> mControllerCreationRequests =
new CopyOnWriteArrayList<>();
@@ -198,6 +201,10 @@ public final class MediaRouter2 {
@Nullable
private RouteListingPreference mRouteListingPreference;
+ @GuardedBy("mLock")
+ @Nullable
+ private Map<String, List<SuggestedDeviceInfo>> mSuggestedDeviceInfo = new HashMap<>();
+
/**
* Stores an auxiliary copy of {@link #mFilteredRoutes} at the time of the last route callback
* dispatch. This is only used to determine what callback a route should be assigned to (added,
@@ -760,6 +767,27 @@ public final class MediaRouter2 {
}
/**
+ * Registers the given callback to be invoked when the {@link SuggestedDeviceInfo} of the target
+ * router changes.
+ *
+ * <p>Calls using a previously registered callback will overwrite the previous executor.
+ *
+ * @hide
+ */
+ public void registerDeviceSuggestionsCallback(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull DeviceSuggestionsCallback deviceSuggestionsCallback) {
+ Objects.requireNonNull(executor, "executor must not be null");
+ Objects.requireNonNull(deviceSuggestionsCallback, "callback must not be null");
+
+ DeviceSuggestionsCallbackRecord record =
+ new DeviceSuggestionsCallbackRecord(executor, deviceSuggestionsCallback);
+
+ mDeviceSuggestionsCallbackRecords.remove(record);
+ mDeviceSuggestionsCallbackRecords.add(record);
+ }
+
+ /**
* Unregisters the given callback to not receive {@link RouteListingPreference} change events.
*
* @see #registerRouteListingPreferenceUpdatedCallback(Executor, Consumer)
@@ -779,6 +807,21 @@ public final class MediaRouter2 {
}
/**
+ * Unregisters the given callback to not receive {@link SuggestedDeviceInfo} change events.
+ *
+ * @see #registerDeviceSuggestionsCallback(Executor, DeviceSuggestionsCallback)
+ * @hide
+ */
+ public void unregisterDeviceSuggestionsCallback(@NonNull DeviceSuggestionsCallback callback) {
+ Objects.requireNonNull(callback, "callback must not be null");
+
+ if (!mDeviceSuggestionsCallbackRecords.remove(
+ new DeviceSuggestionsCallbackRecord(/* executor */ null, callback))) {
+ Log.w(TAG, "unregisterDeviceSuggestionsCallback: Ignoring an unknown" + " callback");
+ }
+ }
+
+ /**
* Shows the system output switcher dialog.
*
* <p>Should only be called when the context of MediaRouter2 is in the foreground and visible on
@@ -832,6 +875,36 @@ public final class MediaRouter2 {
}
/**
+ * Sets the suggested devices.
+ *
+ * <p>Use this method to inform the system UI that this device is suggested in the Output
+ * Switcher and media controls.
+ *
+ * <p>You should pass null to this method to clear a previously set suggestion without setting a
+ * new one.
+ *
+ * @param suggestedDeviceInfo The {@link SuggestedDeviceInfo} the router suggests should be
+ * provided to the user.
+ * @hide
+ */
+ @FlaggedApi(FLAG_ENABLE_SUGGESTED_DEVICE_API)
+ public void setDeviceSuggestions(@Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) {
+ mImpl.setDeviceSuggestions(suggestedDeviceInfo);
+ }
+
+ /**
+ * Gets the current suggested devices.
+ *
+ * @return the suggested devices, keyed by the package name providing each suggestion list.
+ * @hide
+ */
+ @FlaggedApi(FLAG_ENABLE_SUGGESTED_DEVICE_API)
+ @Nullable
+ public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestions() {
+ return mImpl.getDeviceSuggestions();
+ }
+
+ /**
* Returns the current {@link RouteListingPreference} of the target router.
*
* <p>If this instance was created using {@code #getInstance(Context, String)}, then it returns
@@ -1518,6 +1591,17 @@ public final class MediaRouter2 {
}
}
+ private void notifyDeviceSuggestionsUpdated(
+ @NonNull String suggestingPackageName,
+ @Nullable List<SuggestedDeviceInfo> deviceSuggestions) {
+ for (DeviceSuggestionsCallbackRecord record : mDeviceSuggestionsCallbackRecords) {
+ record.mExecutor.execute(
+ () ->
+ record.mDeviceSuggestionsCallback.onSuggestionUpdated(
+ suggestingPackageName, deviceSuggestions));
+ }
+ }
+
private void notifyTransfer(RoutingController oldController, RoutingController newController) {
for (TransferCallbackRecord record : mTransferCallbackRecords) {
record.mExecutor.execute(
@@ -1568,6 +1652,25 @@ public final class MediaRouter2 {
.build();
}
+ /**
+ * Callback for receiving events about device suggestions
+ *
+ * @hide
+ */
+ public interface DeviceSuggestionsCallback {
+
+ /**
+ * Called when suggestions are updated. Whenever you register a callback, this will be
+ * invoked with the current suggestions.
+ *
+ * @param suggestingPackageName the package that provided the suggestions.
+ * @param suggestedDeviceInfo the suggestions provided by the package.
+ */
+ void onSuggestionUpdated(
+ @NonNull String suggestingPackageName,
+ @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo);
+ }
+
/** Callback for receiving events about media route discovery. */
public abstract static class RouteCallback {
/**
@@ -2326,6 +2429,35 @@ public final class MediaRouter2 {
}
}
+ private static final class DeviceSuggestionsCallbackRecord {
+ public final Executor mExecutor;
+ public final DeviceSuggestionsCallback mDeviceSuggestionsCallback;
+
+ /* package */ DeviceSuggestionsCallbackRecord(
+ @NonNull Executor executor,
+ @NonNull DeviceSuggestionsCallback deviceSuggestionsCallback) {
+ mExecutor = executor;
+ mDeviceSuggestionsCallback = deviceSuggestionsCallback;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof DeviceSuggestionsCallbackRecord)) {
+ return false;
+ }
+ return mDeviceSuggestionsCallback
+ == ((DeviceSuggestionsCallbackRecord) obj).mDeviceSuggestionsCallback;
+ }
+
+ @Override
+ public int hashCode() {
+ return mDeviceSuggestionsCallback.hashCode();
+ }
+ }
+
static final class TransferCallbackRecord {
public final Executor mExecutor;
public final TransferCallback mTransferCallback;
@@ -2446,6 +2578,17 @@ public final class MediaRouter2 {
}
@Override
+ public void notifyDeviceSuggestionsUpdated(
+ String suggestingPackageName, List<SuggestedDeviceInfo> suggestions) {
+ mHandler.sendMessage(
+ obtainMessage(
+ MediaRouter2::notifyDeviceSuggestionsUpdated,
+ MediaRouter2.this,
+ suggestingPackageName,
+ suggestions));
+ }
+
+ @Override
public void requestCreateSessionByManager(
long managerRequestId, RoutingSessionInfo oldSession, MediaRoute2Info route) {
mHandler.sendMessage(
@@ -2487,6 +2630,11 @@ public final class MediaRouter2 {
void setRouteListingPreference(@Nullable RouteListingPreference preference);
+ void setDeviceSuggestions(@Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo);
+
+ @Nullable
+ Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestions();
+
boolean showSystemOutputSwitcher();
List<MediaRoute2Info> getAllRoutes();
@@ -2687,6 +2835,29 @@ public final class MediaRouter2 {
}
@Override
+ public void setDeviceSuggestions(@Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) {
+ synchronized (mLock) {
+ try {
+ mMediaRouterService.setDeviceSuggestionsWithManager(
+ mClient, suggestedDeviceInfo);
+ } catch (RemoteException ex) {
+ ex.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ @Override
+ public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestions() {
+ synchronized (mLock) {
+ try {
+ return mMediaRouterService.getDeviceSuggestionsWithManager(mClient);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ @Override
public boolean showSystemOutputSwitcher() {
try {
return mMediaRouterService.showMediaOutputSwitcherWithProxyRouter(mClient);
@@ -3296,6 +3467,23 @@ public final class MediaRouter2 {
notifyRouteListingPreferenceUpdated(routeListingPreference);
}
+ private void onDeviceSuggestionsChangeHandler(
+ @NonNull String packageName,
+ @NonNull String suggestingPackageName,
+ @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) {
+ if (!TextUtils.equals(getClientPackageName(), packageName)) {
+ return;
+ }
+ synchronized (mLock) {
+ if (Objects.equals(
+ mSuggestedDeviceInfo.get(suggestingPackageName), suggestedDeviceInfo)) {
+ return;
+ }
+ mSuggestedDeviceInfo.put(suggestingPackageName, suggestedDeviceInfo);
+ }
+ notifyDeviceSuggestionsUpdated(suggestingPackageName, suggestedDeviceInfo);
+ }
+
private void onRequestFailedOnHandler(int requestId, int reason) {
MediaRouter2Manager.TransferRequest matchingRequest = null;
for (MediaRouter2Manager.TransferRequest request : mTransferRequests) {
@@ -3390,6 +3578,20 @@ public final class MediaRouter2 {
}
@Override
+ public void notifyDeviceSuggestionsUpdated(
+ String packageName,
+ String suggestingPackageName,
+ @Nullable List<SuggestedDeviceInfo> deviceSuggestions) {
+ mHandler.sendMessage(
+ obtainMessage(
+ ProxyMediaRouter2Impl::onDeviceSuggestionsChangeHandler,
+ ProxyMediaRouter2Impl.this,
+ packageName,
+ suggestingPackageName,
+ deviceSuggestions));
+ }
+
+ @Override
public void notifyRoutesUpdated(List<MediaRoute2Info> routes) {
mHandler.sendMessage(
obtainMessage(
@@ -3553,6 +3755,30 @@ public final class MediaRouter2 {
}
@Override
+ public void setDeviceSuggestions(@Nullable List<SuggestedDeviceInfo> deviceSuggestions) {
+ synchronized (mLock) {
+ try {
+ registerRouterStubIfNeededLocked();
+ mMediaRouterService.setDeviceSuggestionsWithRouter2(mStub, deviceSuggestions);
+ } catch (RemoteException ex) {
+ ex.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ @Override
+ @Nullable
+ public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestions() {
+ synchronized (mLock) {
+ try {
+ return mMediaRouterService.getDeviceSuggestionsWithRouter2(mStub);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ @Override
public boolean showSystemOutputSwitcher() {
synchronized (mLock) {
try {
diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java
index 3f18eef2f9aa..bf88709eec33 100644
--- a/media/java/android/media/MediaRouter2Manager.java
+++ b/media/java/android/media/MediaRouter2Manager.java
@@ -1138,6 +1138,14 @@ public final class MediaRouter2Manager {
}
@Override
+ public void notifyDeviceSuggestionsUpdated(
+ String packageName,
+ String suggestingPackageName,
+ @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) {
+ // MediaRouter2Manager doesn't support device suggestions
+ }
+
+ @Override
public void notifyRoutesUpdated(List<MediaRoute2Info> routes) {
mHandler.sendMessage(
obtainMessage(
diff --git a/media/java/android/media/SuggestedDeviceInfo.aidl b/media/java/android/media/SuggestedDeviceInfo.aidl
new file mode 100644
index 000000000000..eab642572ed2
--- /dev/null
+++ b/media/java/android/media/SuggestedDeviceInfo.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+parcelable SuggestedDeviceInfo;
diff --git a/media/java/android/media/SuggestedDeviceInfo.java b/media/java/android/media/SuggestedDeviceInfo.java
new file mode 100644
index 000000000000..2aa139fcca17
--- /dev/null
+++ b/media/java/android/media/SuggestedDeviceInfo.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import static com.android.media.flags.Flags.FLAG_ENABLE_SUGGESTED_DEVICE_API;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.util.Objects;
+
+/**
+ * Allows applications to suggest routes to the system UI (for example, in the System UI Output
+ * Switcher).
+ *
+ * @see MediaRouter2#setSuggestedDevice
+ * @hide
+ */
+@FlaggedApi(FLAG_ENABLE_SUGGESTED_DEVICE_API)
+public final class SuggestedDeviceInfo implements Parcelable {
+ @NonNull
+ public static final Creator<SuggestedDeviceInfo> CREATOR =
+ new Creator<>() {
+ @Override
+ public SuggestedDeviceInfo createFromParcel(Parcel in) {
+ return new SuggestedDeviceInfo(in);
+ }
+
+ @Override
+ public SuggestedDeviceInfo[] newArray(int size) {
+ return new SuggestedDeviceInfo[size];
+ }
+ };
+
+ @NonNull private final String mDeviceDisplayName;
+
+ @NonNull private final String mRouteId;
+
+ private final int mType;
+
+ @NonNull private final Bundle mExtras;
+
+ private SuggestedDeviceInfo(Builder builder) {
+ mDeviceDisplayName = builder.mDeviceDisplayName;
+ mRouteId = builder.mRouteId;
+ mType = builder.mType;
+ mExtras = builder.mExtras;
+ }
+
+ private SuggestedDeviceInfo(Parcel in) {
+ mDeviceDisplayName = in.readString();
+ mRouteId = in.readString();
+ mType = in.readInt();
+ mExtras = in.readBundle();
+ }
+
+ /**
+ * Returns the name to be displayed to the user.
+ *
+ * @return The device display name.
+ */
+ @NonNull
+ public String getDeviceDisplayName() {
+ return mDeviceDisplayName;
+ }
+
+ /**
+ * Returns the route ID associated with the suggestion.
+ *
+ * @return The route ID.
+ */
+ @NonNull
+ public String getRouteId() {
+ return mRouteId;
+ }
+
+ /**
+ * Returns the device type associated with the suggestion.
+ *
+ * @return The device type.
+ */
+ public int getType() {
+ return mType;
+ }
+
+ /**
+ * Returns the extras associated with the suggestion.
+ *
+ * @return The extras.
+ */
+ @Nullable
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ // SuggestedDeviceInfo Parcelable implementation.
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeString(mDeviceDisplayName);
+ dest.writeString(mRouteId);
+ dest.writeInt(mType);
+ dest.writeBundle(mExtras);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof SuggestedDeviceInfo)) {
+ return false;
+ }
+ return Objects.equals(mDeviceDisplayName, ((SuggestedDeviceInfo) obj).mDeviceDisplayName)
+ && Objects.equals(mRouteId, ((SuggestedDeviceInfo) obj).mRouteId)
+ && mType == ((SuggestedDeviceInfo) obj).mType;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mDeviceDisplayName, mRouteId, mType);
+ }
+
+ @Override
+ public String toString() {
+ return mDeviceDisplayName + " | " + mRouteId + " | " + mType;
+ }
+
+ /** Builder for {@link SuggestedDeviceInfo}. */
+ public static final class Builder {
+ @NonNull private String mDeviceDisplayName;
+
+ @NonNull private String mRouteId;
+
+ @NonNull private Integer mType;
+
+ private Bundle mExtras = Bundle.EMPTY;
+
+ /**
+ * Creates a new SuggestedDeviceInfo. The device display name, route ID, and type must be
+ * set. The extras cannot be null, but default to an empty {@link Bundle}.
+ *
+ * @throws IllegalArgumentException if the builder has a mandatory unset field.
+ */
+ public SuggestedDeviceInfo build() {
+ if (mDeviceDisplayName == null) {
+ throw new IllegalArgumentException("Device display name cannot be null");
+ }
+
+ if (mRouteId == null) {
+ throw new IllegalArgumentException("Route ID cannot be null.");
+ }
+
+ if (mType == null) {
+ throw new IllegalArgumentException("Device type cannot be null.");
+ }
+
+ if (mExtras == null) {
+ throw new IllegalArgumentException("Extras cannot be null.");
+ }
+
+ return new SuggestedDeviceInfo(this);
+ }
+
+ /**
+ * Sets the {@link #getDeviceDisplayName() device display name}.
+ *
+ * @throws IllegalArgumentException if the name is null or empty.
+ */
+ public Builder setDeviceDisplayName(@NonNull String deviceDisplayName) {
+ if (TextUtils.isEmpty(deviceDisplayName)) {
+ throw new IllegalArgumentException("Device display name cannot be null");
+ }
+ mDeviceDisplayName = deviceDisplayName;
+ return this;
+ }
+
+ /**
+ * Sets the {@link #getRouteId() route id}.
+ *
+ * @throws IllegalArgumentException if the route id is null or empty.
+ */
+ public Builder setRouteId(@NonNull String routeId) {
+ if (TextUtils.isEmpty(routeId)) {
+ throw new IllegalArgumentException("Device display name cannot be null");
+ }
+ mRouteId = routeId;
+ return this;
+ }
+
+ /** Sets the {@link #getType() device type}. */
+ public Builder setType(int type) {
+ mType = type;
+ return this;
+ }
+
+ /**
+ * Sets the {@link #getExtras() extras}.
+ *
+ * <p>The default value is an empty {@link Bundle}.
+ *
+ * <p>Do not mutate the given {@link Bundle} after passing it to this method. You can use
+ * {@link Bundle#deepCopy()} to keep a mutable copy.
+ *
+ * @throws NullPointerException if the extras are null.
+ */
+ public Builder setExtras(@NonNull Bundle extras) {
+ mExtras = Objects.requireNonNull(extras, "extras must not be null");
+ return this;
+ }
+ }
+}
diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig
index 0deed3982d9b..e39a0aa8717e 100644
--- a/media/java/android/media/flags/media_better_together.aconfig
+++ b/media/java/android/media/flags/media_better_together.aconfig
@@ -62,6 +62,14 @@ flag {
}
flag {
+ name: "enable_suggested_device_api"
+ is_exported: true
+ namespace: "media_better_together"
+ description: "Enables the API allowing proxy routers to suggest routes."
+ bug: "393216553"
+}
+
+flag {
name: "enable_full_scan_with_media_content_control"
namespace: "media_better_together"
description: "Allows holders of the MEDIA_CONTENT_CONTROL permission to scan for routes while not in the foreground."
diff --git a/media/java/android/media/quality/Android.bp b/media/java/android/media/quality/Android.bp
new file mode 100644
index 000000000000..080d5266ccb7
--- /dev/null
+++ b/media/java/android/media/quality/Android.bp
@@ -0,0 +1,39 @@
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+filegroup {
+ name: "framework-media-quality-sources-aidl",
+ srcs: [
+ "aidl/android/media/quality/*.aidl",
+ ],
+ path: "aidl",
+}
+
+aidl_interface {
+ name: "media_quality_aidl_interface",
+ unstable: true,
+ local_include_dir: "aidl",
+ backend: {
+ java: {
+ enabled: true,
+ },
+ cpp: {
+ enabled: false,
+ },
+ ndk: {
+ enabled: false,
+ },
+ rust: {
+ enabled: false,
+ },
+ },
+ srcs: [
+ ":framework-media-quality-sources-aidl",
+ ],
+}
diff --git a/media/java/android/media/quality/MediaQualityContract.java b/media/java/android/media/quality/MediaQualityContract.java
index e4de3e4420fe..fccdba8e727f 100644
--- a/media/java/android/media/quality/MediaQualityContract.java
+++ b/media/java/android/media/quality/MediaQualityContract.java
@@ -82,7 +82,7 @@ public class MediaQualityContract {
String PARAMETER_NAME = "_name";
String PARAMETER_PACKAGE = "_package";
String PARAMETER_INPUT_ID = "_input_id";
-
+ String VENDOR_PARAMETERS = "_vendor_parameters";
}
/**
diff --git a/media/java/android/media/quality/MediaQualityManager.java b/media/java/android/media/quality/MediaQualityManager.java
index 0d6d32a22dae..bfd01380a2ee 100644
--- a/media/java/android/media/quality/MediaQualityManager.java
+++ b/media/java/android/media/quality/MediaQualityManager.java
@@ -274,9 +274,9 @@ public final class MediaQualityManager {
@NonNull String name,
@Nullable ProfileQueryParams options) {
try {
- Bundle optionsBundle = options == null
- ? ProfileQueryParams.DEFAULT.toBundle() : options.toBundle();
- return mService.getPictureProfile(type, name, optionsBundle, mUserHandle);
+ boolean includeParams = options == null || options.mParametersIncluded;
+ return mService.getPictureProfile(
+ type, name, includeParams, mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -299,10 +299,9 @@ public final class MediaQualityManager {
public List<PictureProfile> getPictureProfilesByPackage(
@NonNull String packageName, @Nullable ProfileQueryParams options) {
try {
- Bundle optionsBundle = options == null
- ? ProfileQueryParams.DEFAULT.toBundle() : options.toBundle();
+ boolean includeParams = options == null || options.mParametersIncluded;
return mService.getPictureProfilesByPackage(
- packageName, optionsBundle, mUserHandle);
+ packageName, includeParams, mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -321,9 +320,8 @@ public final class MediaQualityManager {
@NonNull
public List<PictureProfile> getAvailablePictureProfiles(@Nullable ProfileQueryParams options) {
try {
- Bundle optionsBundle = options == null
- ? ProfileQueryParams.DEFAULT.toBundle() : options.toBundle();
- return mService.getAvailablePictureProfiles(optionsBundle, mUserHandle);
+ boolean includeParams = options == null || options.mParametersIncluded;
+ return mService.getAvailablePictureProfiles(includeParams, mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -344,7 +342,7 @@ public final class MediaQualityManager {
@RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE)
public boolean setDefaultPictureProfile(@Nullable String pictureProfileId) {
try {
- return mService.setDefaultPictureProfile(pictureProfileId, mUserHandle);
+ return mService.setDefaultPictureProfile(pictureProfileId, mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -361,7 +359,7 @@ public final class MediaQualityManager {
@RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE)
public List<String> getPictureProfilePackageNames() {
try {
- return mService.getPictureProfilePackageNames(mUserHandle);
+ return mService.getPictureProfilePackageNames(mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -373,7 +371,7 @@ public final class MediaQualityManager {
*/
public List<PictureProfileHandle> getPictureProfileHandle(String[] id) {
try {
- return mService.getPictureProfileHandle(id, mUserHandle);
+ return mService.getPictureProfileHandle(id, mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -385,7 +383,7 @@ public final class MediaQualityManager {
*/
public List<SoundProfileHandle> getSoundProfileHandle(String[] id) {
try {
- return mService.getSoundProfileHandle(id, mUserHandle);
+ return mService.getSoundProfileHandle(id, mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -401,7 +399,7 @@ public final class MediaQualityManager {
*/
public void createPictureProfile(@NonNull PictureProfile pp) {
try {
- mService.createPictureProfile(pp, mUserHandle);
+ mService.createPictureProfile(pp, mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -416,7 +414,7 @@ public final class MediaQualityManager {
*/
public void updatePictureProfile(@NonNull String profileId, @NonNull PictureProfile pp) {
try {
- mService.updatePictureProfile(profileId, pp, mUserHandle);
+ mService.updatePictureProfile(profileId, pp, mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -430,7 +428,7 @@ public final class MediaQualityManager {
*/
public void removePictureProfile(@NonNull String profileId) {
try {
- mService.removePictureProfile(profileId, mUserHandle);
+ mService.removePictureProfile(profileId, mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -484,9 +482,8 @@ public final class MediaQualityManager {
@NonNull String name,
@Nullable ProfileQueryParams options) {
try {
- Bundle optionsBundle = options == null
- ? ProfileQueryParams.DEFAULT.toBundle() : options.toBundle();
- return mService.getSoundProfile(type, name, optionsBundle, mUserHandle);
+ boolean includeParams = options == null || options.mParametersIncluded;
+ return mService.getSoundProfile(type, name, includeParams, mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -510,9 +507,9 @@ public final class MediaQualityManager {
public List<SoundProfile> getSoundProfilesByPackage(
@NonNull String packageName, @Nullable ProfileQueryParams options) {
try {
- Bundle optionsBundle = options == null
- ? ProfileQueryParams.DEFAULT.toBundle() : options.toBundle();
- return mService.getSoundProfilesByPackage(packageName, optionsBundle, mUserHandle);
+ boolean includeParams = options == null || options.mParametersIncluded;
+ return mService.getSoundProfilesByPackage(
+ packageName, includeParams, mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -531,9 +528,8 @@ public final class MediaQualityManager {
@NonNull
public List<SoundProfile> getAvailableSoundProfiles(@Nullable ProfileQueryParams options) {
try {
- Bundle optionsBundle = options == null
- ? ProfileQueryParams.DEFAULT.toBundle() : options.toBundle();
- return mService.getAvailableSoundProfiles(optionsBundle, mUserHandle);
+ boolean includeParams = options == null || options.mParametersIncluded;
+ return mService.getAvailableSoundProfiles(includeParams, mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -554,7 +550,7 @@ public final class MediaQualityManager {
@RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE)
public boolean setDefaultSoundProfile(@Nullable String soundProfileId) {
try {
- return mService.setDefaultSoundProfile(soundProfileId, mUserHandle);
+ return mService.setDefaultSoundProfile(soundProfileId, mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -572,7 +568,7 @@ public final class MediaQualityManager {
@RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE)
public List<String> getSoundProfilePackageNames() {
try {
- return mService.getSoundProfilePackageNames(mUserHandle);
+ return mService.getSoundProfilePackageNames(mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -589,7 +585,7 @@ public final class MediaQualityManager {
*/
public void createSoundProfile(@NonNull SoundProfile sp) {
try {
- mService.createSoundProfile(sp, mUserHandle);
+ mService.createSoundProfile(sp, mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -604,7 +600,7 @@ public final class MediaQualityManager {
*/
public void updateSoundProfile(@NonNull String profileId, @NonNull SoundProfile sp) {
try {
- mService.updateSoundProfile(profileId, sp, mUserHandle);
+ mService.updateSoundProfile(profileId, sp, mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -618,7 +614,7 @@ public final class MediaQualityManager {
*/
public void removeSoundProfile(@NonNull String profileId) {
try {
- mService.removeSoundProfile(profileId, mUserHandle);
+ mService.removeSoundProfile(profileId, mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -636,7 +632,7 @@ public final class MediaQualityManager {
@NonNull
public List<ParameterCapability> getParameterCapabilities(@NonNull List<String> names) {
try {
- return mService.getParameterCapabilities(names, mUserHandle);
+ return mService.getParameterCapabilities(names, mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -654,7 +650,7 @@ public final class MediaQualityManager {
@NonNull
public List<String> getPictureProfileAllowList() {
try {
- return mService.getPictureProfileAllowList(mUserHandle);
+ return mService.getPictureProfileAllowList(mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -668,7 +664,7 @@ public final class MediaQualityManager {
@RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE)
public void setPictureProfileAllowList(@NonNull List<String> packageNames) {
try {
- mService.setPictureProfileAllowList(packageNames, mUserHandle);
+ mService.setPictureProfileAllowList(packageNames, mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -686,7 +682,7 @@ public final class MediaQualityManager {
@NonNull
public List<String> getSoundProfileAllowList() {
try {
- return mService.getSoundProfileAllowList(mUserHandle);
+ return mService.getSoundProfileAllowList(mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -700,7 +696,7 @@ public final class MediaQualityManager {
@RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE)
public void setSoundProfileAllowList(@NonNull List<String> packageNames) {
try {
- mService.setSoundProfileAllowList(packageNames, mUserHandle);
+ mService.setSoundProfileAllowList(packageNames, mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -712,7 +708,7 @@ public final class MediaQualityManager {
*/
public boolean isSupported() {
try {
- return mService.isSupported(mUserHandle);
+ return mService.isSupported(mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -730,7 +726,7 @@ public final class MediaQualityManager {
@RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE)
public void setAutoPictureQualityEnabled(boolean enabled) {
try {
- mService.setAutoPictureQualityEnabled(enabled, mUserHandle);
+ mService.setAutoPictureQualityEnabled(enabled, mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -741,7 +737,7 @@ public final class MediaQualityManager {
*/
public boolean isAutoPictureQualityEnabled() {
try {
- return mService.isAutoPictureQualityEnabled(mUserHandle);
+ return mService.isAutoPictureQualityEnabled(mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -758,7 +754,7 @@ public final class MediaQualityManager {
@RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE)
public void setSuperResolutionEnabled(boolean enabled) {
try {
- mService.setSuperResolutionEnabled(enabled, mUserHandle);
+ mService.setSuperResolutionEnabled(enabled, mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -769,7 +765,7 @@ public final class MediaQualityManager {
*/
public boolean isSuperResolutionEnabled() {
try {
- return mService.isSuperResolutionEnabled(mUserHandle);
+ return mService.isSuperResolutionEnabled(mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -787,7 +783,7 @@ public final class MediaQualityManager {
@RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE)
public void setAutoSoundQualityEnabled(boolean enabled) {
try {
- mService.setAutoSoundQualityEnabled(enabled, mUserHandle);
+ mService.setAutoSoundQualityEnabled(enabled, mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -798,7 +794,7 @@ public final class MediaQualityManager {
*/
public boolean isAutoSoundQualityEnabled() {
try {
- return mService.isAutoSoundQualityEnabled(mUserHandle);
+ return mService.isAutoSoundQualityEnabled(mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -847,7 +843,7 @@ public final class MediaQualityManager {
@NonNull AmbientBacklightSettings settings) {
Preconditions.checkNotNull(settings);
try {
- mService.setAmbientBacklightSettings(settings, mUserHandle);
+ mService.setAmbientBacklightSettings(settings, mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -858,7 +854,7 @@ public final class MediaQualityManager {
*/
public boolean isAmbientBacklightEnabled() {
try {
- return mService.isAmbientBacklightEnabled(mUserHandle);
+ return mService.isAmbientBacklightEnabled(mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -872,7 +868,7 @@ public final class MediaQualityManager {
@RequiresPermission(android.Manifest.permission.READ_COLOR_ZONES)
public void setAmbientBacklightEnabled(boolean enabled) {
try {
- mService.setAmbientBacklightEnabled(enabled, mUserHandle);
+ mService.setAmbientBacklightEnabled(enabled, mUserHandle.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/media/java/android/media/quality/SoundProfileHandle.java b/media/java/android/media/quality/SoundProfileHandle.java
deleted file mode 100644
index edb546efdaf3..000000000000
--- a/media/java/android/media/quality/SoundProfileHandle.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.media.quality;
-
-import android.annotation.NonNull;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-/**
- * A type-safe handle to a sound profile.
- *
- * @hide
- */
-public final class SoundProfileHandle implements Parcelable {
- public static final @NonNull SoundProfileHandle NONE = new SoundProfileHandle(-1000);
-
- private final long mId;
-
- /** @hide */
- public SoundProfileHandle(long id) {
- mId = id;
- }
-
- /** @hide */
- public long getId() {
- return mId;
- }
-
- /** @hide */
- @Override
- public void writeToParcel(@NonNull Parcel dest, int flags) {
- dest.writeLong(mId);
- }
-
- /** @hide */
- @Override
- public int describeContents() {
- return 0;
- }
-
- /** @hide */
- public static final @NonNull Creator<SoundProfileHandle> CREATOR =
- new Creator<SoundProfileHandle>() {
- @Override
- public SoundProfileHandle createFromParcel(Parcel in) {
- return new SoundProfileHandle(in);
- }
-
- @Override
- public SoundProfileHandle[] newArray(int size) {
- return new SoundProfileHandle[size];
- }
- };
-
- private SoundProfileHandle(@NonNull Parcel in) {
- mId = in.readLong();
- }
-}
diff --git a/media/java/android/media/quality/ActiveProcessingPicture.aidl b/media/java/android/media/quality/aidl/android/media/quality/ActiveProcessingPicture.aidl
index 2851306f6e4d..2851306f6e4d 100644
--- a/media/java/android/media/quality/ActiveProcessingPicture.aidl
+++ b/media/java/android/media/quality/aidl/android/media/quality/ActiveProcessingPicture.aidl
diff --git a/media/java/android/media/quality/AmbientBacklightEvent.aidl b/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightEvent.aidl
index 174cd461e846..174cd461e846 100644
--- a/media/java/android/media/quality/AmbientBacklightEvent.aidl
+++ b/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightEvent.aidl
diff --git a/media/java/android/media/quality/AmbientBacklightMetadata.aidl b/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightMetadata.aidl
index b95a474fbf90..b95a474fbf90 100644
--- a/media/java/android/media/quality/AmbientBacklightMetadata.aidl
+++ b/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightMetadata.aidl
diff --git a/media/java/android/media/quality/AmbientBacklightSettings.aidl b/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightSettings.aidl
index e2cdd03194cd..e2cdd03194cd 100644
--- a/media/java/android/media/quality/AmbientBacklightSettings.aidl
+++ b/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightSettings.aidl
diff --git a/media/java/android/media/quality/IAmbientBacklightCallback.aidl b/media/java/android/media/quality/aidl/android/media/quality/IAmbientBacklightCallback.aidl
index 159f5b7b5e71..159f5b7b5e71 100644
--- a/media/java/android/media/quality/IAmbientBacklightCallback.aidl
+++ b/media/java/android/media/quality/aidl/android/media/quality/IAmbientBacklightCallback.aidl
diff --git a/media/java/android/media/quality/IMediaQualityManager.aidl b/media/java/android/media/quality/aidl/android/media/quality/IMediaQualityManager.aidl
index 6e9fa1dcf93d..0191ea786de0 100644
--- a/media/java/android/media/quality/IMediaQualityManager.aidl
+++ b/media/java/android/media/quality/aidl/android/media/quality/IMediaQualityManager.aidl
@@ -25,57 +25,57 @@ import android.media.quality.PictureProfileHandle;
import android.media.quality.PictureProfile;
import android.media.quality.SoundProfileHandle;
import android.media.quality.SoundProfile;
-import android.os.Bundle;
-import android.os.UserHandle;
/**
* Interface for Media Quality Manager
* @hide
*/
interface IMediaQualityManager {
- PictureProfile createPictureProfile(in PictureProfile pp, in UserHandle user);
- void updatePictureProfile(in String id, in PictureProfile pp, in UserHandle user);
- void removePictureProfile(in String id, in UserHandle user);
- boolean setDefaultPictureProfile(in String id, in UserHandle user);
+ // TODO: use UserHandle
+ PictureProfile createPictureProfile(in PictureProfile pp, int userId);
+ void updatePictureProfile(in String id, in PictureProfile pp, int userId);
+ void removePictureProfile(in String id, int userId);
+ boolean setDefaultPictureProfile(in String id, int userId);
+ // TODO: use Bundle for includeParams
PictureProfile getPictureProfile(
- in int type, in String name, in Bundle options, in UserHandle user);
+ in int type, in String name, in boolean includeParams, int userId);
List<PictureProfile> getPictureProfilesByPackage(
- in String packageName, in Bundle options, in UserHandle user);
- List<PictureProfile> getAvailablePictureProfiles(in Bundle options, in UserHandle user);
- List<String> getPictureProfilePackageNames(in UserHandle user);
- List<String> getPictureProfileAllowList(in UserHandle user);
- void setPictureProfileAllowList(in List<String> packages, in UserHandle user);
- List<PictureProfileHandle> getPictureProfileHandle(in String[] id, in UserHandle user);
+ in String packageName, in boolean includeParams, int userId);
+ List<PictureProfile> getAvailablePictureProfiles(in boolean includeParams, int userId);
+ List<String> getPictureProfilePackageNames(int userId);
+ List<String> getPictureProfileAllowList(int userId);
+ void setPictureProfileAllowList(in List<String> packages, int userId);
+ List<PictureProfileHandle> getPictureProfileHandle(in String[] id, int userId);
- SoundProfile createSoundProfile(in SoundProfile pp, in UserHandle user);
- void updateSoundProfile(in String id, in SoundProfile pp, in UserHandle user);
- void removeSoundProfile(in String id, in UserHandle user);
- boolean setDefaultSoundProfile(in String id, in UserHandle user);
+ SoundProfile createSoundProfile(in SoundProfile pp, int userId);
+ void updateSoundProfile(in String id, in SoundProfile pp, int userId);
+ void removeSoundProfile(in String id, int userId);
+ boolean setDefaultSoundProfile(in String id, int userId);
SoundProfile getSoundProfile(
- in int type, in String name, in Bundle options, in UserHandle user);
+ in int type, in String name, in boolean includeParams, int userId);
List<SoundProfile> getSoundProfilesByPackage(
- in String packageName, in Bundle options, in UserHandle user);
- List<SoundProfile> getAvailableSoundProfiles(in Bundle options, in UserHandle user);
- List<String> getSoundProfilePackageNames(in UserHandle user);
- List<String> getSoundProfileAllowList(in UserHandle user);
- void setSoundProfileAllowList(in List<String> packages, in UserHandle user);
- List<SoundProfileHandle> getSoundProfileHandle(in String[] id, in UserHandle user);
+ in String packageName, in boolean includeParams, int userId);
+ List<SoundProfile> getAvailableSoundProfiles(in boolean includeParams, int userId);
+ List<String> getSoundProfilePackageNames(int userId);
+ List<String> getSoundProfileAllowList(int userId);
+ void setSoundProfileAllowList(in List<String> packages, int userId);
+ List<SoundProfileHandle> getSoundProfileHandle(in String[] id, int userId);
void registerPictureProfileCallback(in IPictureProfileCallback cb);
void registerSoundProfileCallback(in ISoundProfileCallback cb);
void registerAmbientBacklightCallback(in IAmbientBacklightCallback cb);
- List<ParameterCapability> getParameterCapabilities(in List<String> names, in UserHandle user);
+ List<ParameterCapability> getParameterCapabilities(in List<String> names, int userId);
- boolean isSupported(in UserHandle user);
- void setAutoPictureQualityEnabled(in boolean enabled, in UserHandle user);
- boolean isAutoPictureQualityEnabled(in UserHandle user);
- void setSuperResolutionEnabled(in boolean enabled, in UserHandle user);
- boolean isSuperResolutionEnabled(in UserHandle user);
- void setAutoSoundQualityEnabled(in boolean enabled, in UserHandle user);
- boolean isAutoSoundQualityEnabled(in UserHandle user);
+ boolean isSupported(int userId);
+ void setAutoPictureQualityEnabled(in boolean enabled, int userId);
+ boolean isAutoPictureQualityEnabled(int userId);
+ void setSuperResolutionEnabled(in boolean enabled, int userId);
+ boolean isSuperResolutionEnabled(int userId);
+ void setAutoSoundQualityEnabled(in boolean enabled, int userId);
+ boolean isAutoSoundQualityEnabled(int userId);
- void setAmbientBacklightSettings(in AmbientBacklightSettings settings, in UserHandle user);
- void setAmbientBacklightEnabled(in boolean enabled, in UserHandle user);
- boolean isAmbientBacklightEnabled(in UserHandle user);
+ void setAmbientBacklightSettings(in AmbientBacklightSettings settings, int userId);
+ void setAmbientBacklightEnabled(in boolean enabled, int userId);
+ boolean isAmbientBacklightEnabled(int userId);
}
diff --git a/media/java/android/media/quality/IPictureProfileCallback.aidl b/media/java/android/media/quality/aidl/android/media/quality/IPictureProfileCallback.aidl
index eed77f695416..eed77f695416 100644
--- a/media/java/android/media/quality/IPictureProfileCallback.aidl
+++ b/media/java/android/media/quality/aidl/android/media/quality/IPictureProfileCallback.aidl
diff --git a/media/java/android/media/quality/ISoundProfileCallback.aidl b/media/java/android/media/quality/aidl/android/media/quality/ISoundProfileCallback.aidl
index 3871fb212259..3871fb212259 100644
--- a/media/java/android/media/quality/ISoundProfileCallback.aidl
+++ b/media/java/android/media/quality/aidl/android/media/quality/ISoundProfileCallback.aidl
diff --git a/media/java/android/media/quality/ParameterCapability.aidl b/media/java/android/media/quality/aidl/android/media/quality/ParameterCapability.aidl
index eb2ac97916f3..eb2ac97916f3 100644
--- a/media/java/android/media/quality/ParameterCapability.aidl
+++ b/media/java/android/media/quality/aidl/android/media/quality/ParameterCapability.aidl
diff --git a/media/java/android/media/quality/PictureProfile.aidl b/media/java/android/media/quality/aidl/android/media/quality/PictureProfile.aidl
index 41d018b12f33..41d018b12f33 100644
--- a/media/java/android/media/quality/PictureProfile.aidl
+++ b/media/java/android/media/quality/aidl/android/media/quality/PictureProfile.aidl
diff --git a/media/java/android/media/quality/PictureProfileHandle.aidl b/media/java/android/media/quality/aidl/android/media/quality/PictureProfileHandle.aidl
index 5d14631dbb73..5d14631dbb73 100644
--- a/media/java/android/media/quality/PictureProfileHandle.aidl
+++ b/media/java/android/media/quality/aidl/android/media/quality/PictureProfileHandle.aidl
diff --git a/media/java/android/media/quality/SoundProfile.aidl b/media/java/android/media/quality/aidl/android/media/quality/SoundProfile.aidl
index e79fcaac97be..e79fcaac97be 100644
--- a/media/java/android/media/quality/SoundProfile.aidl
+++ b/media/java/android/media/quality/aidl/android/media/quality/SoundProfile.aidl
diff --git a/media/java/android/media/quality/SoundProfileHandle.aidl b/media/java/android/media/quality/aidl/android/media/quality/SoundProfileHandle.aidl
index 6b8161c8cc43..ea26b19d84d7 100644
--- a/media/java/android/media/quality/SoundProfileHandle.aidl
+++ b/media/java/android/media/quality/aidl/android/media/quality/SoundProfileHandle.aidl
@@ -16,4 +16,7 @@
package android.media.quality;
-parcelable SoundProfileHandle;
+// TODO: add SoundProfileHandle.java
+parcelable SoundProfileHandle {
+ long id;
+}
diff --git a/media/java/android/media/tv/extension/scan/IHDPlusInfo.aidl b/media/java/android/media/tv/extension/scan/IHDPlusInfo.aidl
index cdf6e23f4b47..40848fe6f875 100644
--- a/media/java/android/media/tv/extension/scan/IHDPlusInfo.aidl
+++ b/media/java/android/media/tv/extension/scan/IHDPlusInfo.aidl
@@ -21,5 +21,5 @@ package android.media.tv.extension.scan;
*/
interface IHDPlusInfo {
// Specifying a HDPlusInfo and start a network scan.
- int setHDPlusInfo(String isBlindScanContinue, String isHDMode);
+ int setHDPlusInfo(boolean isBlindScanContinue, boolean isHDMode);
}
diff --git a/media/java/android/media/tv/extension/scan/IScanListener.aidl b/media/java/android/media/tv/extension/scan/IScanListener.aidl
index 2c4807f97c58..79810a7b035d 100644
--- a/media/java/android/media/tv/extension/scan/IScanListener.aidl
+++ b/media/java/android/media/tv/extension/scan/IScanListener.aidl
@@ -27,7 +27,7 @@ oneway interface IScanListener {
// notify the scan progress.
void onScanProgress(String scanProgress, in Bundle scanProgressInfo);
// notify the scan completion.
- void onScanCompleted(int scanResult);
+ void onScanCompleted(int scanResult, in Bundle optionScanInfo);
// notify that the temporaily held channel list is stored.
void onStoreCompleted(int storeResult);
}
diff --git a/media/java/android/media/tv/extension/scan/IScanSession.aidl b/media/java/android/media/tv/extension/scan/IScanSession.aidl
index d42eca1342b5..f53b09661ba6 100644
--- a/media/java/android/media/tv/extension/scan/IScanSession.aidl
+++ b/media/java/android/media/tv/extension/scan/IScanSession.aidl
@@ -24,7 +24,7 @@ import android.os.Bundle;
interface IScanSession {
// Start a service scan.
int startScan(int broadcastType, String countryCode, String operator, in int[] frequency,
- String scanType, String languageCode);
+ String scanType, String languageCode, in Bundle optionalScanParams);
// Reset the scan information held in TIS.
int resetScan();
// Cancel scan.
diff --git a/media/java/android/media/tv/extension/servicedb/IServiceListEdit.aidl b/media/java/android/media/tv/extension/servicedb/IServiceListEdit.aidl
index 1b1577ffc015..0e9ac5f04e5d 100644
--- a/media/java/android/media/tv/extension/servicedb/IServiceListEdit.aidl
+++ b/media/java/android/media/tv/extension/servicedb/IServiceListEdit.aidl
@@ -78,4 +78,11 @@ interface IServiceListEdit {
int addPredefinedChannelList(String serviceListId, in Bundle[] predefinedListBundle);
// Add predefined satellite info of Hotbird 13E in scan two satellite scene EU region.
int addPredefinedSatInfo(String serviceListId, in Bundle predefinedSatInfoBundle);
+
+ // Get the logo URI for a specific service - DVB-I only.
+ String getServiceLogoUri(int serviceRecordId);
+ // Get the installed service list information for a specific channel list id - DVB-I only.
+ Bundle getInstalledServiceListInfo(String channelListId);
+ // Get all installed service list information - DVB-I only.
+ Bundle[] getAllInstalledServiceListInfo();
}
diff --git a/media/java/android/media/tv/extension/servicedb/IServiceListImportListener.aidl b/media/java/android/media/tv/extension/servicedb/IServiceListImportListener.aidl
index abd8320df11d..fced45b11d3b 100644
--- a/media/java/android/media/tv/extension/servicedb/IServiceListImportListener.aidl
+++ b/media/java/android/media/tv/extension/servicedb/IServiceListImportListener.aidl
@@ -16,10 +16,12 @@
package android.media.tv.extension.servicedb;
+import android.os.Bundle;
+
/**
* @hide
*/
interface IServiceListImportListener {
void onImported(int importResult);
- void onPreloaded(int preloadResult);
+ void onPreloaded(int preloadResult, in Bundle serviceListInfo);
} \ No newline at end of file
diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp
index 1e6a7b7f2810..45b746d254e1 100644
--- a/native/android/performance_hint.cpp
+++ b/native/android/performance_hint.cpp
@@ -971,7 +971,7 @@ void FMQWrapper::writeBuffer<HalChannelMessageContents::workDuration>(hal::WorkD
.timeStampNanos = (i == count - 1) ? now : message.timeStampNanos,
.data = HalChannelMessageContents::make<HalChannelMessageContents::workDuration,
hal::WorkDurationFixedV1>({
- .durationNanos = message.cpuDurationNanos,
+ .durationNanos = message.durationNanos,
.workPeriodStartTimestampNanos = message.workPeriodStartTimestampNanos,
.cpuDurationNanos = message.cpuDurationNanos,
.gpuDurationNanos = message.gpuDurationNanos,
diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
index e818a603c5b4..bf739620bc99 100644
--- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
+++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
@@ -138,8 +138,12 @@ public class IllustrationPreference extends Preference implements GroupSectionDi
ImageView backgroundViewTablet =
(ImageView) holder.findViewById(R.id.background_view_tablet);
- backgroundView.setVisibility(mIsTablet ? View.GONE : View.VISIBLE);
- backgroundViewTablet.setVisibility(mIsTablet ? View.VISIBLE : View.GONE);
+ if (backgroundView != null) {
+ backgroundView.setVisibility(mIsTablet ? View.GONE : View.VISIBLE);
+ }
+ if (backgroundViewTablet != null) {
+ backgroundViewTablet.setVisibility(mIsTablet ? View.VISIBLE : View.GONE);
+ }
if (mIsTablet) {
backgroundView = backgroundViewTablet;
}
@@ -183,7 +187,9 @@ public class IllustrationPreference extends Preference implements GroupSectionDi
if (mLottieDynamicColor) {
LottieColorUtils.applyDynamicColors(getContext(), illustrationView);
}
- LottieColorUtils.applyMaterialColor(getContext(), illustrationView);
+ if (SettingsThemeHelper.isExpressiveTheme(getContext())) {
+ LottieColorUtils.applyMaterialColor(getContext(), illustrationView);
+ }
if (mOnBindListener != null) {
mOnBindListener.onBind(illustrationView);
@@ -443,6 +449,10 @@ public class IllustrationPreference extends Preference implements GroupSectionDi
illustrationView.setMaxWidth((int) (restrictedMaxHeight * aspectRatio));
}
+ public boolean isAnimatable() {
+ return mIsAnimatable;
+ }
+
private void startAnimation(Drawable drawable) {
if (!(drawable instanceof Animatable)) {
return;
diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java
index 4421424c0e39..e59cc81d3ba8 100644
--- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java
+++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java
@@ -157,10 +157,6 @@ public class LottieColorUtils {
/** Applies material colors. */
public static void applyMaterialColor(@NonNull Context context,
@NonNull LottieAnimationView lottieAnimationView) {
- if (!SettingsThemeHelper.isExpressiveTheme(context)) {
- return;
- }
-
for (String key : MATERIAL_COLOR_MAP.keySet()) {
final int color = context.getColor(MATERIAL_COLOR_MAP.get(key));
lottieAnimationView.addValueCallback(
diff --git a/packages/SettingsLib/RestrictedLockUtils/res/values/strings.xml b/packages/SettingsLib/RestrictedLockUtils/res/values/strings.xml
index 75809730a514..2ffdc930ccea 100644
--- a/packages/SettingsLib/RestrictedLockUtils/res/values/strings.xml
+++ b/packages/SettingsLib/RestrictedLockUtils/res/values/strings.xml
@@ -21,8 +21,4 @@
<string name="enabled_by_admin">Enabled by admin</string>
<!-- Summary for switch preference to denote it is switched off by an admin [CHAR LIMIT=50] -->
<string name="disabled_by_admin">Disabled by admin</string>
- <!-- Summary for switch preference to denote it is switched on by Advanced protection [CHAR LIMIT=50] -->
- <string name="enabled_by_advanced_protection">Enabled by Advanced Protection</string>
- <!-- Summary for switch preference to denote it is switched off by Advanced protection [CHAR LIMIT=50] -->
- <string name="disabled_by_advanced_protection">Disabled by Advanced Protection</string>
</resources>
diff --git a/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java b/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java
index 0f6a2a082e0c..1170f1e7c695 100644
--- a/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java
+++ b/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java
@@ -19,6 +19,7 @@ package com.android.settingslib.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
import android.widget.AdapterView;
import android.widget.Spinner;
@@ -110,7 +111,6 @@ public class SettingsSpinnerPreference extends Preference
notifyChanged();
}
-
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
@@ -119,6 +119,18 @@ public class SettingsSpinnerPreference extends Preference
spinner.setSelection(mPosition);
spinner.setOnItemSelectedListener(mOnSelectedListener);
spinner.setLongClickable(false);
+ spinner.setAccessibilityDelegate(
+ new View.AccessibilityDelegate() {
+ @Override
+ public void sendAccessibilityEvent(View host, int eventType) {
+ if (eventType == AccessibilityEvent.TYPE_VIEW_SELECTED) {
+ // Ignore the INTERRUPT events TYPE_VIEW_SELECTED or Talkback will speak
+ // for it while fragment updating.
+ return;
+ }
+ super.sendAccessibilityEvent(host, eventType);
+ }
+ });
if (mShouldPerformClick) {
mShouldPerformClick = false;
// To show dropdown view.
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedMode.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedMode.kt
index a8483308556d..6f37f0cc5799 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedMode.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedMode.kt
@@ -46,7 +46,7 @@ internal data class BlockedByAdminImpl(
) : BlockedByAdmin {
override fun getSummary(checked: Boolean?) = when (checked) {
true -> enterpriseRepository.getAdminSummaryString(
- advancedProtectionStringId = R.string.enabled_by_advanced_protection,
+ advancedProtectionStringId = com.android.settingslib.R.string.enabled,
updatableStringId = Settings.ENABLED_BY_ADMIN_SWITCH_SUMMARY,
resId = R.string.enabled_by_admin,
enforcedAdmin = enforcedAdmin,
@@ -54,7 +54,7 @@ internal data class BlockedByAdminImpl(
)
false -> enterpriseRepository.getAdminSummaryString(
- advancedProtectionStringId = R.string.disabled_by_advanced_protection,
+ advancedProtectionStringId = com.android.settingslib.R.string.disabled,
updatableStringId = Settings.DISABLED_BY_ADMIN_SWITCH_SUMMARY,
resId = R.string.disabled_by_admin,
enforcedAdmin = enforcedAdmin,
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSystemInteger.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSystemInteger.kt
new file mode 100644
index 000000000000..db7a640be893
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSystemInteger.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spaprivileged.settingsprovider
+
+import android.content.ContentResolver
+import android.content.Context
+import android.provider.Settings
+import com.android.settingslib.spaprivileged.database.contentChangeFlow
+import kotlin.properties.ReadWriteProperty
+import kotlin.reflect.KProperty
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+
+fun Context.settingsSystemInteger(
+ name: String,
+ defaultValue: Int
+): ReadWriteProperty<Any?, Int> = SettingsSystemIntegerDelegate(this, name, defaultValue)
+
+fun Context.settingsSystemIntegerFlow(name: String, defaultValue: Int): Flow<Int> {
+ val value by settingsSystemInteger(name, defaultValue)
+ return contentChangeFlow(Settings.System.getUriFor(name))
+ .map { value }
+ .distinctUntilChanged()
+ .conflate()
+ .flowOn(Dispatchers.IO)
+}
+
+private class SettingsSystemIntegerDelegate(
+ context: Context,
+ private val name: String,
+ private val defaultValue: Int,
+) : ReadWriteProperty<Any?, Int> {
+
+ private val contentResolver: ContentResolver = context.contentResolver
+
+ override fun getValue(thisRef: Any?, property: KProperty<*>): Int =
+ Settings.System.getInt(contentResolver, name, defaultValue)
+
+ override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
+ Settings.System.putInt(contentResolver, name, value)
+ }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/robotests/Android.bp b/packages/SettingsLib/SpaPrivileged/tests/robotests/Android.bp
new file mode 100644
index 000000000000..e3faf73a69fb
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/tests/robotests/Android.bp
@@ -0,0 +1,59 @@
+//
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+package {
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_app {
+ name: "SpaPrivilegedRoboTestStub",
+ defaults: [
+ "SpaPrivilegedLib-defaults",
+ ],
+ platform_apis: true,
+ certificate: "platform",
+ privileged: true,
+}
+
+android_robolectric_test {
+ name: "SpaPrivilegedRoboTests",
+ srcs: [
+ ":SpaPrivilegedLib_srcs",
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+
+ defaults: [
+ "SpaPrivilegedLib-defaults",
+ ],
+
+ static_libs: [
+ "SpaLibTestUtils",
+ "androidx.test.ext.junit",
+ "androidx.test.runner",
+ ],
+
+ java_resource_dirs: [
+ "config",
+ ],
+
+ instrumentation_for: "SpaPrivilegedRoboTestStub",
+
+ test_options: {
+ timeout: 36000,
+ },
+
+ strict_mode: false,
+}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/robotests/AndroidManifest.xml b/packages/SettingsLib/SpaPrivileged/tests/robotests/AndroidManifest.xml
new file mode 100644
index 000000000000..113852d74f69
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/tests/robotests/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ coreApp="true"
+ package="com.android.settingslib.spaprivileged.settingsprovider">
+
+ <uses-permission android:name="android.permission.WRITE_SETTINGS" />
+
+ <application/>
+</manifest> \ No newline at end of file
diff --git a/packages/SettingsLib/SpaPrivileged/tests/robotests/config/robolectric.properties b/packages/SettingsLib/SpaPrivileged/tests/robotests/config/robolectric.properties
new file mode 100644
index 000000000000..95a24bde00f6
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/tests/robotests/config/robolectric.properties
@@ -0,0 +1,16 @@
+/*
+* Copyright (C) 2025 The Android Open Source Project
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+sdk=NEWEST_SDK \ No newline at end of file
diff --git a/packages/SettingsLib/SpaPrivileged/tests/robotests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSystemIntegerTest.kt b/packages/SettingsLib/SpaPrivileged/tests/robotests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSystemIntegerTest.kt
new file mode 100644
index 000000000000..67e4180a624b
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/tests/robotests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSystemIntegerTest.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settingslib.spaprivileged.settingsprovider
+
+import android.content.Context
+import android.provider.Settings
+
+import androidx.test.core.app.ApplicationProvider
+
+import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
+import com.android.settingslib.spa.testutils.toListWithTimeout
+import com.google.common.truth.Truth.assertThat
+
+import kotlinx.coroutines.async
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class SettingsSystemIntegerTest {
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ @Before
+ fun setUp() {
+ Settings.System.putString(context.contentResolver, TEST_NAME, null)
+ }
+
+ @Test
+ fun setIntValue_returnSameValueByDelegate() {
+ val settingValue = 250
+
+ Settings.System.putInt(context.contentResolver, TEST_NAME, settingValue)
+
+ val value by context.settingsSystemInteger(TEST_NAME, TEST_SETTING_DEFAULT_VALUE)
+
+ assertThat(value).isEqualTo(settingValue)
+ }
+
+ @Test
+ fun setZero_returnZeroByDelegate() {
+ val settingValue = 0
+ Settings.System.putInt(context.contentResolver, TEST_NAME, settingValue)
+
+ val value by context.settingsSystemInteger(TEST_NAME, TEST_SETTING_DEFAULT_VALUE)
+
+ assertThat(value).isEqualTo(settingValue)
+ }
+
+ @Test
+ fun setValueByDelegate_getValueFromSettings() {
+ val settingsValue = 5
+ var value by context.settingsSystemInteger(TEST_NAME, TEST_SETTING_DEFAULT_VALUE)
+
+ value = settingsValue
+
+ assertThat(Settings.System.getInt(context.contentResolver, TEST_NAME, TEST_SETTING_DEFAULT_VALUE)).isEqualTo(settingsValue)
+ }
+
+ @Test
+ fun setZeroByDelegate_getZeroFromSettings() {
+ val settingValue = 0
+ var value by context.settingsSystemInteger(TEST_NAME, TEST_SETTING_DEFAULT_VALUE)
+
+ value = settingValue
+
+ assertThat(Settings.System.getInt(context.contentResolver, TEST_NAME, TEST_SETTING_DEFAULT_VALUE)).isEqualTo(settingValue)
+ }
+
+ @Test
+ fun setValueByDelegate_returnValueFromsettingsSystemIntegerFlow() = runBlocking<Unit> {
+ val settingValue = 7
+ var value by context.settingsSystemInteger(TEST_NAME, TEST_SETTING_DEFAULT_VALUE)
+ value = settingValue
+
+ val flow = context.settingsSystemIntegerFlow(TEST_NAME, TEST_SETTING_DEFAULT_VALUE)
+
+ assertThat(flow.firstWithTimeoutOrNull()).isEqualTo(settingValue)
+ }
+
+ @Test
+ fun setValueByDelegateTwice_collectAfterValueChanged_onlyKeepLatest() = runBlocking<Unit> {
+ val firstSettingValue = 5
+ val secondSettingValue = 10
+
+ var value by context.settingsSystemInteger(TEST_NAME, TEST_SETTING_DEFAULT_VALUE)
+ value = firstSettingValue
+
+ val flow = context.settingsSystemIntegerFlow(TEST_NAME, TEST_SETTING_DEFAULT_VALUE)
+ value = secondSettingValue
+
+ assertThat(flow.firstWithTimeoutOrNull()).isEqualTo(value)
+ }
+
+ @Test
+ fun settingsSystemIntegerFlow_collectBeforeValueChanged_getBoth() = runBlocking<Unit> {
+ val firstSettingValue = 12
+ val secondSettingValue = 17
+ val delay_ms = 100L
+
+ var value by context.settingsSystemInteger(TEST_NAME, TEST_SETTING_DEFAULT_VALUE)
+ value = firstSettingValue
+
+
+ val listDeferred = async {
+ context.settingsSystemIntegerFlow(TEST_NAME, TEST_SETTING_DEFAULT_VALUE).toListWithTimeout()
+ }
+
+ delay(delay_ms)
+ value = secondSettingValue
+
+ assertThat(listDeferred.await())
+ .containsAtLeast(firstSettingValue, secondSettingValue).inOrder()
+ }
+
+ private companion object {
+ const val TEST_NAME = "test_system_integer_delegate"
+ const val TEST_SETTING_DEFAULT_VALUE = -1
+ }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedModeTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedModeTest.kt
index f3245c9085e7..189bf363420c 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedModeTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedModeTest.kt
@@ -77,8 +77,8 @@ class RestrictedModeTest {
if (RestrictedLockUtilsInternal.isPolicyEnforcedByAdvancedProtection(context,
RESTRICTION, userId)) {
return when (advancedProtectionStringId) {
- R.string.enabled_by_advanced_protection -> ENABLED_BY_ADVANCED_PROTECTION
- R.string.disabled_by_advanced_protection -> DISABLED_BY_ADVANCED_PROTECTION
+ com.android.settingslib.R.string.enabled -> ENABLED
+ com.android.settingslib.R.string.disabled -> DISABLED
else -> ""
}
}
@@ -129,7 +129,7 @@ class RestrictedModeTest {
val summary = blockedByAdmin.getSummary(true)
- assertThat(summary).isEqualTo(ENABLED_BY_ADVANCED_PROTECTION)
+ assertThat(summary).isEqualTo(ENABLED)
}
@RequiresFlagsEnabled(Flags.FLAG_AAPM_API)
@@ -148,7 +148,7 @@ class RestrictedModeTest {
val summary = blockedByAdmin.getSummary(false)
- assertThat(summary).isEqualTo(DISABLED_BY_ADVANCED_PROTECTION)
+ assertThat(summary).isEqualTo(DISABLED)
}
@RequiresFlagsEnabled(Flags.FLAG_AAPM_API)
@@ -202,7 +202,7 @@ class RestrictedModeTest {
const val ENABLED_BY_ADMIN = "Enabled by admin"
const val DISABLED_BY_ADMIN = "Disabled by admin"
- const val ENABLED_BY_ADVANCED_PROTECTION = "Enabled by advanced protection"
- const val DISABLED_BY_ADVANCED_PROTECTION = "Disabled by advanced protection"
+ const val ENABLED = "Enabled"
+ const val DISABLED = "Disabled"
}
}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPageTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPageTest.kt
index 79085af63c6d..308b285e0cfc 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPageTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPageTest.kt
@@ -175,13 +175,7 @@ class TogglePermissionAppListPageTest {
val summary = getSummary(listModel)
- assertThat(summary)
- .isEqualTo(
- context.getString(
- com.android.settingslib.widget.restricted.R.string
- .disabled_by_advanced_protection
- )
- )
+ assertThat(summary).isEqualTo(context.getString(com.android.settingslib.R.string.disabled))
}
@RequiresFlagsEnabled(Flags.FLAG_AAPM_API)
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 91ec83690722..03cb1ffbdef1 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1263,6 +1263,8 @@
<!-- [CHAR LIMIT=25] Manage applications, text telling using an application is disabled. -->
<string name="disabled">Disabled</string>
+ <!-- Summary for a settings preference indicating it is enabled [CHAR LIMIT = 30] -->
+ <string name="enabled">Enabled</string>
<!-- Summary of app trusted to install apps [CHAR LIMIT=45] -->
<string name="external_source_trusted">Allowed</string>
<!-- Summary of app not trusted to install apps [CHAR LIMIT=45] -->
diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java
index 1044750bae25..9d979019be58 100644
--- a/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java
+++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java
@@ -120,7 +120,7 @@ public class RestrictedPreferenceHelper {
final TextView summaryView = (TextView) holder.findViewById(android.R.id.summary);
if (summaryView != null) {
final CharSequence disabledText = getDisabledByAdminSummaryString();
- if (mDisabledByAdmin) {
+ if (mDisabledByAdmin && disabledText != null) {
summaryView.setText(disabledText);
} else if (mDisabledByEcm) {
summaryView.setText(getEcmTextResId());
@@ -132,10 +132,10 @@ public class RestrictedPreferenceHelper {
}
}
- private String getDisabledByAdminSummaryString() {
+ private @Nullable String getDisabledByAdminSummaryString() {
if (isRestrictionEnforcedByAdvancedProtection()) {
- return mContext.getString(com.android.settingslib.widget.restricted
- .R.string.disabled_by_advanced_protection);
+ // Advanced Protection doesn't set the summary string, it keeps the current summary.
+ return null;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
return mContext.getSystemService(DevicePolicyManager.class).getResources().getString(
@@ -321,7 +321,10 @@ public class RestrictedPreferenceHelper {
}
if (android.security.Flags.aapmApi() && !isEnabled && mDisabledByAdmin) {
- mPreference.setSummary(getDisabledByAdminSummaryString());
+ String summary = getDisabledByAdminSummaryString();
+ if (summary != null) {
+ mPreference.setSummary(summary);
+ }
}
if (!isEnabled && mDisabledByEcm) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java
index a5fa6a854e97..67c4207cb8be 100644
--- a/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java
+++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java
@@ -36,6 +36,7 @@ import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceViewHolder;
@@ -141,7 +142,7 @@ public class RestrictedSwitchPreference extends SwitchPreferenceCompat implement
final TextView additionalSummaryView = (TextView) holder.findViewById(
R.id.additional_summary);
if (additionalSummaryView != null) {
- if (isDisabledByAdmin()) {
+ if (isDisabledByAdmin() && switchSummary != null) {
additionalSummaryView.setText(switchSummary);
additionalSummaryView.setVisibility(View.VISIBLE);
} else {
@@ -151,7 +152,7 @@ public class RestrictedSwitchPreference extends SwitchPreferenceCompat implement
} else {
final TextView summaryView = (TextView) holder.findViewById(android.R.id.summary);
if (summaryView != null) {
- if (isDisabledByAdmin()) {
+ if (isDisabledByAdmin() && switchSummary != null) {
summaryView.setText(switchSummary);
summaryView.setVisibility(View.VISIBLE);
}
@@ -171,14 +172,10 @@ public class RestrictedSwitchPreference extends SwitchPreferenceCompat implement
() -> context.getString(resId));
}
- private String getRestrictedSwitchSummary() {
+ private @Nullable String getRestrictedSwitchSummary() {
if (mHelper.isRestrictionEnforcedByAdvancedProtection()) {
- final int apmResId = isChecked()
- ? com.android.settingslib.widget.restricted.R.string
- .enabled_by_advanced_protection
- : com.android.settingslib.widget.restricted.R.string
- .disabled_by_advanced_protection;
- return getContext().getString(apmResId);
+ // Advanced Protection doesn't set the summary string, it keeps the current summary.
+ return null;
}
return isChecked()
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedPreferenceHelperTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedPreferenceHelperTest.java
index dbbbd5bf8089..f9769fa61e0d 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedPreferenceHelperTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedPreferenceHelperTest.java
@@ -23,6 +23,7 @@ import static com.google.common.truth.Truth.assertWithMessage;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.atMostOnce;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -130,7 +131,7 @@ public class RestrictedPreferenceHelperTest {
@RequiresFlagsEnabled(android.security.Flags.FLAG_AAPM_API)
@Test
- public void bindPreference_disabled_byAdvancedProtection_shouldDisplayDisabledSummary() {
+ public void bindPreference_disabled_byAdvancedProtection_shouldKeepExistingSummary() {
final TextView summaryView = mock(TextView.class, RETURNS_DEEP_STUBS);
final String userRestriction = UserManager.DISALLOW_UNINSTALL_APPS;
final RestrictedLockUtils.EnforcedAdmin enforcedAdmin = new RestrictedLockUtils
@@ -143,16 +144,14 @@ public class RestrictedPreferenceHelperTest {
.thenReturn(summaryView);
when(mDevicePolicyManager.getEnforcingAdmin(UserHandle.myUserId(), userRestriction))
.thenReturn(advancedProtectionEnforcingAdmin);
- when(mContext.getString(
- com.android.settingslib.widget.restricted.R.string.disabled_by_advanced_protection))
- .thenReturn("advanced_protection");
+ summaryView.setText("existing summary");
mHelper.useAdminDisabledSummary(true);
mHelper.setDisabledByAdmin(enforcedAdmin);
mHelper.onBindViewHolder(mViewHolder);
- verify(summaryView).setText("advanced_protection");
- verify(summaryView, never()).setVisibility(View.GONE);
+ verify(summaryView, atMostOnce()).setText(any()); // To set it to existing summary
+ verify(summaryView, never()).setVisibility(View.VISIBLE);
}
@RequiresFlagsEnabled(android.security.Flags.FLAG_AAPM_API)
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java
index 3d16d6f1cd56..c387d48b33da 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java
@@ -507,4 +507,25 @@ public class MediaDeviceTest {
assertThat(mPhoneMediaDevice.getSelectionBehavior()).isEqualTo(
SELECTION_BEHAVIOR_TRANSFER);
}
+
+ @Test
+ public void getSelectionBehavior_withRouteListingPreferenceItem_returnPreferenceBehavior() {
+ mItem =
+ new RouteListingPreference.Item.Builder(DEVICE_ADDRESS_1)
+ .setSelectionBehavior(SELECTION_BEHAVIOR_GO_TO_APP)
+ .build();
+ MediaDevice castMediaDevice = new ComplexMediaDevice(mContext, mRouteInfo1, mItem);
+
+ assertThat(castMediaDevice.hasRouteListingPreferenceItem()).isTrue();
+ assertThat(castMediaDevice.getSelectionBehavior()).isEqualTo(SELECTION_BEHAVIOR_GO_TO_APP);
+ }
+
+ @Test
+ public void getSelectionBehavior_withoutRouteListingPreferenceItem_returnTransfer() {
+ MediaDevice castMediaDevice =
+ new ComplexMediaDevice(mContext, mRouteInfo1, /* item= */ null);
+
+ assertThat(castMediaDevice.hasRouteListingPreferenceItem()).isFalse();
+ assertThat(castMediaDevice.getSelectionBehavior()).isEqualTo(SELECTION_BEHAVIOR_TRANSFER);
+ }
}
diff --git a/packages/SettingsProvider/res/values/defaults.xml b/packages/SettingsProvider/res/values/defaults.xml
index dafcc729b8f1..d929b0de391a 100644
--- a/packages/SettingsProvider/res/values/defaults.xml
+++ b/packages/SettingsProvider/res/values/defaults.xml
@@ -187,6 +187,9 @@
<!-- Default state of tap to wake -->
<bool name="def_double_tap_to_wake">true</bool>
+ <!-- Default setting for double tap to sleep (Settings.Secure.DOUBLE_TAP_TO_SLEEP) -->
+ <bool name="def_double_tap_to_sleep">false</bool>
+
<!-- Default for Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT -->
<string name="def_nfc_payment_component"></string>
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index c0105298899b..a2291123e192 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -85,6 +85,7 @@ public class SecureSettings {
Settings.Secure.MOUNT_UMS_PROMPT,
Settings.Secure.MOUNT_UMS_NOTIFY_ENABLED,
Settings.Secure.DOUBLE_TAP_TO_WAKE,
+ Settings.Secure.DOUBLE_TAP_TO_SLEEP,
Settings.Secure.WAKE_GESTURE_ENABLED,
Settings.Secure.LONG_PRESS_TIMEOUT,
Settings.Secure.KEY_REPEAT_ENABLED,
@@ -229,6 +230,7 @@ public class SecureSettings {
Settings.Secure.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED,
Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED,
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_ALWAYS_ON_ENABLED,
+ Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE,
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_JOYSTICK_ENABLED,
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED,
Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_ENABLED,
@@ -266,6 +268,7 @@ public class SecureSettings {
Settings.Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED,
Settings.Secure.HUB_MODE_TUTORIAL_STATE,
Settings.Secure.GLANCEABLE_HUB_ENABLED,
+ Settings.Secure.WHEN_TO_START_GLANCEABLE_HUB,
Settings.Secure.STYLUS_BUTTONS_ENABLED,
Settings.Secure.STYLUS_HANDWRITING_ENABLED,
Settings.Secure.DEFAULT_NOTE_TASK_PROFILE,
@@ -293,5 +296,7 @@ public class SecureSettings {
Settings.Secure.FINGERPRINT_APP_ENABLED,
Settings.Secure.FINGERPRINT_KEYGUARD_ENABLED,
Settings.Secure.DUAL_SHADE,
+ Settings.Secure.BROWSER_CONTENT_FILTERS_ENABLED,
+ Settings.Secure.SEARCH_CONTENT_FILTERS_ENABLED,
};
}
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index 0ffdf53f2036..a4325344709a 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -131,6 +131,7 @@ public class SecureSettingsValidators {
VALIDATORS.put(Secure.MOUNT_UMS_PROMPT, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.MOUNT_UMS_NOTIFY_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.DOUBLE_TAP_TO_WAKE, BOOLEAN_VALIDATOR);
+ VALIDATORS.put(Secure.DOUBLE_TAP_TO_SLEEP, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.WAKE_GESTURE_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.LONG_PRESS_TIMEOUT, NON_NEGATIVE_INTEGER_VALIDATOR);
VALIDATORS.put(Secure.KEY_REPEAT_ENABLED, BOOLEAN_VALIDATOR);
@@ -323,6 +324,10 @@ public class SecureSettingsValidators {
Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ALL));
VALIDATORS.put(Secure.ACCESSIBILITY_MAGNIFICATION_FOLLOW_TYPING_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.ACCESSIBILITY_MAGNIFICATION_ALWAYS_ON_ENABLED, BOOLEAN_VALIDATOR);
+ VALIDATORS.put(Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE,
+ new InclusiveIntegerRangeValidator(
+ Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CONTINUOUS,
+ Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_EDGE));
VALIDATORS.put(Secure.ACCESSIBILITY_MAGNIFICATION_JOYSTICK_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(
Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED,
@@ -429,6 +434,8 @@ public class SecureSettingsValidators {
VALIDATORS.put(Secure.DND_CONFIGS_MIGRATED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.HUB_MODE_TUTORIAL_STATE, NON_NEGATIVE_INTEGER_VALIDATOR);
VALIDATORS.put(Secure.GLANCEABLE_HUB_ENABLED, new InclusiveIntegerRangeValidator(0, 1));
+ VALIDATORS.put(Secure.WHEN_TO_START_GLANCEABLE_HUB,
+ new InclusiveIntegerRangeValidator(0, 3));
VALIDATORS.put(Secure.STYLUS_BUTTONS_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.STYLUS_HANDWRITING_ENABLED,
new DiscreteValueValidator(new String[] {"-1", "0", "1"}));
@@ -461,5 +468,7 @@ public class SecureSettingsValidators {
VALIDATORS.put(Secure.FINGERPRINT_APP_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.FINGERPRINT_KEYGUARD_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.DUAL_SHADE, BOOLEAN_VALIDATOR);
+ VALIDATORS.put(Secure.BROWSER_CONTENT_FILTERS_ENABLED, BOOLEAN_VALIDATOR);
+ VALIDATORS.put(Secure.SEARCH_CONTENT_FILTERS_ENABLED, BOOLEAN_VALIDATOR);
}
}
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index e07832eea65e..57facdaa388c 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -1881,6 +1881,10 @@ class SettingsProtoDumpUtil {
SecureSettingsProto.Accessibility
.ACCESSIBILITY_MAGNIFICATION_JOYSTICK_ENABLED);
dumpSetting(s, p,
+ Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE,
+ SecureSettingsProto.Accessibility
+ .ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE);
+ dumpSetting(s, p,
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED,
SecureSettingsProto.Accessibility
.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index 4a225bdbd7e5..65ede9d804d0 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -4080,7 +4080,7 @@ public class SettingsProvider extends ContentProvider {
@VisibleForTesting
final class UpgradeController {
- private static final int SETTINGS_VERSION = 227;
+ private static final int SETTINGS_VERSION = 228;
private final int mUserId;
@@ -6319,6 +6319,23 @@ public class SettingsProvider extends ContentProvider {
currentVersion = 227;
}
+ // Version 227: Add default value for DOUBLE_TAP_TO_SLEEP.
+ if (currentVersion == 227) {
+ final SettingsState secureSettings = getSecureSettingsLocked(userId);
+ final Setting doubleTapToSleep = secureSettings.getSettingLocked(
+ Settings.Secure.DOUBLE_TAP_TO_SLEEP);
+ if (doubleTapToSleep.isNull()) {
+ secureSettings.insertSettingOverrideableByRestoreLocked(
+ Settings.Secure.DOUBLE_TAP_TO_SLEEP,
+ getContext().getResources().getBoolean(
+ R.bool.def_double_tap_to_sleep) ? "1" : "0",
+ null /* tag */,
+ true /* makeDefault */,
+ SettingsState.SYSTEM_PACKAGE_NAME);
+ }
+ currentVersion = 228;
+ }
+
// vXXX: Add new settings above this point.
if (currentVersion != newVersion) {
diff --git a/packages/SystemUI/aconfig/predictive_back.aconfig b/packages/SystemUI/aconfig/predictive_back.aconfig
index 89a0d895a17c..dd9a8b19f498 100644
--- a/packages/SystemUI/aconfig/predictive_back.aconfig
+++ b/packages/SystemUI/aconfig/predictive_back.aconfig
@@ -9,8 +9,11 @@ flag {
}
flag {
- name: "predictive_back_delay_transition"
+ name: "predictive_back_delay_wm_transition"
namespace: "systemui"
description: "Slightly delays the back transition start"
bug: "301195601"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
}
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 9db4346a08b7..9dd49d6b1015 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -292,6 +292,17 @@ flag {
}
flag {
+ name: "notification_row_accessibility_expanded"
+ namespace: "systemui"
+ description: "Prepare ExpandableNotificationRow for new A11y expansion APIs."
+ bug: "380027122"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+
+flag {
name: "scene_container"
namespace: "systemui"
description: "Enables the scene container framework go/flexiglass."
@@ -1390,6 +1401,16 @@ flag {
}
flag {
+ name: "media_controls_device_manager_background_execution"
+ namespace: "systemui"
+ description: "Sends some instances creation to background thread"
+ bug: "400200474"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "output_switcher_redesign"
namespace: "systemui"
description: "Enables visual update for Media Output Switcher"
@@ -1832,6 +1853,16 @@ flag {
}
flag {
+ name: "disable_shade_visible_with_blur"
+ namespace: "systemui"
+ description: "Removes the check for a blur radius when determining shade window visibility"
+ bug: "356804470"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "notification_row_transparency"
namespace: "systemui"
description: "Enables transparency on the Notification Shade."
@@ -2110,3 +2141,10 @@ flag {
description: "Enables moving the launching window on top of the origin window in the Animation library."
bug: "390422470"
}
+
+flag {
+ name: "status_bar_chips_return_animations"
+ namespace: "systemui"
+ description: "Enables return animations for status bar chips"
+ bug: "202516970"
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt
index 9a746870c6ff..e07d7b337ba2 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt
@@ -16,17 +16,18 @@
package com.android.systemui.animation
+import kotlin.text.buildString
+
class FontVariationUtils {
private var mWeight = -1
private var mWidth = -1
private var mOpticalSize = -1
private var mRoundness = -1
- private var isUpdated = false
+ private var mCurrentFVar = ""
/*
* generate fontVariationSettings string, used for key in typefaceCache in TextAnimator
* the order of axes should align to the order of parameters
- * if every axis remains unchanged, return ""
*/
fun updateFontVariation(
weight: Int = -1,
@@ -34,15 +35,17 @@ class FontVariationUtils {
opticalSize: Int = -1,
roundness: Int = -1,
): String {
- isUpdated = false
+ var isUpdated = false
if (weight >= 0 && mWeight != weight) {
isUpdated = true
mWeight = weight
}
+
if (width >= 0 && mWidth != width) {
isUpdated = true
mWidth = width
}
+
if (opticalSize >= 0 && mOpticalSize != opticalSize) {
isUpdated = true
mOpticalSize = opticalSize
@@ -52,23 +55,32 @@ class FontVariationUtils {
isUpdated = true
mRoundness = roundness
}
- var resultString = ""
- if (mWeight >= 0) {
- resultString += "'${GSFAxes.WEIGHT.tag}' $mWeight"
- }
- if (mWidth >= 0) {
- resultString +=
- (if (resultString.isBlank()) "" else ", ") + "'${GSFAxes.WIDTH.tag}' $mWidth"
- }
- if (mOpticalSize >= 0) {
- resultString +=
- (if (resultString.isBlank()) "" else ", ") +
- "'${GSFAxes.OPTICAL_SIZE.tag}' $mOpticalSize"
- }
- if (mRoundness >= 0) {
- resultString +=
- (if (resultString.isBlank()) "" else ", ") + "'${GSFAxes.ROUND.tag}' $mRoundness"
+
+ if (!isUpdated) {
+ return mCurrentFVar
}
- return if (isUpdated) resultString else ""
+
+ return buildString {
+ if (mWeight >= 0) {
+ if (!isBlank()) append(", ")
+ append("'${GSFAxes.WEIGHT.tag}' $mWeight")
+ }
+
+ if (mWidth >= 0) {
+ if (!isBlank()) append(", ")
+ append("'${GSFAxes.WIDTH.tag}' $mWidth")
+ }
+
+ if (mOpticalSize >= 0) {
+ if (!isBlank()) append(", ")
+ append("'${GSFAxes.OPTICAL_SIZE.tag}' $mOpticalSize")
+ }
+
+ if (mRoundness >= 0) {
+ if (!isBlank()) append(", ")
+ append("'${GSFAxes.ROUND.tag}' $mRoundness")
+ }
+ }
+ .also { mCurrentFVar = it }
}
}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GSFAxes.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GSFAxes.kt
index 96feeedb8793..e734dd26eb15 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/GSFAxes.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GSFAxes.kt
@@ -25,6 +25,7 @@ data class AxisDefinition(
)
object GSFAxes {
+ @JvmStatic
val WEIGHT =
AxisDefinition(
tag = "wght",
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
index 5b073e49192a..4a39cff388a9 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
@@ -39,6 +39,7 @@ interface TypefaceVariantCache {
fun getTypefaceForVariant(fvar: String?): Typeface?
companion object {
+ @JvmStatic
fun createVariantTypeface(baseTypeface: Typeface, fVar: String?): Typeface {
if (fVar.isNullOrEmpty()) {
return baseTypeface
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt b/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt
index d08d859ec0d7..fc4d53af4b53 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt
@@ -22,7 +22,6 @@ import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
@@ -32,7 +31,6 @@ import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.modifier.ModifierLocalModifierNode
import androidx.compose.ui.node.DrawModifierNode
@@ -50,11 +48,7 @@ import androidx.compose.ui.util.fastForEach
* The elements redirected to this container will be drawn above the content of this composable.
*/
fun Modifier.container(state: ContainerState): Modifier {
- return onPlaced { state.lastOffsetInWindow = it.positionInWindow() }
- .drawWithContent {
- drawContent()
- state.drawInOverlay(this)
- }
+ return this then ContainerElement(state)
}
/**
@@ -105,6 +99,30 @@ internal interface LayerRenderer {
fun drawInOverlay(drawScope: DrawScope)
}
+private data class ContainerElement(private val state: ContainerState) :
+ ModifierNodeElement<ContainerNode>() {
+ override fun create(): ContainerNode {
+ return ContainerNode(state)
+ }
+
+ override fun update(node: ContainerNode) {
+ node.state = state
+ }
+}
+
+/** A node implementing [container] that can be delegated to. */
+class ContainerNode(var state: ContainerState) :
+ Modifier.Node(), LayoutAwareModifierNode, DrawModifierNode {
+ override fun onPlaced(coordinates: LayoutCoordinates) {
+ state.lastOffsetInWindow = coordinates.positionInWindow()
+ }
+
+ override fun ContentDrawScope.draw() {
+ drawContent()
+ state.drawInOverlay(this)
+ }
+}
+
private data class DrawInContainerElement(
var state: ContainerState,
var enabled: () -> Boolean,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index 061fdd99eb1b..0a711487ccb1 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -356,7 +356,8 @@ private fun ContentScope.QuickSettingsScene(
modifier = Modifier.padding(horizontal = 16.dp),
)
}
- else -> CollapsedShadeHeader(viewModel = headerViewModel)
+ else ->
+ CollapsedShadeHeader(viewModel = headerViewModel, isSplitShade = false)
}
Spacer(modifier = Modifier.height(16.dp))
// This view has its own horizontal padding
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt
index a0216268308c..8f0fb20cef36 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt
@@ -52,6 +52,7 @@ import com.android.compose.animation.scene.UserActionResult
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.modifiers.thenIf
import com.android.systemui.brightness.ui.compose.BrightnessSliderContainer
+import com.android.systemui.brightness.ui.compose.ContainerColors
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.lifecycle.rememberViewModel
@@ -257,15 +258,18 @@ fun ContentScope.QuickSettingsLayout(
modifier = Modifier.padding(horizontal = QuickSettingsShade.Dimensions.Padding),
)
- BrightnessSliderContainer(
- viewModel = viewModel.brightnessSliderViewModel,
- containerColor = OverlayShade.Colors.PanelBackground,
- modifier =
- Modifier.systemGestureExclusionInShade(
- enabled = { layoutState.transitionState is TransitionState.Idle }
- )
- .fillMaxWidth(),
- )
+ Box(
+ Modifier.systemGestureExclusionInShade(
+ enabled = { layoutState.transitionState is TransitionState.Idle }
+ )
+ ) {
+ BrightnessSliderContainer(
+ viewModel = viewModel.brightnessSliderViewModel,
+ containerColors =
+ ContainerColors.singleColor(OverlayShade.Colors.PanelBackground),
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
Box {
GridAnchor()
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
index 23baeacd76ec..86c8fc34a63c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
@@ -127,6 +127,7 @@ object ShadeHeader {
@Composable
fun ContentScope.CollapsedShadeHeader(
viewModel: ShadeHeaderViewModel,
+ isSplitShade: Boolean,
modifier: Modifier = Modifier,
) {
val cutoutLocation = LocalDisplayCutout.current.location
@@ -141,8 +142,6 @@ fun ContentScope.CollapsedShadeHeader(
}
}
- val isShadeLayoutWide = viewModel.isShadeLayoutWide
-
val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle()
// This layout assumes it is globally positioned at (0, 0) and is the same size as the screen.
@@ -154,7 +153,7 @@ fun ContentScope.CollapsedShadeHeader(
horizontalArrangement = Arrangement.spacedBy(5.dp),
modifier = Modifier.padding(horizontal = horizontalPadding),
) {
- Clock(scale = 1f, onClick = viewModel::onClockClicked)
+ Clock(onClick = viewModel::onClockClicked)
VariableDayDate(
longerDateText = viewModel.longerDateText,
shorterDateText = viewModel.shorterDateText,
@@ -184,11 +183,11 @@ fun ContentScope.CollapsedShadeHeader(
Modifier.element(ShadeHeader.Elements.CollapsedContentEnd)
.padding(horizontal = horizontalPadding),
) {
- if (isShadeLayoutWide) {
+ if (isSplitShade) {
ShadeCarrierGroup(viewModel = viewModel)
}
SystemIconChip(
- onClick = viewModel::onSystemIconChipClicked.takeIf { isShadeLayoutWide }
+ onClick = viewModel::onSystemIconChipClicked.takeIf { isSplitShade }
) {
StatusIcons(
viewModel = viewModel,
@@ -233,13 +232,11 @@ fun ContentScope.ExpandedShadeHeader(
.defaultMinSize(minHeight = ShadeHeader.Dimensions.ExpandedHeight),
) {
Box(modifier = Modifier.fillMaxWidth()) {
- Box {
- Clock(
- scale = 2.57f,
- onClick = viewModel::onClockClicked,
- modifier = Modifier.align(Alignment.CenterStart),
- )
- }
+ Clock(
+ onClick = viewModel::onClockClicked,
+ modifier = Modifier.align(Alignment.CenterStart),
+ scale = 2.57f,
+ )
Box(
modifier =
Modifier.element(ShadeHeader.Elements.ShadeCarrierGroup).fillMaxWidth()
@@ -291,8 +288,6 @@ fun ContentScope.OverlayShadeHeader(
val horizontalPadding =
max(LocalScreenCornerRadius.current / 2f, Shade.Dimensions.HorizontalPadding)
- val isShadeLayoutWide = viewModel.isShadeLayoutWide
-
val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle()
// This layout assumes it is globally positioned at (0, 0) and is the same size as the screen.
@@ -301,16 +296,15 @@ fun ContentScope.OverlayShadeHeader(
startContent = {
Row(
verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(5.dp),
modifier = Modifier.padding(horizontal = horizontalPadding),
) {
val chipHighlight = viewModel.notificationsChipHighlight
- if (isShadeLayoutWide) {
+ if (viewModel.showClock) {
Clock(
- scale = 1f,
onClick = viewModel::onClockClicked,
modifier = Modifier.padding(horizontal = 4.dp),
)
- Spacer(modifier = Modifier.width(5.dp))
}
NotificationsChip(
onClick = viewModel::onNotificationIconChipClicked,
@@ -437,7 +431,11 @@ private fun CutoutAwareShadeHeader(
}
@Composable
-private fun ContentScope.Clock(scale: Float, onClick: () -> Unit, modifier: Modifier = Modifier) {
+private fun ContentScope.Clock(
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ scale: Float = 1f,
+) {
val layoutDirection = LocalLayoutDirection.current
ElementWithValues(key = ShadeHeader.Elements.Clock, modifier = modifier) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index 5040490da8f6..885d34fb95c9 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -56,11 +56,11 @@ import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
+import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.animation.scene.ContentScope
import com.android.compose.animation.scene.ElementKey
@@ -68,6 +68,7 @@ import com.android.compose.animation.scene.LowestZIndexContentPicker
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.compose.animation.scene.animateContentDpAsState
+import com.android.compose.animation.scene.animateContentFloatAsState
import com.android.compose.animation.scene.animateSceneFloatAsState
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.modifiers.padding
@@ -223,9 +224,6 @@ private fun ContentScope.ShadeScene(
viewModel = viewModel,
headerViewModel = headerViewModel,
notificationsPlaceholderViewModel = notificationsPlaceholderViewModel,
- createTintedIconManager = createTintedIconManager,
- createBatteryMeterViewController = createBatteryMeterViewController,
- statusBarIconController = statusBarIconController,
mediaCarouselController = mediaCarouselController,
mediaHost = qqsMediaHost,
modifier = modifier,
@@ -253,9 +251,6 @@ private fun ContentScope.SingleShade(
viewModel: ShadeSceneContentViewModel,
headerViewModel: ShadeHeaderViewModel,
notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel,
- createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
- createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
- statusBarIconController: StatusBarIconController,
mediaCarouselController: MediaCarouselController,
mediaHost: MediaHost,
modifier: Modifier = Modifier,
@@ -340,6 +335,7 @@ private fun ContentScope.SingleShade(
content = {
CollapsedShadeHeader(
viewModel = headerViewModel,
+ isSplitShade = false,
modifier = Modifier.layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader),
)
@@ -434,15 +430,13 @@ private fun ContentScope.SplitShade(
val footerActionsViewModel =
remember(lifecycleOwner, viewModel) { viewModel.getFooterActionsViewModel(lifecycleOwner) }
val tileSquishiness by
- animateSceneFloatAsState(
+ animateContentFloatAsState(
value = 1f,
key = QuickSettings.SharedValues.TilesSquishiness,
canOverflow = false,
)
val unfoldTranslationXForStartSide by
viewModel.unfoldTranslationX(isOnStartSide = true).collectAsStateWithLifecycle(0f)
- val unfoldTranslationXForEndSide by
- viewModel.unfoldTranslationX(isOnStartSide = false).collectAsStateWithLifecycle(0f)
val notificationStackPadding = dimensionResource(id = R.dimen.notification_side_paddings)
val navBarBottomHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
@@ -512,6 +506,7 @@ private fun ContentScope.SplitShade(
Column(modifier = Modifier.fillMaxSize()) {
CollapsedShadeHeader(
viewModel = headerViewModel,
+ isSplitShade = true,
modifier =
Modifier.then(brightnessMirrorShowingModifier)
.padding(horizontal = { unfoldTranslationXForStartSide.roundToInt() }),
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
index 72ee75ad2d47..90bf92ae1dd0 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
@@ -32,12 +32,17 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ApproachLayoutModifierNode
+import androidx.compose.ui.layout.ApproachMeasureScope
import androidx.compose.ui.layout.LookaheadScope
-import androidx.compose.ui.layout.approachLayout
-import androidx.compose.ui.layout.layout
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.node.DelegatingNode
+import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.zIndex
import com.android.compose.animation.scene.Ancestor
import com.android.compose.animation.scene.AnimatedState
import com.android.compose.animation.scene.ContentKey
@@ -68,8 +73,8 @@ import com.android.compose.gesture.NestedScrollControlState
import com.android.compose.gesture.NestedScrollableBound
import com.android.compose.gesture.nestedScrollController
import com.android.compose.modifiers.thenIf
+import com.android.compose.ui.graphics.ContainerNode
import com.android.compose.ui.graphics.ContainerState
-import com.android.compose.ui.graphics.container
import kotlin.math.pow
/** A content defined in a [SceneTransitionLayout], i.e. a scene or an overlay. */
@@ -158,24 +163,14 @@ internal sealed class Content(
fun Content(modifier: Modifier = Modifier, isInvisible: Boolean = false) {
// If this content has a custom factory, provide it to the content so that the factory is
// automatically used when calling rememberOverscrollEffect().
+ val isElevationPossible =
+ layoutImpl.state.isElevationPossible(content = key, element = null)
Box(
- modifier
- .thenIf(isInvisible) { InvisibleModifier }
- .zIndex(zIndex)
- .approachLayout(
- isMeasurementApproachInProgress = { layoutImpl.state.isTransitioning() }
- ) { measurable, constraints ->
- // TODO(b/353679003): Use the ModifierNode API to set this *before* the
- // approach
- // pass is started.
- targetSize = lookaheadSize
- val placeable = measurable.measure(constraints)
- layout(placeable.width, placeable.height) { placeable.place(0, 0) }
- }
- .thenIf(layoutImpl.state.isElevationPossible(content = key, element = null)) {
- Modifier.container(containerState)
- }
- .thenIf(layoutImpl.implicitTestTags) { Modifier.testTag(key.testTag) }
+ modifier.then(ContentElement(this, isElevationPossible, isInvisible)).thenIf(
+ layoutImpl.implicitTestTags
+ ) {
+ Modifier.testTag(key.testTag)
+ }
) {
CompositionLocalProvider(LocalOverscrollFactory provides lastFactory) {
scope.content()
@@ -194,6 +189,72 @@ internal sealed class Content(
}
}
+private data class ContentElement(
+ private val content: Content,
+ private val isElevationPossible: Boolean,
+ private val isInvisible: Boolean,
+) : ModifierNodeElement<ContentNode>() {
+ override fun create(): ContentNode = ContentNode(content, isElevationPossible, isInvisible)
+
+ override fun update(node: ContentNode) {
+ node.update(content, isElevationPossible, isInvisible)
+ }
+}
+
+private class ContentNode(
+ private var content: Content,
+ private var isElevationPossible: Boolean,
+ private var isInvisible: Boolean,
+) : DelegatingNode(), ApproachLayoutModifierNode {
+ private var containerDelegate = containerDelegate(isElevationPossible)
+
+ private fun containerDelegate(isElevationPossible: Boolean): ContainerNode? {
+ return if (isElevationPossible) delegate(ContainerNode(content.containerState)) else null
+ }
+
+ fun update(content: Content, isElevationPossible: Boolean, isInvisible: Boolean) {
+ if (content != this.content || isElevationPossible != this.isElevationPossible) {
+ this.content = content
+ this.isElevationPossible = isElevationPossible
+
+ containerDelegate?.let { undelegate(it) }
+ containerDelegate = containerDelegate(isElevationPossible)
+ }
+
+ this.isInvisible = isInvisible
+ }
+
+ override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean = false
+
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints,
+ ): MeasureResult {
+ check(isLookingAhead)
+ return measurable.measure(constraints).run {
+ content.targetSize = IntSize(width, height)
+ layout(width, height) {
+ if (!isInvisible) {
+ place(0, 0, zIndex = content.zIndex)
+ }
+ }
+ }
+ }
+
+ override fun ApproachMeasureScope.approachMeasure(
+ measurable: Measurable,
+ constraints: Constraints,
+ ): MeasureResult {
+ return measurable.measure(constraints).run {
+ layout(width, height) {
+ if (!isInvisible) {
+ place(0, 0, zIndex = content.zIndex)
+ }
+ }
+ }
+ }
+}
+
internal class ContentEffects(factory: OverscrollFactory) {
val overscrollEffect = factory.createOverscrollEffect()
val gestureEffect = GestureEffect(overscrollEffect)
@@ -307,8 +368,3 @@ internal class ContentScopeImpl(
)
}
}
-
-private val InvisibleModifier =
- Modifier.layout { measurable, constraints ->
- measurable.measure(constraints).run { layout(width, height) {} }
- }
diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragFullyClose.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragFullyClose.json
index 0fcdfa3e1b53..57f67665242c 100644
--- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragFullyClose.json
+++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragFullyClose.json
@@ -60,7 +60,9 @@
912,
928,
944,
- 960
+ 960,
+ 976,
+ 992
],
"features": [
{
@@ -310,6 +312,14 @@
{
"x": 64,
"y": 50
+ },
+ {
+ "x": 64,
+ "y": 50
+ },
+ {
+ "x": 64,
+ "y": 50
}
]
},
@@ -491,67 +501,75 @@
},
{
"width": 170.4,
- "height": 39.2
+ "height": 28.8
},
{
"width": 166.8,
- "height": 36
+ "height": 15.2
},
{
"width": 164,
- "height": 31.6
+ "height": 6.4
},
{
"width": 162.4,
- "height": 26.8
+ "height": 0.8
},
{
"width": 161.2,
- "height": 22
+ "height": 0
},
{
"width": 160.4,
- "height": 17.6
+ "height": 0
+ },
+ {
+ "width": 160,
+ "height": 0
+ },
+ {
+ "width": 160,
+ "height": 0
},
{
"width": 160,
- "height": 14
+ "height": 0
},
{
"width": 160,
- "height": 10.8
+ "height": 0
},
{
"width": 160,
- "height": 8
+ "height": 0
},
{
"width": 160,
- "height": 6
+ "height": 0
},
{
"width": 160,
- "height": 4.4
+ "height": 0
},
{
"width": 160,
- "height": 2.8
+ "height": 0
},
{
"width": 160,
- "height": 2
+ "height": 0
},
{
"width": 160,
- "height": 1.2
+ "height": 0
},
{
"width": 160,
- "height": 0.8
+ "height": 0
},
{
"width": 160,
- "height": 0.4
+ "height": 0
},
{
"width": 160,
@@ -627,6 +645,8 @@
0,
0,
0,
+ 0,
+ 0,
0
]
}
diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragHalfClose.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragHalfClose.json
index 3196334c5314..01bc852cf7f4 100644
--- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragHalfClose.json
+++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragHalfClose.json
@@ -59,7 +59,9 @@
896,
912,
928,
- 944
+ 944,
+ 960,
+ 976
],
"features": [
{
@@ -305,6 +307,14 @@
{
"x": 64,
"y": 50
+ },
+ {
+ "x": 64,
+ "y": 50
+ },
+ {
+ "x": 64,
+ "y": 50
}
]
},
@@ -482,71 +492,79 @@
},
{
"width": 171.6,
- "height": 39.6
+ "height": 32
},
{
"width": 167.6,
- "height": 36.8
+ "height": 18
},
{
"width": 164.8,
- "height": 32.4
+ "height": 8.8
},
{
"width": 162.8,
- "height": 27.6
+ "height": 2.8
},
{
"width": 161.6,
- "height": 22.8
+ "height": 0
},
{
"width": 160.8,
- "height": 18.4
+ "height": 0
},
{
"width": 160.4,
- "height": 14.4
+ "height": 0
},
{
"width": 160,
- "height": 11.2
+ "height": 0
},
{
"width": 160,
- "height": 8.4
+ "height": 0
},
{
"width": 160,
- "height": 6.4
+ "height": 0
},
{
"width": 160,
- "height": 4.4
+ "height": 0
},
{
"width": 160,
- "height": 3.2
+ "height": 0
},
{
"width": 160,
- "height": 2
+ "height": 0
},
{
"width": 160,
- "height": 1.6
+ "height": 0
},
{
"width": 160,
- "height": 0.8
+ "height": 0
},
{
"width": 160,
- "height": 0.4
+ "height": 0
},
{
"width": 160,
- "height": 0.4
+ "height": 0
+ },
+ {
+ "width": 160,
+ "height": 0
+ },
+ {
+ "width": 160,
+ "height": 0
},
{
"width": 160,
@@ -617,6 +635,8 @@
0,
0,
0,
+ 0,
+ 0,
0
]
}
diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragOpen.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragOpen.json
index 4b0306853903..b6e423afc6c4 100644
--- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragOpen.json
+++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragOpen.json
@@ -336,55 +336,55 @@
},
{
"width": 188,
- "height": 24.8
+ "height": 25.6
},
{
"width": 188,
- "height": 32.8
+ "height": 36.4
},
{
"width": 188,
- "height": 44
+ "height": 45.6
},
{
"width": 188,
- "height": 57.2
+ "height": 59.2
},
{
"width": 188,
- "height": 70.8
+ "height": 72.8
},
{
"width": 188,
- "height": 78
+ "height": 79.6
},
{
"width": 188,
- "height": 91.2
+ "height": 92.8
},
{
"width": 188,
- "height": 103.2
+ "height": 104.4
},
{
"width": 188,
- "height": 114.4
+ "height": 115.2
},
{
"width": 188,
- "height": 124.4
+ "height": 125.2
},
{
"width": 188,
- "height": 134
+ "height": 134.8
},
{
"width": 188,
- "height": 142.8
+ "height": 143.2
},
{
"width": 188,
- "height": 150.8
+ "height": 151.2
},
{
"width": 188,
@@ -392,7 +392,7 @@
},
{
"width": 188,
- "height": 159.6
+ "height": 160
},
{
"width": 188,
@@ -400,7 +400,7 @@
},
{
"width": 188,
- "height": 174
+ "height": 174.4
},
{
"width": 188,
@@ -412,7 +412,7 @@
},
{
"width": 188,
- "height": 187.6
+ "height": 188
},
{
"width": 188,
diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingClose.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingClose.json
index 10a9ba7e2760..a82db346ed58 100644
--- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingClose.json
+++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingClose.json
@@ -39,7 +39,9 @@
576,
592,
608,
- 624
+ 624,
+ 640,
+ 656
],
"features": [
{
@@ -205,6 +207,14 @@
{
"x": 64,
"y": 50
+ },
+ {
+ "x": 64,
+ "y": 50
+ },
+ {
+ "x": 64,
+ "y": 50
}
]
},
@@ -302,67 +312,75 @@
},
{
"width": 170.8,
- "height": 39.6
+ "height": 29.6
},
{
"width": 166.8,
- "height": 36.8
+ "height": 12.8
},
{
"width": 164,
- "height": 32.4
+ "height": 2.4
},
{
"width": 162,
- "height": 27.6
+ "height": 0
},
{
"width": 160.8,
- "height": 22.8
+ "height": 0
},
{
"width": 160.4,
- "height": 18.4
+ "height": 0
},
{
"width": 160,
- "height": 14.4
+ "height": 0
},
{
"width": 160,
- "height": 11.2
+ "height": 0
},
{
"width": 160,
- "height": 8.4
+ "height": 0
},
{
"width": 160,
- "height": 6
+ "height": 0
},
{
"width": 160,
- "height": 4.4
+ "height": 0
},
{
"width": 160,
- "height": 3.2
+ "height": 0
},
{
"width": 160,
- "height": 2
+ "height": 0
},
{
"width": 160,
- "height": 1.2
+ "height": 0
},
{
"width": 160,
- "height": 0.8
+ "height": 0
},
{
"width": 160,
- "height": 0.4
+ "height": 0
+ },
+ {
+ "width": 160,
+ "height": 0
+ },
+ {
+ "width": 160,
+ "height": 0
},
{
"width": 160,
@@ -417,6 +435,8 @@
0,
0,
0,
+ 0,
+ 0,
0
]
}
diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingOpen.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingOpen.json
index d8bf48d32d20..6dc5a0e79e81 100644
--- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingOpen.json
+++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingOpen.json
@@ -32,8 +32,7 @@
464,
480,
496,
- 512,
- 528
+ 512
],
"features": [
{
@@ -163,10 +162,6 @@
{
"x": 50,
"y": 50
- },
- {
- "x": 50,
- "y": 50
}
]
},
@@ -228,63 +223,59 @@
},
{
"width": 188,
- "height": 42.4
+ "height": 44.4
},
{
"width": 188,
- "height": 100
+ "height": 103.6
},
{
"width": 188,
- "height": 161.6
+ "height": 166
},
{
"width": 188,
- "height": 218
+ "height": 222.4
},
{
"width": 188,
- "height": 265.6
+ "height": 270
},
{
"width": 188,
- "height": 303.6
+ "height": 307.2
},
{
"width": 188,
- "height": 332.4
+ "height": 335.6
},
{
"width": 188,
- "height": 354
+ "height": 356.4
},
{
"width": 188,
- "height": 369.2
+ "height": 371.2
},
{
"width": 188,
- "height": 380
+ "height": 381.6
},
{
"width": 188,
- "height": 387.2
+ "height": 388.8
},
{
"width": 188,
- "height": 392
+ "height": 393.2
},
{
"width": 188,
- "height": 395.2
+ "height": 396
},
{
"width": 188,
- "height": 397.6
- },
- {
- "width": 188,
- "height": 398.4
+ "height": 398
},
{
"width": 188,
@@ -356,7 +347,6 @@
1,
1,
1,
- 1,
1
]
}
diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_magneticDetachAndReattach.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_magneticDetachAndReattach.json
index 57bdf3e1ecab..1cd971aa2898 100644
--- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_magneticDetachAndReattach.json
+++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_magneticDetachAndReattach.json
@@ -69,9 +69,7 @@
1056,
1072,
1088,
- 1104,
- 1120,
- 1136
+ 1104
],
"features": [
{
@@ -353,14 +351,6 @@
{
"x": 64,
"y": 50
- },
- {
- "x": 64,
- "y": 50
- },
- {
- "x": 64,
- "y": 50
}
]
},
@@ -466,43 +456,43 @@
},
{
"width": 188,
- "height": 24
+ "height": 24.8
},
{
"width": 188,
- "height": 29.6
+ "height": 30
},
{
"width": 188,
- "height": 37.2
+ "height": 38
},
{
"width": 188,
- "height": 45.6
+ "height": 46
},
{
"width": 188,
- "height": 53.6
+ "height": 54
},
{
"width": 188,
- "height": 60.4
+ "height": 61.2
},
{
"width": 188,
- "height": 66.4
+ "height": 66.8
},
{
"width": 188,
- "height": 71.2
+ "height": 71.6
},
{
"width": 188,
- "height": 75.2
+ "height": 75.6
},
{
"width": 188,
- "height": 77.6
+ "height": 78
},
{
"width": 188,
@@ -510,7 +500,7 @@
},
{
"width": 188,
- "height": 80.4
+ "height": 80.8
},
{
"width": 188,
@@ -522,7 +512,7 @@
},
{
"width": 188,
- "height": 79.2
+ "height": 79.6
},
{
"width": 187.6,
@@ -530,7 +520,7 @@
},
{
"width": 186.8,
- "height": 76
+ "height": 76.4
},
{
"width": 186,
@@ -578,43 +568,39 @@
},
{
"width": 172.4,
- "height": 39.2
+ "height": 37.6
},
{
"width": 170.8,
- "height": 38.4
+ "height": 38
},
{
"width": 169.2,
- "height": 34.8
+ "height": 30.4
},
{
"width": 167.6,
- "height": 30
+ "height": 25.2
},
{
"width": 166,
- "height": 25.2
+ "height": 20.4
},
{
"width": 164,
- "height": 20.4
+ "height": 16
},
{
"width": 162.4,
- "height": 16.4
+ "height": 12.4
},
{
"width": 160.8,
- "height": 12.8
- },
- {
- "width": 160,
- "height": 9.6
+ "height": 9.2
},
{
"width": 160,
- "height": 7.2
+ "height": 6.8
},
{
"width": 160,
@@ -626,7 +612,7 @@
},
{
"width": 160,
- "height": 2.8
+ "height": 2.4
},
{
"width": 160,
@@ -634,10 +620,6 @@
},
{
"width": 160,
- "height": 1.2
- },
- {
- "width": 160,
"height": 0.8
},
{
@@ -646,7 +628,7 @@
},
{
"width": 160,
- "height": 0
+ "height": 0.4
},
{
"width": 160,
@@ -735,8 +717,6 @@
0.03147719,
0.019312752,
0.011740655,
- 0,
- 0,
0
]
}
diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_triggeredRevealCloseTransition.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_triggeredRevealCloseTransition.json
index 9aa91c2d5e17..1030455e873f 100644
--- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_triggeredRevealCloseTransition.json
+++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_triggeredRevealCloseTransition.json
@@ -27,7 +27,9 @@
384,
400,
416,
- 432
+ 432,
+ 448,
+ 464
],
"features": [
{
@@ -145,6 +147,14 @@
{
"x": 64,
"y": 50
+ },
+ {
+ "x": 64,
+ "y": 50
+ },
+ {
+ "x": 64,
+ "y": 50
}
]
},
@@ -194,67 +204,75 @@
},
{
"width": 168.8,
- "height": 38.4
+ "height": 22.4
},
{
"width": 165.2,
- "height": 34.8
+ "height": 10
},
{
"width": 162.8,
- "height": 30
+ "height": 2.4
},
{
"width": 161.2,
- "height": 25.2
+ "height": 0
},
{
"width": 160.4,
- "height": 20.4
+ "height": 0
},
{
"width": 160,
- "height": 16.4
+ "height": 0
},
{
"width": 160,
- "height": 12.8
+ "height": 0
},
{
"width": 160,
- "height": 9.6
+ "height": 0
},
{
"width": 160,
- "height": 7.2
+ "height": 0
},
{
"width": 160,
- "height": 5.2
+ "height": 0
},
{
"width": 160,
- "height": 3.6
+ "height": 0
},
{
"width": 160,
- "height": 2.8
+ "height": 0
},
{
"width": 160,
- "height": 1.6
+ "height": 0
},
{
"width": 160,
- "height": 1.2
+ "height": 0
},
{
"width": 160,
- "height": 0.8
+ "height": 0
},
{
"width": 160,
- "height": 0.4
+ "height": 0
+ },
+ {
+ "width": 160,
+ "height": 0
+ },
+ {
+ "width": 160,
+ "height": 0
},
{
"width": 160,
@@ -297,6 +315,8 @@
0,
0,
0,
+ 0,
+ 0,
0
]
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt
index 0bd51cd9822d..44f2353dcb75 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt
@@ -111,8 +111,8 @@ private constructor(metadata: FailureMetadata, private val actual: TransitionSta
}
companion object {
- fun transitionStates() = Factory { metadata, actual: TransitionState ->
- TransitionStateSubject(metadata, actual)
+ fun transitionStates() = Factory { metadata, actual: TransitionState? ->
+ TransitionStateSubject(metadata, actual!!)
}
}
}
@@ -181,8 +181,8 @@ private constructor(metadata: FailureMetadata, actual: TransitionState.Transitio
companion object {
fun sceneTransitions() =
- Factory { metadata, actual: TransitionState.Transition.ChangeScene ->
- SceneTransitionSubject(metadata, actual)
+ Factory { metadata, actual: TransitionState.Transition.ChangeScene? ->
+ SceneTransitionSubject(metadata, actual!!)
}
}
}
@@ -202,8 +202,8 @@ private constructor(
companion object {
fun showOrHideOverlayTransitions() =
- Factory { metadata, actual: TransitionState.Transition.ShowOrHideOverlay ->
- ShowOrHideOverlayTransitionSubject(metadata, actual)
+ Factory { metadata, actual: TransitionState.Transition.ShowOrHideOverlay? ->
+ ShowOrHideOverlayTransitionSubject(metadata, actual!!)
}
}
}
@@ -221,8 +221,8 @@ private constructor(metadata: FailureMetadata, actual: TransitionState.Transitio
companion object {
fun replaceOverlayTransitions() =
- Factory { metadata, actual: TransitionState.Transition.ReplaceOverlay ->
- ReplaceOverlayTransitionSubject(metadata, actual)
+ Factory { metadata, actual: TransitionState.Transition.ReplaceOverlay? ->
+ ReplaceOverlayTransitionSubject(metadata, actual!!)
}
}
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/subjects/DpOffsetSubject.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/subjects/DpOffsetSubject.kt
index ab31038fac8f..368a333fbd78 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/subjects/DpOffsetSubject.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/subjects/DpOffsetSubject.kt
@@ -49,8 +49,8 @@ class DpOffsetSubject(metadata: FailureMetadata, private val actual: DpOffset) :
val DefaultTolerance = Dp(.5f)
fun dpOffsets() =
- Factory<DpOffsetSubject, DpOffset> { metadata, actual ->
- DpOffsetSubject(metadata, actual)
+ Factory { metadata, actual: DpOffset? ->
+ DpOffsetSubject(metadata, actual!!)
}
}
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index 4bf0ceb51784..6e29e6932629 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -38,8 +38,9 @@ import com.android.systemui.animation.TypefaceVariantCacheImpl
import com.android.systemui.customization.R
import com.android.systemui.log.core.LogLevel
import com.android.systemui.log.core.LogcatOnlyMessageBuffer
-import com.android.systemui.log.core.Logger
import com.android.systemui.log.core.MessageBuffer
+import com.android.systemui.plugins.clocks.ClockLogger
+import com.android.systemui.plugins.clocks.ClockLogger.Companion.escapeTime
import java.io.PrintWriter
import java.util.Calendar
import java.util.Locale
@@ -67,7 +68,7 @@ constructor(
var messageBuffer: MessageBuffer
get() = logger.buffer
set(value) {
- logger = Logger(value, TAG)
+ logger = ClockLogger(this, value, TAG)
}
var hasCustomPositionUpdatedAnimation: Boolean = false
@@ -185,7 +186,9 @@ constructor(
time.timeInMillis = timeOverrideInMillis ?: System.currentTimeMillis()
contentDescription = DateFormat.format(descFormat, time)
val formattedText = DateFormat.format(format, time)
- logger.d({ "refreshTime: new formattedText=$str1" }) { str1 = formattedText?.toString() }
+ logger.d({ "refreshTime: new formattedText=${escapeTime(str1)}" }) {
+ str1 = formattedText?.toString()
+ }
// Setting text actually triggers a layout pass in TextView (because the text view is set to
// wrap_content width and TextView always relayouts for this). This avoids needless relayout
@@ -195,7 +198,7 @@ constructor(
}
text = formattedText
- logger.d({ "refreshTime: done setting new time text to: $str1" }) {
+ logger.d({ "refreshTime: done setting new time text to: ${escapeTime(str1)}" }) {
str1 = formattedText?.toString()
}
@@ -225,7 +228,7 @@ constructor(
@SuppressLint("DrawAllocation")
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
- logger.d("onMeasure")
+ logger.onMeasure(widthMeasureSpec, heightMeasureSpec)
if (!isSingleLineInternal && MeasureSpec.getMode(heightMeasureSpec) == EXACTLY) {
// Call straight into TextView.setTextSize to avoid setting lastUnconstrainedTextSize
@@ -263,14 +266,14 @@ constructor(
canvas.translate(parentWidth / 4f, 0f)
}
- logger.d({ "onDraw($str1)" }) { str1 = text.toString() }
+ logger.onDraw("$text")
// intentionally doesn't call super.onDraw here or else the text will be rendered twice
textAnimator?.draw(canvas)
canvas.restore()
}
override fun invalidate() {
- logger.d("invalidate")
+ logger.invalidate()
super.invalidate()
}
@@ -280,7 +283,7 @@ constructor(
lengthBefore: Int,
lengthAfter: Int,
) {
- logger.d({ "onTextChanged($str1)" }) { str1 = text.toString() }
+ logger.d({ "onTextChanged(${escapeTime(str1)})" }) { str1 = "$text" }
super.onTextChanged(text, start, lengthBefore, lengthAfter)
}
@@ -370,7 +373,7 @@ constructor(
return
}
- logger.d("animateCharge")
+ logger.animateCharge()
val startAnimPhase2 = Runnable {
setTextStyle(
weight = if (isDozing()) dozingWeight else lockScreenWeight,
@@ -394,7 +397,7 @@ constructor(
}
fun animateDoze(isDozing: Boolean, animate: Boolean) {
- logger.d("animateDoze")
+ logger.animateDoze(isDozing, animate)
setTextStyle(
weight = if (isDozing) dozingWeight else lockScreenWeight,
color = if (isDozing) dozingColor else lockScreenColor,
@@ -484,7 +487,7 @@ constructor(
isSingleLineInternal && !use24HourFormat -> Patterns.sClockView12
else -> DOUBLE_LINE_FORMAT_12_HOUR
}
- logger.d({ "refreshFormat($str1)" }) { str1 = format?.toString() }
+ logger.d({ "refreshFormat(${escapeTime(str1)})" }) { str1 = format?.toString() }
descFormat = if (use24HourFormat) Patterns.sClockView24 else Patterns.sClockView12
refreshTime()
@@ -634,7 +637,7 @@ constructor(
companion object {
private val TAG = AnimatableClockView::class.simpleName!!
- private val DEFAULT_LOGGER = Logger(LogcatOnlyMessageBuffer(LogLevel.WARNING), TAG)
+ private val DEFAULT_LOGGER = ClockLogger(null, LogcatOnlyMessageBuffer(LogLevel.DEBUG), TAG)
const val ANIMATION_DURATION_FOLD_TO_AOD: Int = 600
private const val DOUBLE_LINE_FORMAT_12_HOUR = "hh\nmm"
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/CanvasUtil.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/CanvasUtil.kt
index dd1599e5259d..9857d7f3d69d 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/CanvasUtil.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/CanvasUtil.kt
@@ -17,6 +17,8 @@
package com.android.systemui.shared.clocks
import android.graphics.Canvas
+import com.android.systemui.plugins.clocks.VPoint
+import com.android.systemui.plugins.clocks.VPointF
object CanvasUtil {
fun Canvas.translate(pt: VPointF) = this.translate(pt.x, pt.y)
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
index e9e61a718f08..37acbe261f76 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt
@@ -161,15 +161,7 @@ class ComposedDigitalLayerController(private val clockCtx: ClockContext) :
}
override fun onThemeChanged(theme: ThemeConfig) {
- val color =
- when {
- theme.seedColor != null -> theme.seedColor!!
- theme.isDarkTheme ->
- clockCtx.resources.getColor(android.R.color.system_accent1_100)
- else -> clockCtx.resources.getColor(android.R.color.system_accent2_600)
- }
-
- view.updateColor(color)
+ view.updateColor(theme.getDefaultColor(clockCtx.context))
}
override fun onFontSettingChanged(fontSizePx: Float) {
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
index 365567b17ec0..bc4bdf4243cb 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
@@ -149,14 +149,7 @@ class DefaultClockController(
override fun onThemeChanged(theme: ThemeConfig) {
this@DefaultClockFaceController.theme = theme
- val color =
- when {
- theme.seedColor != null -> theme.seedColor!!
- theme.isDarkTheme ->
- resources.getColor(android.R.color.system_accent1_100)
- else -> resources.getColor(android.R.color.system_accent2_600)
- }
-
+ val color = theme.getDefaultColor(ctx)
if (currentColor == color) {
return
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt
index f5ccc52c8c6b..941cebfb4014 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt
@@ -20,7 +20,8 @@ import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.TimeInterpolator
import android.animation.ValueAnimator
-import com.android.systemui.shared.clocks.VPointF.Companion.times
+import com.android.systemui.plugins.clocks.VPointF
+import com.android.systemui.plugins.clocks.VPointF.Companion.times
class DigitTranslateAnimator(private val updateCallback: (VPointF) -> Unit) {
var currentTranslation = VPointF.ZERO
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
index 336c66eed889..9ac9e60f05fd 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt
@@ -16,13 +16,13 @@
package com.android.systemui.shared.clocks
-import android.graphics.RectF
import android.view.View
import androidx.annotation.VisibleForTesting
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.VRectF
interface SimpleClockLayerController {
val view: View
@@ -32,5 +32,5 @@ interface SimpleClockLayerController {
val config: ClockFaceConfig
@VisibleForTesting var fakeTimeMills: Long?
- var onViewBoundsChanged: ((RectF) -> Unit)?
+ var onViewBoundsChanged: ((VRectF) -> Unit)?
}
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
index 97004ef6f9a9..1d963af3ad22 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
@@ -229,15 +229,7 @@ open class SimpleDigitalHandLayerController(
}
override fun onThemeChanged(theme: ThemeConfig) {
- val color =
- when {
- theme.seedColor != null -> theme.seedColor!!
- theme.isDarkTheme ->
- clockCtx.resources.getColor(android.R.color.system_accent1_100)
- else -> clockCtx.resources.getColor(android.R.color.system_accent2_600)
- }
-
- view.updateColor(color)
+ view.updateColor(theme.getDefaultColor(clockCtx.context))
refreshTime()
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ViewUtils.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ViewUtils.kt
index 1e90a2370786..0740b0e504cb 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ViewUtils.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ViewUtils.kt
@@ -18,8 +18,9 @@ package com.android.systemui.shared.clocks
import android.graphics.Rect
import android.view.View
-import com.android.systemui.shared.clocks.VPoint.Companion.center
-import com.android.systemui.shared.clocks.VPointF.Companion.center
+import com.android.systemui.plugins.clocks.VPoint.Companion.center
+import com.android.systemui.plugins.clocks.VPointF
+import com.android.systemui.plugins.clocks.VPointF.Companion.center
object ViewUtils {
fun View.computeLayoutDiff(targetRegion: Rect, isLargeClock: Boolean): VPointF {
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
index 2dc3e2b7af73..ba32ab083063 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
@@ -17,7 +17,6 @@
package com.android.systemui.shared.clocks.view
import android.graphics.Canvas
-import android.graphics.RectF
import android.icu.text.NumberFormat
import android.util.MathUtils.constrainedMap
import android.view.View
@@ -29,14 +28,15 @@ import com.android.app.animation.Interpolators
import com.android.systemui.customization.R
import com.android.systemui.plugins.clocks.ClockFontAxisSetting
import com.android.systemui.plugins.clocks.ClockLogger
+import com.android.systemui.plugins.clocks.VPoint
+import com.android.systemui.plugins.clocks.VPointF
+import com.android.systemui.plugins.clocks.VPointF.Companion.max
+import com.android.systemui.plugins.clocks.VPointF.Companion.times
+import com.android.systemui.plugins.clocks.VRectF
import com.android.systemui.shared.clocks.CanvasUtil.translate
import com.android.systemui.shared.clocks.CanvasUtil.use
import com.android.systemui.shared.clocks.ClockContext
import com.android.systemui.shared.clocks.DigitTranslateAnimator
-import com.android.systemui.shared.clocks.VPoint
-import com.android.systemui.shared.clocks.VPointF
-import com.android.systemui.shared.clocks.VPointF.Companion.max
-import com.android.systemui.shared.clocks.VPointF.Companion.times
import com.android.systemui.shared.clocks.ViewUtils.measuredSize
import java.util.Locale
import kotlin.collections.filterNotNull
@@ -101,7 +101,7 @@ class FlexClockView(clockCtx: ClockContext) : ViewGroup(clockCtx.context) {
updateLocale(Locale.getDefault())
}
- var onViewBoundsChanged: ((RectF) -> Unit)? = null
+ var onViewBoundsChanged: ((VRectF) -> Unit)? = null
private val digitOffsets = mutableMapOf<Int, Float>()
protected fun calculateSize(
@@ -189,13 +189,7 @@ class FlexClockView(clockCtx: ClockContext) : ViewGroup(clockCtx.context) {
fun updateLocation() {
val layoutBounds = this.layoutBounds ?: return
- val bounds =
- RectF(
- layoutBounds.centerX() - measuredWidth / 2f,
- layoutBounds.centerY() - measuredHeight / 2f,
- layoutBounds.centerX() + measuredWidth / 2f,
- layoutBounds.centerY() + measuredHeight / 2f,
- )
+ val bounds = VRectF.fromCenter(layoutBounds.center, this.measuredSize)
setFrame(
bounds.left.roundToInt(),
bounds.top.roundToInt(),
@@ -215,16 +209,11 @@ class FlexClockView(clockCtx: ClockContext) : ViewGroup(clockCtx.context) {
onAnimateDoze = null
}
- private val layoutBounds = RectF()
+ private var layoutBounds = VRectF.ZERO
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
logger.onLayout(changed, left, top, right, bottom)
-
- layoutBounds.left = left.toFloat()
- layoutBounds.top = top.toFloat()
- layoutBounds.right = right.toFloat()
- layoutBounds.bottom = bottom.toFloat()
-
+ layoutBounds = VRectF(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat())
updateChildFrames(isLayout = true)
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
index fae17a5321ff..2af25fe339a2 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
@@ -23,7 +23,6 @@ import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.Rect
-import android.graphics.RectF
import android.os.VibrationEffect
import android.text.Layout
import android.text.TextPaint
@@ -45,6 +44,11 @@ import com.android.systemui.plugins.clocks.ClockFontAxisSetting
import com.android.systemui.plugins.clocks.ClockFontAxisSetting.Companion.replace
import com.android.systemui.plugins.clocks.ClockFontAxisSetting.Companion.toFVar
import com.android.systemui.plugins.clocks.ClockLogger
+import com.android.systemui.plugins.clocks.VPoint
+import com.android.systemui.plugins.clocks.VPointF
+import com.android.systemui.plugins.clocks.VPointF.Companion.size
+import com.android.systemui.plugins.clocks.VRectF
+import com.android.systemui.shared.Flags.ambientAod
import com.android.systemui.shared.clocks.CanvasUtil.translate
import com.android.systemui.shared.clocks.CanvasUtil.use
import com.android.systemui.shared.clocks.ClockContext
@@ -52,9 +56,6 @@ import com.android.systemui.shared.clocks.DigitTranslateAnimator
import com.android.systemui.shared.clocks.DimensionParser
import com.android.systemui.shared.clocks.FLEX_CLOCK_ID
import com.android.systemui.shared.clocks.FontTextStyle
-import com.android.systemui.shared.clocks.VPoint
-import com.android.systemui.shared.clocks.VPointF
-import com.android.systemui.shared.clocks.VPointF.Companion.size
import com.android.systemui.shared.clocks.ViewUtils.measuredSize
import com.android.systemui.shared.clocks.ViewUtils.size
import com.android.systemui.shared.clocks.toClockAxisSetting
@@ -65,11 +66,11 @@ import kotlin.math.roundToInt
private val TAG = SimpleDigitalClockTextView::class.simpleName!!
-private fun Paint.getTextBounds(text: CharSequence, result: RectF = RectF()): RectF {
- val rect = Rect()
- this.getTextBounds(text, 0, text.length, rect)
- result.set(rect)
- return result
+private val tempRect = Rect()
+
+private fun Paint.getTextBounds(text: CharSequence): VRectF {
+ this.getTextBounds(text, 0, text.length, tempRect)
+ return VRectF(tempRect)
}
enum class VerticalAlignment {
@@ -142,7 +143,7 @@ open class SimpleDigitalClockTextView(
fidgetFontVariation = buildFidgetVariation(lsFontAxes).toFVar()
}
- var onViewBoundsChanged: ((RectF) -> Unit)? = null
+ var onViewBoundsChanged: ((VRectF) -> Unit)? = null
private val parser = DimensionParser(clockCtx.context)
var maxSingleDigitHeight = -1f
var maxSingleDigitWidth = -1f
@@ -158,13 +159,13 @@ open class SimpleDigitalClockTextView(
private val initThread = Thread.currentThread()
// textBounds is the size of text in LS, which only measures current text in lockscreen style
- var textBounds = RectF()
+ var textBounds = VRectF.ZERO
// prevTextBounds and targetTextBounds are to deal with dozing animation between LS and AOD
// especially for the textView which has different bounds during the animation
// prevTextBounds holds the state we are transitioning from
- private val prevTextBounds = RectF()
+ private var prevTextBounds = VRectF.ZERO
// targetTextBounds holds the state we are interpolating to
- private val targetTextBounds = RectF()
+ private var targetTextBounds = VRectF.ZERO
protected val logger = ClockLogger(this, clockCtx.messageBuffer, this::class.simpleName!!)
get() = field ?: ClockLogger.INIT_LOGGER
@@ -214,8 +215,8 @@ open class SimpleDigitalClockTextView(
lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(lsFontVariation)
typeface = lockScreenPaint.typeface
- lockScreenPaint.getTextBounds(text, textBounds)
- targetTextBounds.set(textBounds)
+ textBounds = lockScreenPaint.getTextBounds(text)
+ targetTextBounds = textBounds
textAnimator.setTextStyle(TextAnimator.Style(fVar = lsFontVariation))
measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
@@ -286,7 +287,7 @@ open class SimpleDigitalClockTextView(
canvas.use {
digitTranslateAnimator?.apply { canvas.translate(currentTranslation) }
canvas.translate(getDrawTranslation(interpBounds))
- if (isLayoutRtl()) canvas.translate(interpBounds.width() - textBounds.width(), 0f)
+ if (isLayoutRtl()) canvas.translate(interpBounds.width - textBounds.width, 0f)
textAnimator.draw(canvas)
}
}
@@ -301,16 +302,12 @@ open class SimpleDigitalClockTextView(
super.setAlpha(alpha)
}
- private val layoutBounds = RectF()
+ private var layoutBounds = VRectF.ZERO
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
logger.onLayout(changed, left, top, right, bottom)
-
- layoutBounds.left = left.toFloat()
- layoutBounds.top = top.toFloat()
- layoutBounds.right = right.toFloat()
- layoutBounds.bottom = bottom.toFloat()
+ layoutBounds = VRectF(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat())
}
override fun invalidate() {
@@ -326,11 +323,11 @@ open class SimpleDigitalClockTextView(
fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
if (!this::textAnimator.isInitialized) return
- logger.animateDoze()
+ logger.animateDoze(isDozing, isAnimated)
textAnimator.setTextStyle(
TextAnimator.Style(
fVar = if (isDozing) aodFontVariation else lsFontVariation,
- color = if (isDozing) AOD_COLOR else lockscreenColor,
+ color = if (isDozing && !ambientAod()) AOD_COLOR else lockscreenColor,
textSize = if (isDozing) aodFontSizePx else lockScreenPaint.textSize,
),
TextAnimator.Animation(
@@ -340,6 +337,11 @@ open class SimpleDigitalClockTextView(
),
)
updateTextBoundsForTextAnimator()
+
+ if (!isAnimated) {
+ requestLayout()
+ (parent as? FlexClockView)?.requestLayout()
+ }
}
fun animateCharge() {
@@ -407,10 +409,10 @@ open class SimpleDigitalClockTextView(
}
fun refreshText() {
- lockScreenPaint.getTextBounds(text, textBounds)
- if (this::textAnimator.isInitialized) {
- textAnimator.textInterpolator.targetPaint.getTextBounds(text, targetTextBounds)
- }
+ textBounds = lockScreenPaint.getTextBounds(text)
+ targetTextBounds =
+ if (!this::textAnimator.isInitialized) textBounds
+ else textAnimator.textInterpolator.targetPaint.getTextBounds(text)
if (layout == null) {
requestLayout()
@@ -431,23 +433,23 @@ open class SimpleDigitalClockTextView(
}
/** Returns the interpolated text bounding rect based on interpolation progress */
- private fun getInterpolatedTextBounds(progress: Float = getInterpolatedProgress()): RectF {
+ private fun getInterpolatedTextBounds(progress: Float = getInterpolatedProgress()): VRectF {
if (progress <= 0f) {
return prevTextBounds
} else if (!textAnimator.isRunning || progress >= 1f) {
return targetTextBounds
}
- return RectF().apply {
- left = lerp(prevTextBounds.left, targetTextBounds.left, progress)
- right = lerp(prevTextBounds.right, targetTextBounds.right, progress)
- top = lerp(prevTextBounds.top, targetTextBounds.top, progress)
- bottom = lerp(prevTextBounds.bottom, targetTextBounds.bottom, progress)
- }
+ return VRectF(
+ left = lerp(prevTextBounds.left, targetTextBounds.left, progress),
+ right = lerp(prevTextBounds.right, targetTextBounds.right, progress),
+ top = lerp(prevTextBounds.top, targetTextBounds.top, progress),
+ bottom = lerp(prevTextBounds.bottom, targetTextBounds.bottom, progress),
+ )
}
private fun computeMeasuredSize(
- interpBounds: RectF,
+ interpBounds: VRectF,
widthMeasureSpec: Int = measuredWidthAndState,
heightMeasureSpec: Int = measuredHeightAndState,
): VPointF {
@@ -460,11 +462,11 @@ open class SimpleDigitalClockTextView(
return VPointF(
when {
mode.x == EXACTLY -> MeasureSpec.getSize(widthMeasureSpec).toFloat()
- else -> interpBounds.width() + 2 * lockScreenPaint.strokeWidth
+ else -> interpBounds.width + 2 * lockScreenPaint.strokeWidth
},
when {
mode.y == EXACTLY -> MeasureSpec.getSize(heightMeasureSpec).toFloat()
- else -> interpBounds.height() + 2 * lockScreenPaint.strokeWidth
+ else -> interpBounds.height + 2 * lockScreenPaint.strokeWidth
},
)
}
@@ -488,44 +490,23 @@ open class SimpleDigitalClockTextView(
}
/** Set the location of the view to match the interpolated text bounds */
- private fun setInterpolatedLocation(measureSize: VPointF): RectF {
- val targetRect = RectF()
- targetRect.apply {
- when (xAlignment) {
- XAlignment.LEFT -> {
- left = layoutBounds.left
- right = layoutBounds.left + measureSize.x
- }
- XAlignment.CENTER -> {
- left = layoutBounds.centerX() - measureSize.x / 2f
- right = layoutBounds.centerX() + measureSize.x / 2f
- }
- XAlignment.RIGHT -> {
- left = layoutBounds.right - measureSize.x
- right = layoutBounds.right
- }
- }
-
- when (verticalAlignment) {
- VerticalAlignment.TOP -> {
- top = layoutBounds.top
- bottom = layoutBounds.top + measureSize.y
- }
- VerticalAlignment.CENTER -> {
- top = layoutBounds.centerY() - measureSize.y / 2f
- bottom = layoutBounds.centerY() + measureSize.y / 2f
- }
- VerticalAlignment.BOTTOM -> {
- top = layoutBounds.bottom - measureSize.y
- bottom = layoutBounds.bottom
- }
- VerticalAlignment.BASELINE -> {
- top = layoutBounds.centerY() - measureSize.y / 2f
- bottom = layoutBounds.centerY() + measureSize.y / 2f
- }
- }
- }
+ private fun setInterpolatedLocation(measureSize: VPointF): VRectF {
+ val pos =
+ VPointF(
+ when (xAlignment) {
+ XAlignment.LEFT -> layoutBounds.left
+ XAlignment.CENTER -> layoutBounds.center.x - measureSize.x / 2f
+ XAlignment.RIGHT -> layoutBounds.right - measureSize.x
+ },
+ when (verticalAlignment) {
+ VerticalAlignment.TOP -> layoutBounds.top
+ VerticalAlignment.CENTER -> layoutBounds.center.y - measureSize.y / 2f
+ VerticalAlignment.BOTTOM -> layoutBounds.bottom - measureSize.y
+ VerticalAlignment.BASELINE -> layoutBounds.center.y - measureSize.y / 2f
+ },
+ )
+ val targetRect = VRectF.fromTopLeft(pos, measureSize)
setFrame(
targetRect.left.roundToInt(),
targetRect.top.roundToInt(),
@@ -536,7 +517,7 @@ open class SimpleDigitalClockTextView(
return targetRect
}
- private fun getDrawTranslation(interpBounds: RectF): VPointF {
+ private fun getDrawTranslation(interpBounds: VRectF): VPointF {
val sizeDiff = this.measuredSize - interpBounds.size
val alignment =
VPointF(
@@ -585,11 +566,11 @@ open class SimpleDigitalClockTextView(
if (fontSizePx > 0) {
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSizePx)
lockScreenPaint.textSize = textSize
- lockScreenPaint.getTextBounds(text, textBounds)
- targetTextBounds.set(textBounds)
+ textBounds = lockScreenPaint.getTextBounds(text)
+ targetTextBounds = textBounds
}
if (!constrainedByHeight) {
- val lastUnconstrainedHeight = textBounds.height() + lockScreenPaint.strokeWidth * 2
+ val lastUnconstrainedHeight = textBounds.height + lockScreenPaint.strokeWidth * 2
fontSizeAdjustFactor = lastUnconstrainedHeight / lastUnconstrainedTextSize
}
@@ -607,8 +588,8 @@ open class SimpleDigitalClockTextView(
for (i in 0..9) {
val rectForCalculate = lockScreenPaint.getTextBounds("$i")
- maxSingleDigitHeight = max(maxSingleDigitHeight, rectForCalculate.height())
- maxSingleDigitWidth = max(maxSingleDigitWidth, rectForCalculate.width())
+ maxSingleDigitHeight = max(maxSingleDigitHeight, rectForCalculate.height)
+ maxSingleDigitWidth = max(maxSingleDigitWidth, rectForCalculate.width)
}
maxSingleDigitWidth += 2 * lockScreenPaint.strokeWidth
maxSingleDigitHeight += 2 * lockScreenPaint.strokeWidth
@@ -636,8 +617,8 @@ open class SimpleDigitalClockTextView(
* and targetPaint will store the state we transition to
*/
private fun updateTextBoundsForTextAnimator() {
- textAnimator.textInterpolator.basePaint.getTextBounds(text, prevTextBounds)
- textAnimator.textInterpolator.targetPaint.getTextBounds(text, targetTextBounds)
+ prevTextBounds = textAnimator.textInterpolator.basePaint.getTextBounds(text)
+ targetTextBounds = textAnimator.textInterpolator.targetPaint.getTextBounds(text)
}
/**
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
index e26e19d27417..29647cd082b1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
@@ -16,12 +16,18 @@
package com.android.keyguard;
+import static com.android.internal.widget.flags.Flags.FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
+import android.hardware.input.InputManager;
+import android.platform.test.annotations.EnableFlags;
import android.testing.TestableLooper.RunWithLooper;
import android.view.View;
import android.view.ViewGroup;
@@ -90,6 +96,8 @@ public class KeyguardPinBasedInputViewControllerTest extends SysuiTestCase {
@Mock
private UserActivityNotifier mUserActivityNotifier;
private NumPadKey[] mButtons = new NumPadKey[]{};
+ @Mock
+ private InputManager mInputManager;
private KeyguardPinBasedInputViewController mKeyguardPinViewController;
@@ -118,12 +126,13 @@ public class KeyguardPinBasedInputViewControllerTest extends SysuiTestCase {
new KeyguardKeyboardInteractor(new FakeKeyboardRepository());
FakeFeatureFlags featureFlags = new FakeFeatureFlags();
mSetFlagsRule.enableFlags(com.android.systemui.Flags.FLAG_REVAMPED_BOUNCER_MESSAGES);
+ when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(false);
mKeyguardPinViewController = new KeyguardPinBasedInputViewController(mPinBasedInputView,
mKeyguardUpdateMonitor, mSecurityMode, mLockPatternUtils, mKeyguardSecurityCallback,
mKeyguardMessageAreaControllerFactory, mLatencyTracker,
mEmergencyButtonController, mFalsingCollector, featureFlags,
mSelectedUserInteractor, keyguardKeyboardInteractor, mBouncerHapticPlayer,
- mUserActivityNotifier) {
+ mUserActivityNotifier, mInputManager) {
@Override
public void onResume(int reason) {
super.onResume(reason);
@@ -148,4 +157,112 @@ public class KeyguardPinBasedInputViewControllerTest extends SysuiTestCase {
mKeyguardPinViewController.resetState();
verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pin);
}
+
+ @Test
+ @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT)
+ public void updateAnimations_addDevice_notKeyboard() {
+ when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(false);
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ mKeyguardPinViewController.onViewAttached();
+ mKeyguardPinViewController.onInputDeviceAdded(1);
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ }
+
+ @Test
+ @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT)
+ public void updateAnimations_addDevice_keyboard() {
+ when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(true);
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ mKeyguardPinViewController.onViewAttached();
+ mKeyguardPinViewController.onInputDeviceAdded(1);
+ verify(mPasswordEntry, times(1)).setShowPassword(false);
+ }
+
+ @Test
+ @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT)
+ public void updateAnimations_addDevice_multipleKeyboards() {
+ when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(true);
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ mKeyguardPinViewController.onViewAttached();
+ mKeyguardPinViewController.onInputDeviceAdded(1);
+ verify(mPasswordEntry, times(1)).setShowPassword(false);
+ mKeyguardPinViewController.onInputDeviceAdded(1);
+ verify(mPasswordEntry, times(1)).setShowPassword(false);
+ }
+
+ @Test
+ @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT)
+ public void updateAnimations_removeDevice_notKeyboard() {
+ when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(false);
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ mKeyguardPinViewController.onViewAttached();
+ mKeyguardPinViewController.onInputDeviceRemoved(1);
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ }
+
+ @Test
+ @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT)
+ public void updateAnimations_removeDevice_keyboard() {
+ when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(true, false);
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ verify(mPasswordEntry, times(0)).setShowPassword(false);
+ mKeyguardPinViewController.onViewAttached();
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ verify(mPasswordEntry, times(1)).setShowPassword(false);
+ mKeyguardPinViewController.onInputDeviceRemoved(1);
+ verify(mPasswordEntry, times(2)).setShowPassword(true);
+ verify(mPasswordEntry, times(1)).setShowPassword(false);
+ }
+
+ @Test
+ @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT)
+ public void updateAnimations_removeDevice_multipleKeyboards() {
+ when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(true, true);
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ verify(mPasswordEntry, times(0)).setShowPassword(false);
+ mKeyguardPinViewController.onViewAttached();
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ verify(mPasswordEntry, times(1)).setShowPassword(false);
+ mKeyguardPinViewController.onInputDeviceRemoved(1);
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ verify(mPasswordEntry, times(1)).setShowPassword(false);
+ }
+
+ @Test
+ @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT)
+ public void updateAnimations_updateDevice_notKeyboard() {
+ when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(false);
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ mKeyguardPinViewController.onViewAttached();
+ mKeyguardPinViewController.onInputDeviceChanged(1);
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ }
+
+ @Test
+ @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT)
+ public void updateAnimations_updateDevice_keyboard() {
+ when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(true, false);
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ verify(mPasswordEntry, times(0)).setShowPassword(false);
+ mKeyguardPinViewController.onViewAttached();
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ verify(mPasswordEntry, times(1)).setShowPassword(false);
+ mKeyguardPinViewController.onInputDeviceChanged(1);
+ verify(mPasswordEntry, times(2)).setShowPassword(true);
+ verify(mPasswordEntry, times(1)).setShowPassword(false);
+ }
+
+ @Test
+ @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT)
+ public void updateAnimations_updateDevice_multipleKeyboards() {
+ when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(true, true);
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ verify(mPasswordEntry, times(0)).setShowPassword(false);
+ mKeyguardPinViewController.onViewAttached();
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ verify(mPasswordEntry, times(1)).setShowPassword(false);
+ mKeyguardPinViewController.onInputDeviceChanged(1);
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ verify(mPasswordEntry, times(1)).setShowPassword(false);
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
index 142a2868ec14..7fea06ec7f41 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
@@ -16,6 +16,7 @@
package com.android.keyguard
+import android.hardware.input.InputManager
import android.testing.TestableLooper
import android.view.View
import android.view.ViewGroup
@@ -104,6 +105,7 @@ class KeyguardPinViewControllerTest : SysuiTestCase() {
@Mock lateinit var enterButton: View
@Mock lateinit var uiEventLogger: UiEventLogger
@Mock lateinit var mUserActivityNotifier: UserActivityNotifier
+ @Mock lateinit var inputManager: InputManager
@Captor lateinit var postureCallbackCaptor: ArgumentCaptor<DevicePostureController.Callback>
@@ -154,6 +156,7 @@ class KeyguardPinViewControllerTest : SysuiTestCase() {
keyguardKeyboardInteractor,
kosmos.bouncerHapticPlayer,
mUserActivityNotifier,
+ inputManager,
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
index c751a7db51dc..003669da498e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
@@ -16,6 +16,7 @@
package com.android.keyguard
+import android.hardware.input.InputManager
import android.telephony.TelephonyManager
import android.testing.TestableLooper
import android.view.LayoutInflater
@@ -73,6 +74,7 @@ class KeyguardSimPinViewControllerTest : SysuiTestCase() {
@Mock private lateinit var mUserActivityNotifier: UserActivityNotifier
private val updateMonitorCallbackArgumentCaptor =
ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java)
+ @Mock private lateinit var inputManager: InputManager
private val kosmos = testKosmos()
@@ -107,6 +109,7 @@ class KeyguardSimPinViewControllerTest : SysuiTestCase() {
keyguardKeyboardInteractor,
kosmos.bouncerHapticPlayer,
mUserActivityNotifier,
+ inputManager,
)
underTest.init()
underTest.onViewAttached()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
index c34682551eda..85cb388ace5c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
@@ -16,6 +16,7 @@
package com.android.keyguard
+import android.hardware.input.InputManager
import android.telephony.PinResult
import android.telephony.TelephonyManager
import android.testing.TestableLooper
@@ -65,6 +66,7 @@ class KeyguardSimPukViewControllerTest : SysuiTestCase() {
private lateinit var keyguardMessageAreaController:
KeyguardMessageAreaController<BouncerKeyguardMessageArea>
@Mock private lateinit var mUserActivityNotifier: UserActivityNotifier
+ @Mock private lateinit var inputManager: InputManager
private val kosmos = testKosmos()
@@ -102,6 +104,7 @@ class KeyguardSimPukViewControllerTest : SysuiTestCase() {
keyguardKeyboardInteractor,
kosmos.bouncerHapticPlayer,
mUserActivityNotifier,
+ inputManager,
)
underTest.init()
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ExpandHelperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ExpandHelperTest.java
index 7fb879c02778..f0c9141a76e6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ExpandHelperTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ExpandHelperTest.java
@@ -32,7 +32,7 @@ import androidx.test.filters.SmallTest;
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.systemui.animation.AnimatorTestRule;
import com.android.systemui.flags.FakeFeatureFlagsClassic;
-import com.android.systemui.statusbar.NotificationMediaManager;
+import com.android.systemui.media.NotificationMediaManager;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
index 0b13900d826b..7e4704a6179a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
@@ -71,6 +71,7 @@ import com.android.systemui.bluetooth.qsdialog.DeviceItem;
import com.android.systemui.bluetooth.qsdialog.DeviceItemType;
import com.android.systemui.model.SysUiState;
import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.qs.shared.QSSettingsPackageRepository;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.statusbar.phone.SystemUIDialogManager;
@@ -108,6 +109,7 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {
private static final String TEST_LABEL = "label";
private static final int TEST_PRESET_INDEX = 1;
private static final String TEST_PRESET_NAME = "test_preset";
+ private static final String SETTINGS_PACKAGE_NAME = "com.android.settings";
private final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
@Mock
@@ -137,6 +139,8 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {
@Mock
private HearingDevicesUiEventLogger mUiEventLogger;
@Mock
+ private QSSettingsPackageRepository mQSSettingsPackageRepository;
+ @Mock
private CachedBluetoothDevice mCachedDevice;
@Mock
private BluetoothDevice mDevice;
@@ -164,6 +168,8 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {
when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(List.of(mCachedDevice));
when(mLocalBluetoothManager.getEventManager()).thenReturn(mBluetoothEventManager);
when(mSysUiState.setFlag(anyLong(), anyBoolean())).thenReturn(mSysUiState);
+ when(mQSSettingsPackageRepository.getSettingsPackageName())
+ .thenReturn(SETTINGS_PACKAGE_NAME);
when(mDevice.getBondState()).thenReturn(BOND_BONDED);
when(mDevice.isConnected()).thenReturn(true);
when(mCachedDevice.getDevice()).thenReturn(mDevice);
@@ -195,6 +201,7 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {
anyInt(), any());
assertThat(intentCaptor.getValue().getAction()).isEqualTo(
Settings.ACTION_HEARING_DEVICE_PAIRING_SETTINGS);
+ assertThat(intentCaptor.getValue().getPackage()).isEqualTo(SETTINGS_PACKAGE_NAME);
verify(mUiEventLogger).log(HearingDevicesUiEvent.HEARING_DEVICES_PAIR,
TEST_LAUNCH_SOURCE_ID);
}
@@ -210,6 +217,7 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {
anyInt(), any());
assertThat(intentCaptor.getValue().getAction()).isEqualTo(
HearingDevicesDialogDelegate.ACTION_BLUETOOTH_DEVICE_DETAILS);
+ assertThat(intentCaptor.getValue().getPackage()).isEqualTo(SETTINGS_PACKAGE_NAME);
verify(mUiEventLogger).log(HearingDevicesUiEvent.HEARING_DEVICES_GEAR_CLICK,
TEST_LAUNCH_SOURCE_ID);
}
@@ -392,7 +400,8 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {
mExecutor,
mExecutor,
mAudioManager,
- mUiEventLogger
+ mUiEventLogger,
+ mQSSettingsPackageRepository
);
mDialog = mDialogDelegate.createDialog();
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt
index 8d3640d8d809..53b364c13063 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt
@@ -41,21 +41,9 @@ class FontVariationUtilsTest : SysuiTestCase() {
@Test
fun testStyleValueUnchange_getBlankStr() {
val fontVariationUtils = FontVariationUtils()
- fontVariationUtils.updateFontVariation(
- weight = 100,
- width = 100,
- opticalSize = 0,
- roundness = 100,
- )
- val updatedFvar1 =
- fontVariationUtils.updateFontVariation(
- weight = 100,
- width = 100,
- opticalSize = 0,
- roundness = 100,
- )
- Assert.assertEquals("", updatedFvar1)
- val updatedFvar2 = fontVariationUtils.updateFontVariation()
- Assert.assertEquals("", updatedFvar2)
+ Assert.assertEquals("", fontVariationUtils.updateFontVariation())
+ val fVar = fontVariationUtils.updateFontVariation(weight = 100)
+ Assert.assertEquals(fVar, fontVariationUtils.updateFontVariation())
+ Assert.assertEquals(fVar, fontVariationUtils.updateFontVariation(weight = 100))
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt
index 25a287c4cfff..15a6de896e92 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt
@@ -247,20 +247,20 @@ class PinInputViewModelTest : SysuiTestCase() {
}
private class PinInputSubject
-private constructor(metadata: FailureMetadata, private val actual: PinInputViewModel) :
+private constructor(metadata: FailureMetadata, private val actual: PinInputViewModel?) :
Subject(metadata, actual) {
fun matches(mnemonics: String) {
val actualMnemonics =
- actual.input
- .map { entry ->
+ actual?.input
+ ?.map { entry ->
when (entry) {
is Digit -> entry.input.digitToChar()
is ClearAll -> 'C'
else -> throw IllegalArgumentException()
}
}
- .joinToString(separator = "")
+ ?.joinToString(separator = "")
if (mnemonics != actualMnemonics) {
failWithActual(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandlerTest.kt
index 0f400892f988..56b06de0a9ba 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandlerTest.kt
@@ -17,14 +17,12 @@
package com.android.systemui.common.ui.view
+import android.testing.TestableLooper
+import android.view.MotionEvent
import android.view.ViewConfiguration
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.ui.view.TouchHandlingViewInteractionHandler.MotionEventModel
-import com.android.systemui.common.ui.view.TouchHandlingViewInteractionHandler.MotionEventModel.Down
-import com.android.systemui.common.ui.view.TouchHandlingViewInteractionHandler.MotionEventModel.Move
-import com.android.systemui.common.ui.view.TouchHandlingViewInteractionHandler.MotionEventModel.Up
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
@@ -33,18 +31,22 @@ import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito.never
+import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
class TouchHandlingViewInteractionHandlerTest : SysuiTestCase() {
@Mock private lateinit var postDelayed: (Runnable, Long) -> DisposableHandle
@Mock private lateinit var onLongPressDetected: (Int, Int) -> Unit
@Mock private lateinit var onSingleTapDetected: (Int, Int) -> Unit
+ @Mock private lateinit var onDoubleTapDetected: () -> Unit
private lateinit var underTest: TouchHandlingViewInteractionHandler
@@ -61,14 +63,17 @@ class TouchHandlingViewInteractionHandlerTest : SysuiTestCase() {
underTest =
TouchHandlingViewInteractionHandler(
+ context = context,
postDelayed = postDelayed,
isAttachedToWindow = { isAttachedToWindow },
onLongPressDetected = onLongPressDetected,
onSingleTapDetected = onSingleTapDetected,
+ onDoubleTapDetected = onDoubleTapDetected,
longPressDuration = { ViewConfiguration.getLongPressTimeout().toLong() },
allowedTouchSlop = ViewConfiguration.getTouchSlop(),
)
underTest.isLongPressHandlingEnabled = true
+ underTest.isDoubleTapHandlingEnabled = true
}
@Test
@@ -76,63 +81,250 @@ class TouchHandlingViewInteractionHandlerTest : SysuiTestCase() {
val downX = 123
val downY = 456
dispatchTouchEvents(
- Down(x = downX, y = downY),
- Move(distanceMoved = ViewConfiguration.getTouchSlop() - 0.1f),
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+ MotionEvent.obtain(
+ 0L,
+ 0L,
+ MotionEvent.ACTION_MOVE,
+ 123f + ViewConfiguration.getTouchSlop() - 0.1f,
+ 456f,
+ 0,
+ ),
)
delayedRunnable?.run()
verify(onLongPressDetected).invoke(downX, downY)
- verify(onSingleTapDetected, never()).invoke(any(), any())
+ verify(onSingleTapDetected, never()).invoke(anyInt(), anyInt())
}
@Test
fun longPressButFeatureNotEnabled() = runTest {
underTest.isLongPressHandlingEnabled = false
- dispatchTouchEvents(Down(x = 123, y = 456))
+ dispatchTouchEvents(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0))
assertThat(delayedRunnable).isNull()
- verify(onLongPressDetected, never()).invoke(any(), any())
- verify(onSingleTapDetected, never()).invoke(any(), any())
+ verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+ verify(onSingleTapDetected, never()).invoke(anyInt(), anyInt())
}
@Test
fun longPressButViewNotAttached() = runTest {
isAttachedToWindow = false
- dispatchTouchEvents(Down(x = 123, y = 456))
+ dispatchTouchEvents(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0))
delayedRunnable?.run()
- verify(onLongPressDetected, never()).invoke(any(), any())
- verify(onSingleTapDetected, never()).invoke(any(), any())
+ verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+ verify(onSingleTapDetected, never()).invoke(anyInt(), anyInt())
}
@Test
fun draggedTooFarToBeConsideredAlongPress() = runTest {
dispatchTouchEvents(
- Down(x = 123, y = 456),
- Move(distanceMoved = ViewConfiguration.getTouchSlop() + 0.1f),
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123F, 456F, 0),
+ // Drag action within touch slop
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 123f, 456f, 0).apply {
+ addBatch(0L, 123f + ViewConfiguration.getTouchSlop() + 0.1f, 456f, 0f, 0f, 0)
+ },
)
assertThat(delayedRunnable).isNull()
- verify(onLongPressDetected, never()).invoke(any(), any())
- verify(onSingleTapDetected, never()).invoke(any(), any())
+ verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+ verify(onSingleTapDetected, never()).invoke(anyInt(), anyInt())
}
@Test
fun heldDownTooBrieflyToBeConsideredAlongPress() = runTest {
dispatchTouchEvents(
- Down(x = 123, y = 456),
- Up(
- distanceMoved = ViewConfiguration.getTouchSlop().toFloat(),
- gestureDuration = ViewConfiguration.getLongPressTimeout() - 1L,
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+ MotionEvent.obtain(
+ 0L,
+ ViewConfiguration.getLongPressTimeout() - 1L,
+ MotionEvent.ACTION_UP,
+ 123f,
+ 456F,
+ 0,
),
)
assertThat(delayedRunnable).isNull()
- verify(onLongPressDetected, never()).invoke(any(), any())
+ verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
verify(onSingleTapDetected).invoke(123, 456)
}
- private fun dispatchTouchEvents(vararg models: MotionEventModel) {
- models.forEach { model -> underTest.onTouchEvent(model) }
+ @Test
+ fun doubleTap() = runTest {
+ val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L
+ dispatchTouchEvents(
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 123f, 456f, 0),
+ MotionEvent.obtain(
+ secondTapTime,
+ secondTapTime,
+ MotionEvent.ACTION_DOWN,
+ 123f,
+ 456f,
+ 0,
+ ),
+ MotionEvent.obtain(secondTapTime, secondTapTime, MotionEvent.ACTION_UP, 123f, 456f, 0),
+ )
+
+ verify(onDoubleTapDetected).invoke()
+ assertThat(delayedRunnable).isNull()
+ verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+ verify(onSingleTapDetected, times(2)).invoke(anyInt(), anyInt())
+ }
+
+ @Test
+ fun doubleTapButFeatureNotEnabled() = runTest {
+ underTest.isDoubleTapHandlingEnabled = false
+
+ val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L
+ dispatchTouchEvents(
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 123f, 456f, 0),
+ MotionEvent.obtain(
+ secondTapTime,
+ secondTapTime,
+ MotionEvent.ACTION_DOWN,
+ 123f,
+ 456f,
+ 0,
+ ),
+ MotionEvent.obtain(secondTapTime, secondTapTime, MotionEvent.ACTION_UP, 123f, 456f, 0),
+ )
+
+ verify(onDoubleTapDetected, never()).invoke()
+ assertThat(delayedRunnable).isNull()
+ verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+ verify(onSingleTapDetected, times(2)).invoke(anyInt(), anyInt())
+ }
+
+ @Test
+ fun tapIntoLongPress() = runTest {
+ val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L
+ dispatchTouchEvents(
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 123f, 456f, 0),
+ MotionEvent.obtain(
+ secondTapTime,
+ secondTapTime,
+ MotionEvent.ACTION_DOWN,
+ 123f,
+ 456f,
+ 0,
+ ),
+ MotionEvent.obtain(
+ secondTapTime + ViewConfiguration.getLongPressTimeout() + 1L,
+ secondTapTime + ViewConfiguration.getLongPressTimeout() + 1L,
+ MotionEvent.ACTION_MOVE,
+ 123f + ViewConfiguration.getTouchSlop() - 0.1f,
+ 456f,
+ 0,
+ ),
+ )
+ delayedRunnable?.run()
+
+ verify(onDoubleTapDetected, never()).invoke()
+ verify(onSingleTapDetected).invoke(anyInt(), anyInt())
+ verify(onLongPressDetected).invoke(anyInt(), anyInt())
+ }
+
+ @Test
+ fun tapIntoDownHoldTooBrieflyToBeConsideredLongPress() = runTest {
+ val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L
+ dispatchTouchEvents(
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+ MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 123f, 456f, 0),
+ MotionEvent.obtain(
+ secondTapTime,
+ secondTapTime,
+ MotionEvent.ACTION_DOWN,
+ 123f,
+ 456f,
+ 0,
+ ),
+ MotionEvent.obtain(
+ secondTapTime + ViewConfiguration.getLongPressTimeout() + 1L,
+ secondTapTime + ViewConfiguration.getLongPressTimeout() + 1L,
+ MotionEvent.ACTION_UP,
+ 123f,
+ 456f,
+ 0,
+ ),
+ )
+ delayedRunnable?.run()
+
+ verify(onDoubleTapDetected, never()).invoke()
+ verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+ verify(onSingleTapDetected, times(2)).invoke(anyInt(), anyInt())
+ }
+
+ @Test
+ fun tapIntoDrag() = runTest {
+ val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L
+ dispatchTouchEvents(
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 123f, 456f, 0),
+ MotionEvent.obtain(
+ secondTapTime,
+ secondTapTime,
+ MotionEvent.ACTION_DOWN,
+ 123f,
+ 456f,
+ 0,
+ ),
+ // Drag event within touch slop
+ MotionEvent.obtain(secondTapTime, secondTapTime, MotionEvent.ACTION_MOVE, 123f, 456f, 0)
+ .apply {
+ addBatch(
+ secondTapTime,
+ 123f + ViewConfiguration.getTouchSlop() + 0.1f,
+ 456f,
+ 0f,
+ 0f,
+ 0,
+ )
+ },
+ )
+ delayedRunnable?.run()
+
+ verify(onDoubleTapDetected, never()).invoke()
+ verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+ verify(onSingleTapDetected).invoke(anyInt(), anyInt())
+ }
+
+ @Test
+ fun doubleTapOutOfAllowableSlop() = runTest {
+ val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L
+ val scaledDoubleTapSlop = ViewConfiguration.get(context).scaledDoubleTapSlop
+ dispatchTouchEvents(
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 123f, 456f, 0),
+ MotionEvent.obtain(
+ secondTapTime,
+ secondTapTime,
+ MotionEvent.ACTION_DOWN,
+ 123f + scaledDoubleTapSlop + 0.1f,
+ 456f + scaledDoubleTapSlop + 0.1f,
+ 0,
+ ),
+ MotionEvent.obtain(
+ secondTapTime,
+ secondTapTime,
+ MotionEvent.ACTION_UP,
+ 123f + scaledDoubleTapSlop + 0.1f,
+ 456f + scaledDoubleTapSlop + 0.1f,
+ 0,
+ ),
+ )
+
+ verify(onDoubleTapDetected, never()).invoke()
+ assertThat(delayedRunnable).isNull()
+ verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+ verify(onSingleTapDetected, times(2)).invoke(anyInt(), anyInt())
+ }
+
+ private fun dispatchTouchEvents(vararg events: MotionEvent) {
+ events.forEach { event -> underTest.onTouchEvent(event) }
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt
index e0515000b232..454c15667f22 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt
@@ -56,6 +56,7 @@ import com.android.systemui.statusbar.phone.dozeScrimController
import com.android.systemui.statusbar.phone.screenOffAnimationController
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
@@ -105,7 +106,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() {
@Test
fun nonPowerButtonFPS_vibrateSuccess() =
testScope.runTest {
- val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic)
+ val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry)
enrollFingerprint(FingerprintSensorType.UDFPS_ULTRASONIC)
runCurrent()
enterDeviceFromFingerprintUnlockLegacy()
@@ -116,7 +117,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() {
@Test
fun powerButtonFPS_vibrateSuccess() =
testScope.runTest {
- val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic)
+ val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry)
enrollFingerprint(FingerprintSensorType.POWER_BUTTON)
kosmos.fakeKeyEventRepository.setPowerButtonDown(false)
@@ -133,7 +134,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() {
@Test
fun powerButtonFPS_powerDown_doNotVibrateSuccess() =
testScope.runTest {
- val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic)
+ val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry)
enrollFingerprint(FingerprintSensorType.POWER_BUTTON)
kosmos.fakeKeyEventRepository.setPowerButtonDown(true) // power button is currently DOWN
@@ -150,7 +151,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() {
@Test
fun powerButtonFPS_powerButtonRecentlyPressed_doNotVibrateSuccess() =
testScope.runTest {
- val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic)
+ val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry)
enrollFingerprint(FingerprintSensorType.POWER_BUTTON)
kosmos.fakeKeyEventRepository.setPowerButtonDown(false)
@@ -174,14 +175,14 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() {
}
@Test
- fun nonPowerButtonFPS_coExFaceFailure_vibrateError() =
+ fun nonPowerButtonFPS_coExFaceFailure_doNotVibrateError() =
testScope.runTest {
val playErrorHaptic by collectLastValue(underTest.playErrorHaptic)
enrollFingerprint(FingerprintSensorType.UDFPS_ULTRASONIC)
enrollFace()
runCurrent()
faceFailure()
- assertThat(playErrorHaptic).isNotNull()
+ assertThat(playErrorHaptic).isNull()
}
@Test
@@ -211,7 +212,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() {
testScope.runTest {
kosmos.configureKeyguardBypass(isBypassAvailable = false)
underTest = kosmos.deviceEntryHapticsInteractor
- val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic)
+ val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry)
enrollFingerprint(FingerprintSensorType.UDFPS_ULTRASONIC)
runCurrent()
configureDeviceEntryFromBiometricSource(isFpUnlock = true)
@@ -225,7 +226,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() {
testScope.runTest {
kosmos.configureKeyguardBypass(isBypassAvailable = false)
underTest = kosmos.deviceEntryHapticsInteractor
- val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic)
+ val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry)
enrollFingerprint(FingerprintSensorType.POWER_BUTTON)
kosmos.fakeKeyEventRepository.setPowerButtonDown(false)
@@ -246,18 +247,19 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() {
enrollFace()
kosmos.configureKeyguardBypass(isBypassAvailable = true)
underTest = kosmos.deviceEntryHapticsInteractor
- val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic)
+ val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry)
configureDeviceEntryFromBiometricSource(isFaceUnlock = true)
verifyDeviceEntryFromFaceAuth()
assertThat(playSuccessHaptic).isNotNull()
}
+ @OptIn(ExperimentalCoroutinesApi::class)
@EnableSceneContainer
@Test
- fun playSuccessHaptic_onFaceAuthSuccess_whenBypassDisabled_sceneContainer() =
+ fun skipSuccessHaptic_onFaceAuthSuccess_whenBypassDisabled_sceneContainer() =
testScope.runTest {
underTest = kosmos.deviceEntryHapticsInteractor
- val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic)
+ val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry)
enrollFace()
kosmos.configureKeyguardBypass(isBypassAvailable = false)
@@ -265,7 +267,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() {
configureDeviceEntryFromBiometricSource(isFaceUnlock = true, bypassEnabled = false)
kosmos.fakeDeviceEntryFaceAuthRepository.isAuthenticated.value = true
- assertThat(playSuccessHaptic).isNotNull()
+ assertThat(playSuccessHaptic).isNull()
}
@EnableSceneContainer
@@ -274,7 +276,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() {
testScope.runTest {
kosmos.configureKeyguardBypass(isBypassAvailable = false)
underTest = kosmos.deviceEntryHapticsInteractor
- val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic)
+ val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry)
enrollFingerprint(FingerprintSensorType.POWER_BUTTON)
// power button is currently DOWN
kosmos.fakeKeyEventRepository.setPowerButtonDown(true)
@@ -295,7 +297,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() {
testScope.runTest {
kosmos.configureKeyguardBypass(isBypassAvailable = false)
underTest = kosmos.deviceEntryHapticsInteractor
- val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic)
+ val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry)
enrollFingerprint(FingerprintSensorType.POWER_BUTTON)
kosmos.fakeKeyEventRepository.setPowerButtonDown(false)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt
index f37306276848..efc68f3b6884 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt
@@ -57,4 +57,17 @@ class KeyEventInteractorTest : SysuiTestCase() {
repository.setPowerButtonDown(true)
assertThat(isPowerDown).isTrue()
}
+
+ @Test
+ fun testPowerButtonBeingLongPressedInteractor() =
+ runTest {
+ val isPowerButtonLongPressed by collectLastValue(
+ underTest.isPowerButtonLongPressed)
+
+ repository.setPowerButtonLongPressed(false)
+ assertThat(isPowerButtonLongPressed).isFalse()
+
+ repository.setPowerButtonLongPressed(true)
+ assertThat(isPowerButtonLongPressed).isTrue()
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java
index 909acca12551..573216dd680a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java
@@ -49,9 +49,9 @@ import androidx.test.filters.SmallTest;
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.systemui.SystemUIInitializerImpl;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.media.NotificationMediaManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.settings.UserTracker;
-import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
@@ -177,7 +177,8 @@ public class KeyguardSliceProviderTest extends SysuiTestCase {
@Test
public void schedulesAlarm12hBefore() {
long in16Hours = System.currentTimeMillis() + TimeUnit.HOURS.toHours(16);
- AlarmManager.AlarmClockInfo alarmClockInfo = new AlarmManager.AlarmClockInfo(in16Hours, null);
+ AlarmManager.AlarmClockInfo alarmClockInfo = new AlarmManager.AlarmClockInfo(in16Hours,
+ null);
mProvider.onNextAlarmChanged(alarmClockInfo);
long twelveHours = TimeUnit.HOURS.toMillis(KeyguardSliceProvider.ALARM_VISIBILITY_HOURS);
@@ -189,7 +190,8 @@ public class KeyguardSliceProviderTest extends SysuiTestCase {
@Test
public void updatingNextAlarmInvalidatesSlice() {
long in16Hours = System.currentTimeMillis() + TimeUnit.HOURS.toHours(8);
- AlarmManager.AlarmClockInfo alarmClockInfo = new AlarmManager.AlarmClockInfo(in16Hours, null);
+ AlarmManager.AlarmClockInfo alarmClockInfo = new AlarmManager.AlarmClockInfo(in16Hours,
+ null);
mProvider.onNextAlarmChanged(alarmClockInfo);
verify(mContentResolver).notifyChange(eq(mProvider.getUri()), eq(null));
@@ -204,7 +206,7 @@ public class KeyguardSliceProviderTest extends SysuiTestCase {
@Test
public void addZenMode_addedToSlice() {
ListBuilder listBuilder = spy(new ListBuilder(getContext(), mProvider.getUri(),
- ListBuilder.INFINITY));
+ ListBuilder.INFINITY));
mProvider.addZenModeLocked(listBuilder);
verify(listBuilder, never()).addRow(any(ListBuilder.RowBuilder.class));
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyEventRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyEventRepositoryTest.kt
index c7f1525e2946..9ab5f8948ecc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyEventRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyEventRepositoryTest.kt
@@ -25,6 +25,7 @@ import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyevent.data.repository.KeyEventRepositoryImpl
import com.android.systemui.statusbar.CommandQueue
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@@ -34,10 +35,10 @@ import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
-import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class KeyEventRepositoryTest : SysuiTestCase() {
@@ -62,6 +63,15 @@ class KeyEventRepositoryTest : SysuiTestCase() {
}
@Test
+ fun isPowerButtonBeingLongPressed_initialValueFalse() =
+ testScope.runTest {
+ val isPowerButtonLongPressed by collectLastValue(
+ underTest.isPowerButtonLongPressed)
+ runCurrent()
+ assertThat(isPowerButtonLongPressed).isFalse()
+ }
+
+ @Test
fun isPowerButtonDown_onChange() =
testScope.runTest {
val isPowerButtonDown by collectLastValue(underTest.isPowerButtonDown)
@@ -77,4 +87,54 @@ class KeyEventRepositoryTest : SysuiTestCase() {
)
assertThat(isPowerButtonDown).isFalse()
}
+
+
+ @Test
+ fun isPowerButtonBeingLongPressed_onPowerButtonDown() =
+ testScope.runTest {
+ val isPowerButtonLongPressed by collectLastValue(
+ underTest.isPowerButtonLongPressed)
+
+ runCurrent()
+
+ verify(commandQueue).addCallback(commandQueueCallbacks.capture())
+
+ val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_POWER)
+ commandQueueCallbacks.value.handleSystemKey(keyEvent)
+
+ assertThat(isPowerButtonLongPressed).isFalse()
+ }
+
+ @Test
+ fun isPowerButtonBeingLongPressed_onPowerButtonUp() =
+ testScope.runTest {
+ val isPowerButtonLongPressed by collectLastValue(
+ underTest.isPowerButtonLongPressed)
+
+ runCurrent()
+
+ verify(commandQueue).addCallback(commandQueueCallbacks.capture())
+
+ val keyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_POWER)
+ commandQueueCallbacks.value.handleSystemKey(keyEvent)
+
+ assertThat(isPowerButtonLongPressed).isFalse()
+ }
+
+ @Test
+ fun isPowerButtonBeingLongPressed_onPowerButtonDown_longPressFlagSet() =
+ testScope.runTest {
+ val isPowerButtonBeingLongPressed by collectLastValue(
+ underTest.isPowerButtonLongPressed)
+
+ runCurrent()
+
+ verify(commandQueue).addCallback(commandQueueCallbacks.capture())
+
+ val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_POWER)
+ keyEvent.setFlags(KeyEvent.FLAG_LONG_PRESS)
+ commandQueueCallbacks.value.handleSystemKey(keyEvent)
+
+ assertThat(isPowerButtonBeingLongPressed).isTrue()
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractorTest.kt
index 2558d583b001..89a53f5722ac 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractorTest.kt
@@ -49,6 +49,7 @@ import org.mockito.kotlin.verify
@SmallTest
@RunWith(AndroidJUnit4::class)
+@EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
class KeyguardStateCallbackInteractorTest : SysuiTestCase() {
private val kosmos = testKosmos()
@@ -81,7 +82,6 @@ class KeyguardStateCallbackInteractorTest : SysuiTestCase() {
}
@Test
- @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
fun test_lockscreenVisibility_notifyDismissSucceeded_ifNotVisible() =
testScope.runTest {
underTest.addCallback(callback)
@@ -109,7 +109,6 @@ class KeyguardStateCallbackInteractorTest : SysuiTestCase() {
}
@Test
- @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
fun test_lockscreenVisibility_reportsKeyguardShowingChanged() =
testScope.runTest {
underTest.addCallback(callback)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt
index e203a276a2f2..1dddfc1bba9c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt
@@ -18,11 +18,17 @@
package com.android.systemui.keyguard.domain.interactor
import android.content.Intent
+import android.os.PowerManager
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.provider.Settings
import android.view.accessibility.accessibilityManagerWrapper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.internal.logging.uiEventLogger
+import com.android.systemui.Flags.FLAG_DOUBLE_TAP_TO_SLEEP
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor
@@ -39,6 +45,8 @@ import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository
+import com.android.systemui.util.time.fakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.advanceTimeBy
@@ -46,14 +54,19 @@ 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.Mock
import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidJUnit4::class)
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
class KeyguardTouchHandlingInteractorTest : SysuiTestCase() {
private val kosmos =
testKosmos().apply {
@@ -61,17 +74,23 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() {
this.uiEventLogger = mock<UiEventLoggerFake>()
}
+ @get:Rule val setFlagsRule = SetFlagsRule()
+
private lateinit var underTest: KeyguardTouchHandlingInteractor
private val logger = kosmos.uiEventLogger
private val testScope = kosmos.testScope
private val keyguardRepository = kosmos.fakeKeyguardRepository
private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+ private val secureSettingsRepository = kosmos.userAwareSecureSettingsRepository
+
+ @Mock private lateinit var powerManager: PowerManager
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
overrideResource(R.bool.long_press_keyguard_customize_lockscreen_enabled, true)
+ overrideResource(com.android.internal.R.bool.config_supportDoubleTapSleep, true)
whenever(kosmos.accessibilityManagerWrapper.getRecommendedTimeoutMillis(anyInt(), anyInt()))
.thenAnswer { it.arguments[0] }
@@ -80,13 +99,13 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() {
@After
fun tearDown() {
- mContext
- .getOrCreateTestableResources()
- .removeOverride(R.bool.long_press_keyguard_customize_lockscreen_enabled)
+ val testableResource = mContext.getOrCreateTestableResources()
+ testableResource.removeOverride(R.bool.long_press_keyguard_customize_lockscreen_enabled)
+ testableResource.removeOverride(com.android.internal.R.bool.config_supportDoubleTapSleep)
}
@Test
- fun isEnabled() =
+ fun isLongPressEnabled() =
testScope.runTest {
val isEnabled = collectLastValue(underTest.isLongPressHandlingEnabled)
KeyguardState.values().forEach { keyguardState ->
@@ -101,7 +120,7 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() {
}
@Test
- fun isEnabled_alwaysFalseWhenQuickSettingsAreVisible() =
+ fun isLongPressEnabled_alwaysFalseWhenQuickSettingsAreVisible() =
testScope.runTest {
val isEnabled = collectLastValue(underTest.isLongPressHandlingEnabled)
KeyguardState.values().forEach { keyguardState ->
@@ -112,7 +131,7 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() {
}
@Test
- fun isEnabled_alwaysFalseWhenConfigEnabledBooleanIsFalse() =
+ fun isLongPressEnabled_alwaysFalseWhenConfigEnabledBooleanIsFalse() =
testScope.runTest {
overrideResource(R.bool.long_press_keyguard_customize_lockscreen_enabled, false)
createUnderTest()
@@ -294,6 +313,119 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() {
assertThat(isMenuVisible).isFalse()
}
+ @Test
+ @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP)
+ fun isDoubleTapEnabled_flagEnabled_userSettingEnabled_onlyTrueInLockScreenState() {
+ testScope.runTest {
+ secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, true)
+
+ val isEnabled = collectLastValue(underTest.isDoubleTapHandlingEnabled)
+ KeyguardState.entries.forEach { keyguardState ->
+ setUpState(keyguardState = keyguardState)
+
+ if (keyguardState == KeyguardState.LOCKSCREEN) {
+ assertThat(isEnabled()).isTrue()
+ } else {
+ assertThat(isEnabled()).isFalse()
+ }
+ }
+ }
+ }
+
+ @Test
+ @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP)
+ fun isDoubleTapEnabled_flagEnabled_userSettingDisabled_alwaysFalse() {
+ testScope.runTest {
+ secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, false)
+
+ val isEnabled = collectLastValue(underTest.isDoubleTapHandlingEnabled)
+ KeyguardState.entries.forEach { keyguardState ->
+ setUpState(keyguardState = keyguardState)
+
+ assertThat(isEnabled()).isFalse()
+ }
+ }
+ }
+
+ @Test
+ @DisableFlags(FLAG_DOUBLE_TAP_TO_SLEEP)
+ fun isDoubleTapEnabled_flagDisabled_userSettingEnabled_alwaysFalse() {
+ testScope.runTest {
+ secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, true)
+
+ val isEnabled = collectLastValue(underTest.isDoubleTapHandlingEnabled)
+ KeyguardState.entries.forEach { keyguardState ->
+ setUpState(keyguardState = keyguardState)
+
+ assertThat(isEnabled()).isFalse()
+ }
+ }
+ }
+
+
+
+ @Test
+ @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP)
+ fun isDoubleTapEnabled_flagEnabledAndConfigDisabled_alwaysFalse() {
+ testScope.runTest {
+ secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, true)
+ overrideResource(com.android.internal.R.bool.config_supportDoubleTapSleep, false)
+ createUnderTest()
+
+ val isEnabled = collectLastValue(underTest.isDoubleTapHandlingEnabled)
+ KeyguardState.entries.forEach { keyguardState ->
+ setUpState(keyguardState = keyguardState)
+
+ assertThat(isEnabled()).isFalse()
+ }
+ }
+ }
+
+ @Test
+ @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP)
+ fun isDoubleTapEnabled_quickSettingsVisible_alwaysFalse() {
+ testScope.runTest {
+ secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, true)
+
+ val isEnabled = collectLastValue(underTest.isDoubleTapHandlingEnabled)
+ KeyguardState.entries.forEach { keyguardState ->
+ setUpState(keyguardState = keyguardState, isQuickSettingsVisible = true)
+
+ assertThat(isEnabled()).isFalse()
+ }
+ }
+ }
+
+ @Test
+ @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP)
+ fun onDoubleClick_doubleTapEnabled() {
+ testScope.runTest {
+ secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, true)
+ val isEnabled by collectLastValue(underTest.isDoubleTapHandlingEnabled)
+ runCurrent()
+
+ underTest.onDoubleClick()
+
+ assertThat(isEnabled).isTrue()
+ verify(powerManager).goToSleep(anyLong())
+ }
+ }
+
+ @Test
+ @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP)
+ fun onDoubleClick_doubleTapDisabled() {
+ testScope.runTest {
+ secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, false)
+ val isEnabled by collectLastValue(underTest.isDoubleTapHandlingEnabled)
+ runCurrent()
+
+ underTest.onDoubleClick()
+
+ assertThat(isEnabled).isFalse()
+ verify(powerManager, never()).goToSleep(anyLong())
+ }
+ }
+
private suspend fun createUnderTest(isRevampedWppFeatureEnabled: Boolean = true) {
// This needs to be re-created for each test outside of kosmos since the flag values are
// read during initialization to set up flows. Maybe there is a better way to handle that.
@@ -309,6 +441,9 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() {
accessibilityManager = kosmos.accessibilityManagerWrapper,
pulsingGestureListener = kosmos.pulsingGestureListener,
faceAuthInteractor = kosmos.deviceEntryFaceAuthInteractor,
+ secureSettingsRepository = secureSettingsRepository,
+ powerManager = powerManager,
+ systemClock = kosmos.fakeSystemClock,
)
setUpState()
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModelTest.kt
index 8533134fd94e..95a6e56717fa 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModelTest.kt
@@ -21,15 +21,20 @@ import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
import com.android.systemui.Flags.FLAG_NOTIFICATION_SHADE_BLUR
import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.flags.BrokenWithSceneContainer
+import com.android.systemui.flags.DisableSceneContainer
import com.android.systemui.flags.andSceneContainer
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.keyguard.ui.transitions.blurConfig
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
@@ -43,6 +48,7 @@ class OccludedToPrimaryBouncerTransitionViewModelTest(flags: FlagsParameterizati
SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
+ private val repository = kosmos.fakeKeyguardTransitionRepository
private lateinit var underTest: OccludedToPrimaryBouncerTransitionViewModel
companion object {
@@ -63,6 +69,25 @@ class OccludedToPrimaryBouncerTransitionViewModelTest(flags: FlagsParameterizati
}
@Test
+ @DisableSceneContainer
+ fun lockscreenAlphaImmediatelyToZero() =
+ testScope.runTest {
+ val alpha by collectLastValue(underTest.lockscreenAlpha)
+
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+ runCurrent()
+ assertThat(alpha).isEqualTo(0f)
+
+ repository.sendTransitionStep(step(0.1f, TransitionState.RUNNING))
+ runCurrent()
+ assertThat(alpha).isEqualTo(0f)
+
+ repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+ runCurrent()
+ assertThat(alpha).isEqualTo(0f)
+ }
+
+ @Test
@BrokenWithSceneContainer(388068805)
fun notificationsAreBlurredImmediatelyWhenBouncerIsOpenedAndShadeIsExpanded() =
testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/log/LogWtfHandlerRuleTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/log/LogWtfHandlerRuleTest.kt
new file mode 100644
index 000000000000..d5d256e5cd97
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/log/LogWtfHandlerRuleTest.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.log
+
+import android.util.Log
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.model.Statement
+import org.mockito.kotlin.mock
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class LogWtfHandlerRuleTest : SysuiTestCase() {
+
+ val underTest = LogWtfHandlerRule()
+
+ @Test
+ fun passingTestWithoutWtf_shouldPass() {
+ val result = runTestCodeWithRule {
+ Log.e(TAG, "just an error", IndexOutOfBoundsException())
+ }
+ assertThat(result.isSuccess).isTrue()
+ }
+
+ @Test
+ fun passingTestWithWtf_shouldFail() {
+ val result = runTestCodeWithRule {
+ Log.wtf(TAG, "some terrible failure", IllegalStateException())
+ }
+ assertThat(result.isFailure).isTrue()
+ val exception = result.exceptionOrNull()
+ assertThat(exception).isInstanceOf(AssertionError::class.java)
+ assertThat(exception?.cause).isInstanceOf(Log.TerribleFailure::class.java)
+ assertThat(exception?.cause?.cause).isInstanceOf(IllegalStateException::class.java)
+ }
+
+ @Test
+ fun failingTestWithoutWtf_shouldFail() {
+ val result = runTestCodeWithRule {
+ Log.e(TAG, "just an error", IndexOutOfBoundsException())
+ throw NullPointerException("some npe")
+ }
+ assertThat(result.isFailure).isTrue()
+ assertThat(result.exceptionOrNull()).isInstanceOf(NullPointerException::class.java)
+ }
+
+ @Test
+ fun failingTestWithWtf_shouldFail() {
+ val result = runTestCodeWithRule {
+ Log.wtf(TAG, "some terrible failure", IllegalStateException())
+ throw NullPointerException("some npe")
+ }
+ assertThat(result.isFailure).isTrue()
+ assertThat(result.exceptionOrNull()).isInstanceOf(NullPointerException::class.java)
+ val suppressedExceptions = result.exceptionOrNull()!!.suppressedExceptions
+ assertThat(suppressedExceptions).hasSize(1)
+ val suppressed = suppressedExceptions.first()
+ assertThat(suppressed).isInstanceOf(AssertionError::class.java)
+ assertThat(suppressed.cause).isInstanceOf(Log.TerribleFailure::class.java)
+ assertThat(suppressed.cause?.cause).isInstanceOf(IllegalStateException::class.java)
+ }
+
+ @Test
+ fun passingTestWithExemptWtf_shouldPass() {
+ underTest.addFailureLogExemption { it.tag == TAG_EXPECTED }
+ val result = runTestCodeWithRule {
+ Log.wtf(TAG_EXPECTED, "some expected failure", IllegalStateException())
+ }
+ assertThat(result.isSuccess).isTrue()
+ }
+
+ @Test
+ fun failingTestWithExemptWtf_shouldFail() {
+ underTest.addFailureLogExemption { it.tag == TAG_EXPECTED }
+ val result = runTestCodeWithRule {
+ Log.wtf(TAG_EXPECTED, "some expected failure", IllegalStateException())
+ throw NullPointerException("some npe")
+ }
+ assertThat(result.isFailure).isTrue()
+ assertThat(result.exceptionOrNull()).isInstanceOf(NullPointerException::class.java)
+ val suppressedExceptions = result.exceptionOrNull()!!.suppressedExceptions
+ assertThat(suppressedExceptions).isEmpty()
+ }
+
+ @Test
+ fun passingTestWithOneExemptWtfOfTwo_shouldFail() {
+ underTest.addFailureLogExemption { it.tag == TAG_EXPECTED }
+ val result = runTestCodeWithRule {
+ Log.wtf(TAG_EXPECTED, "some expected failure", IllegalStateException())
+ Log.wtf(TAG, "some terrible failure", IllegalStateException())
+ }
+ assertThat(result.isFailure).isTrue()
+ val exception = result.exceptionOrNull()
+ assertThat(exception).isInstanceOf(AssertionError::class.java)
+ assertThat(exception?.cause).isInstanceOf(Log.TerribleFailure::class.java)
+ assertThat(exception?.cause?.cause).isInstanceOf(IllegalStateException::class.java)
+ }
+
+ @Test
+ fun failingTestWithOneExemptWtfOfTwo_shouldFail() {
+ underTest.addFailureLogExemption { it.tag == TAG_EXPECTED }
+ val result = runTestCodeWithRule {
+ Log.wtf(TAG_EXPECTED, "some expected failure", IllegalStateException())
+ Log.wtf(TAG, "some terrible failure", IllegalStateException())
+ throw NullPointerException("some npe")
+ }
+ assertThat(result.isFailure).isTrue()
+ assertThat(result.exceptionOrNull()).isInstanceOf(NullPointerException::class.java)
+ val suppressedExceptions = result.exceptionOrNull()!!.suppressedExceptions
+ assertThat(suppressedExceptions).hasSize(1)
+ val suppressed = suppressedExceptions.first()
+ assertThat(suppressed).isInstanceOf(AssertionError::class.java)
+ assertThat(suppressed.cause).isInstanceOf(Log.TerribleFailure::class.java)
+ assertThat(suppressed.cause?.cause).isInstanceOf(IllegalStateException::class.java)
+ }
+
+ private fun runTestCodeWithRule(testCode: () -> Unit): Result<Unit> {
+ val testCodeStatement =
+ object : Statement() {
+ override fun evaluate() {
+ testCode()
+ }
+ }
+ val wrappedTest = underTest.apply(testCodeStatement, mock())
+ return try {
+ wrappedTest.evaluate()
+ Result.success(Unit)
+ } catch (e: Throwable) {
+ Result.failure(e)
+ }
+ }
+
+ companion object {
+ const val TAG = "LogWtfHandlerRuleTest"
+ const val TAG_EXPECTED = "EXPECTED"
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationMediaManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/NotificationMediaManagerTest.kt
index a7fe586cbfa5..10b00857e887 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationMediaManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/NotificationMediaManagerTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar
+package com.android.systemui.media
import android.media.MediaMetadata
import android.media.session.MediaController
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java
index 2db2199602b8..9c4d93c17d00 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java
@@ -48,10 +48,12 @@ import androidx.test.filters.SmallTest;
import com.android.media.flags.Flags;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
+import com.android.settingslib.utils.ThreadUtils;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.res.R;
import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.ListeningExecutorService;
import org.junit.Before;
import org.junit.Test;
@@ -61,6 +63,7 @@ import org.mockito.Captor;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.Executor;
import java.util.stream.Collectors;
@SmallTest
@@ -95,6 +98,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
private List<MediaDevice> mMediaDevices = new ArrayList<>();
private List<MediaItem> mMediaItems = new ArrayList<>();
MediaOutputSeekbar mSpyMediaOutputSeekbar;
+ Executor mMainExecutor = mContext.getMainExecutor();
+ ListeningExecutorService mBackgroundExecutor = ThreadUtils.getBackgroundExecutor();
@Before
public void setUp() {
@@ -108,6 +113,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
when(mMediaSwitchingController.getSessionVolumeMax()).thenReturn(TEST_MAX_VOLUME);
when(mMediaSwitchingController.getSessionVolume()).thenReturn(TEST_CURRENT_VOLUME);
when(mMediaSwitchingController.getSessionName()).thenReturn(TEST_SESSION_NAME);
+ when(mMediaSwitchingController.getColorSchemeLegacy()).thenReturn(
+ mock(MediaOutputColorSchemeLegacy.class));
when(mIconCompat.toIcon(mContext)).thenReturn(mIcon);
when(mMediaDevice1.getName()).thenReturn(TEST_DEVICE_NAME_1);
when(mMediaDevice1.getId()).thenReturn(TEST_DEVICE_ID_1);
@@ -122,7 +129,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
mMediaItems.add(MediaItem.createDeviceMediaItem(mMediaDevice1, true));
mMediaItems.add(MediaItem.createDeviceMediaItem(mMediaDevice2, false));
- mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mMainExecutor,
+ mBackgroundExecutor);
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -148,7 +156,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
@Test
public void onBindViewHolder_bindPairNew_verifyView() {
- mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mMainExecutor,
+ mBackgroundExecutor);
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -173,7 +182,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
.map((item) -> item.getMediaDevice().get())
.collect(Collectors.toList()));
when(mMediaSwitchingController.getSessionName()).thenReturn(TEST_SESSION_NAME);
- mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mMainExecutor,
+ mBackgroundExecutor);
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -195,7 +205,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
.map((item) -> item.getMediaDevice().get())
.collect(Collectors.toList()));
when(mMediaSwitchingController.getSessionName()).thenReturn(null);
- mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mMainExecutor,
+ mBackgroundExecutor);
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -665,7 +676,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
@Test
public void onItemClick_clickPairNew_verifyLaunchBluetoothPairing() {
- mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mMainExecutor,
+ mBackgroundExecutor);
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -683,7 +695,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
assertThat(mMediaDevice2.getState()).isEqualTo(
LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_TRANSFER);
- mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mMainExecutor,
+ mBackgroundExecutor);
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -701,7 +714,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
assertThat(mMediaDevice2.getState()).isEqualTo(
LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_TRANSFER);
- mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController,
+ mContext.getMainExecutor(), ThreadUtils.getBackgroundExecutor());
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -723,7 +737,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
when(mMediaDevice2.getState()).thenReturn(
LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_GO_TO_APP);
- mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mMainExecutor,
+ mBackgroundExecutor);
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -778,6 +793,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2);
+ assertThat(mViewHolder.mTitleText.getAlpha())
+ .isEqualTo(MediaOutputAdapterLegacy.DEVICE_ACTIVE_ALPHA);
assertThat(mViewHolder.mContainerLayout.isFocusable()).isTrue();
mViewHolder.mContainerLayout.performClick();
@@ -799,6 +816,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2);
+ assertThat(mViewHolder.mTitleText.getAlpha())
+ .isEqualTo(MediaOutputAdapterLegacy.DEVICE_ACTIVE_ALPHA);
assertThat(mViewHolder.mContainerLayout.isFocusable()).isTrue();
mViewHolder.mContainerLayout.performClick();
@@ -820,6 +839,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2);
+ assertThat(mViewHolder.mTitleText.getAlpha())
+ .isEqualTo(MediaOutputAdapterLegacy.DEVICE_ACTIVE_ALPHA);
assertThat(mViewHolder.mContainerLayout.isFocusable()).isTrue();
mViewHolder.mContainerLayout.performClick();
@@ -841,6 +862,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2);
+ assertThat(mViewHolder.mTitleText.getAlpha())
+ .isEqualTo(MediaOutputAdapterLegacy.DEVICE_ACTIVE_ALPHA);
assertThat(mViewHolder.mContainerLayout.isFocusable()).isTrue();
mViewHolder.mContainerLayout.performClick();
@@ -862,6 +885,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2);
+ assertThat(mViewHolder.mTitleText.getAlpha())
+ .isEqualTo(MediaOutputAdapterLegacy.DEVICE_ACTIVE_ALPHA);
assertThat(mViewHolder.mContainerLayout.isFocusable()).isTrue();
mViewHolder.mContainerLayout.performClick();
@@ -883,6 +908,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2);
+ assertThat(mViewHolder.mTitleText.getAlpha())
+ .isEqualTo(MediaOutputAdapterLegacy.DEVICE_DISABLED_ALPHA);
assertThat(mViewHolder.mContainerLayout.isFocusable()).isTrue();
mViewHolder.mContainerLayout.performClick();
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModelTest.kt
index 3029928f070f..cb13b118fd68 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModelTest.kt
@@ -25,6 +25,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.app.iUriGrantsManager
import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.ui.viewmodel.iconProvider
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.runCurrent
import com.android.systemui.kosmos.runTest
@@ -32,6 +33,8 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.qs.external.TileData
+import com.android.systemui.qs.panels.ui.viewmodel.IconProvider
+import com.android.systemui.qs.panels.ui.viewmodel.toIconProvider
import com.android.systemui.qs.panels.ui.viewmodel.toUiState
import com.android.systemui.qs.tileimpl.QSTileImpl
import com.android.systemui.qs.tileimpl.QSTileImpl.ResourceIcon
@@ -80,28 +83,32 @@ class TileRequestDialogViewModelTest : SysuiTestCase() {
@Test
fun uiState_beforeActivation_hasDefaultIcon_andCorrectData() =
kosmos.runTest {
- val expectedState =
- baseResultLegacyState.apply { icon = defaultIcon }.toUiState(mainResources)
+ val state = baseResultLegacyState.apply { icon = defaultIcon }
+
+ val expectedState = state.toUiState(mainResources)
+ val expectedIconProvider = state.toIconProvider()
with(underTest.uiState) {
expect.that(label).isEqualTo(TEST_LABEL)
expect.that(secondaryLabel).isEmpty()
- expect.that(state).isEqualTo(expectedState.state)
+ expect.that(this.state).isEqualTo(expectedState.state)
expect.that(handlesLongClick).isFalse()
expect.that(handlesSecondaryClick).isFalse()
- expect.that(icon).isEqualTo(defaultIcon)
expect.that(sideDrawable).isNull()
expect.that(accessibilityUiState).isEqualTo(expectedState.accessibilityUiState)
}
+
+ expect.that(underTest.iconProvider).isEqualTo(expectedIconProvider)
}
@Test
fun uiState_afterActivation_hasCorrectIcon_andCorrectData() =
kosmos.runTest {
- val expectedState =
- baseResultLegacyState
- .apply { icon = QSTileImpl.DrawableIcon(loadedDrawable) }
- .toUiState(mainResources)
+ val state =
+ baseResultLegacyState.apply { icon = QSTileImpl.DrawableIcon(loadedDrawable) }
+
+ val expectedState = state.toUiState(mainResources)
+ val expectedIconProvider = state.toIconProvider()
underTest.activateIn(testScope)
runCurrent()
@@ -109,13 +116,13 @@ class TileRequestDialogViewModelTest : SysuiTestCase() {
with(underTest.uiState) {
expect.that(label).isEqualTo(TEST_LABEL)
expect.that(secondaryLabel).isEmpty()
- expect.that(state).isEqualTo(expectedState.state)
+ expect.that(this.state).isEqualTo(expectedState.state)
expect.that(handlesLongClick).isFalse()
expect.that(handlesSecondaryClick).isFalse()
- expect.that(icon).isEqualTo(QSTileImpl.DrawableIcon(loadedDrawable))
expect.that(sideDrawable).isNull()
expect.that(accessibilityUiState).isEqualTo(expectedState.accessibilityUiState)
}
+ expect.that(underTest.iconProvider).isEqualTo(expectedIconProvider)
}
@Test
@@ -135,7 +142,7 @@ class TileRequestDialogViewModelTest : SysuiTestCase() {
underTest.activateIn(testScope)
runCurrent()
- assertThat(underTest.uiState.icon).isEqualTo(defaultIcon)
+ assertThat(underTest.iconProvider).isEqualTo(IconProvider.ConstantIcon(defaultIcon))
}
companion object {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt
index 68a591dd075f..1d42424bc6ed 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt
@@ -16,11 +16,9 @@
package com.android.systemui.qs.panels.ui.viewmodel
-
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.qs.FakeQSTile
import com.android.systemui.qs.pipeline.data.repository.tileSpecRepository
@@ -48,45 +46,43 @@ class DetailsViewModelTest : SysuiTestCase() {
}
@Test
- fun changeTileDetailsViewModel() = with(kosmos) {
- testScope.runTest {
- val specs = listOf(
- spec,
- specNoDetails,
- )
- tileSpecRepository.setTiles(0, specs)
- runCurrent()
+ fun changeTileDetailsViewModel() =
+ with(kosmos) {
+ testScope.runTest {
+ val specs = listOf(spec, specNoDetails)
+ tileSpecRepository.setTiles(0, specs)
+ runCurrent()
- val tiles = currentTilesInteractor.currentTiles.value
+ val tiles = currentTilesInteractor.currentTiles.value
- assertThat(currentTilesInteractor.currentTilesSpecs.size).isEqualTo(2)
- assertThat(tiles[1].spec).isEqualTo(specNoDetails)
- (tiles[1].tile as FakeQSTile).hasDetailsViewModel = false
+ assertThat(currentTilesInteractor.currentTilesSpecs.size).isEqualTo(2)
+ assertThat(tiles[1].spec).isEqualTo(specNoDetails)
+ (tiles[1].tile as FakeQSTile).hasDetailsViewModel = false
- assertThat(underTest.activeTileDetails).isNull()
+ assertThat(underTest.activeTileDetails).isNull()
- // Click on the tile who has the `spec`.
- assertThat(underTest.onTileClicked(spec)).isTrue()
- assertThat(underTest.activeTileDetails).isNotNull()
- assertThat(underTest.activeTileDetails?.getTitle()).isEqualTo("internet")
+ // Click on the tile who has the `spec`.
+ assertThat(underTest.onTileClicked(spec)).isTrue()
+ assertThat(underTest.activeTileDetails).isNotNull()
+ assertThat(underTest.activeTileDetails?.title).isEqualTo("internet")
- // Click on a tile who dose not have a valid spec.
- assertThat(underTest.onTileClicked(null)).isFalse()
- assertThat(underTest.activeTileDetails).isNull()
+ // Click on a tile who dose not have a valid spec.
+ assertThat(underTest.onTileClicked(null)).isFalse()
+ assertThat(underTest.activeTileDetails).isNull()
- // Click again on the tile who has the `spec`.
- assertThat(underTest.onTileClicked(spec)).isTrue()
- assertThat(underTest.activeTileDetails).isNotNull()
- assertThat(underTest.activeTileDetails?.getTitle()).isEqualTo("internet")
+ // Click again on the tile who has the `spec`.
+ assertThat(underTest.onTileClicked(spec)).isTrue()
+ assertThat(underTest.activeTileDetails).isNotNull()
+ assertThat(underTest.activeTileDetails?.title).isEqualTo("internet")
- // Click on a tile who dose not have a detailed view.
- assertThat(underTest.onTileClicked(specNoDetails)).isFalse()
- assertThat(underTest.activeTileDetails).isNull()
+ // Click on a tile who dose not have a detailed view.
+ assertThat(underTest.onTileClicked(specNoDetails)).isFalse()
+ assertThat(underTest.activeTileDetails).isNull()
- underTest.closeDetailedView()
- assertThat(underTest.activeTileDetails).isNull()
+ underTest.closeDetailedView()
+ assertThat(underTest.activeTileDetails).isNull()
- assertThat(underTest.onTileClicked(null)).isFalse()
+ assertThat(underTest.onTileClicked(null)).isFalse()
+ }
}
- }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/IconProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/IconProviderTest.kt
new file mode 100644
index 000000000000..7257a89c214b
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/IconProviderTest.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.viewmodel
+
+import android.graphics.drawable.TestStubDrawable
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.qs.tileimpl.QSTileImpl.ResourceIcon
+import com.android.systemui.res.R
+import com.google.common.truth.Truth.assertThat
+import java.util.function.Supplier
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class IconProviderTest : SysuiTestCase() {
+
+ @Test
+ fun iconAndSupplier_prefersIcon() {
+ val state =
+ QSTile.State().apply {
+ icon = ResourceIcon.get(R.drawable.android)
+ iconSupplier = Supplier { QSTileImpl.DrawableIcon(TestStubDrawable()) }
+ }
+ val iconProvider = state.toIconProvider()
+
+ assertThat(iconProvider).isEqualTo(IconProvider.ConstantIcon(state.icon))
+ }
+
+ @Test
+ fun iconOnly_hasIcon() {
+ val state = QSTile.State().apply { icon = ResourceIcon.get(R.drawable.android) }
+ val iconProvider = state.toIconProvider()
+
+ assertThat(iconProvider).isEqualTo(IconProvider.ConstantIcon(state.icon))
+ }
+
+ @Test
+ fun supplierOnly_hasIcon() {
+ val state =
+ QSTile.State().apply {
+ iconSupplier = Supplier { ResourceIcon.get(R.drawable.android) }
+ }
+ val iconProvider = state.toIconProvider()
+
+ assertThat(iconProvider).isEqualTo(IconProvider.IconSupplier(state.iconSupplier))
+ }
+
+ @Test
+ fun noIconOrSupplier_iconNull() {
+ val state = QSTile.State()
+ val iconProvider = state.toIconProvider()
+
+ assertThat(iconProvider).isEqualTo(IconProvider.Empty)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiStateTest.kt
index 9c8e3225f3a4..b144f0678471 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiStateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiStateTest.kt
@@ -18,7 +18,6 @@ package com.android.systemui.qs.panels.ui.viewmodel
import android.content.res.Resources
import android.content.res.mainResources
-import android.graphics.drawable.TestStubDrawable
import android.service.quicksettings.Tile
import android.widget.Button
import android.widget.Switch
@@ -27,12 +26,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.plugins.qs.QSTile
-import com.android.systemui.qs.tileimpl.QSTileImpl
-import com.android.systemui.qs.tileimpl.QSTileImpl.ResourceIcon
import com.android.systemui.res.R
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
-import java.util.function.Supplier
import org.junit.Test
import org.junit.runner.RunWith
@@ -267,45 +263,6 @@ class TileUiStateTest : SysuiTestCase() {
.contains(resources.getString(R.string.tile_unavailable))
}
- @Test
- fun iconAndSupplier_prefersIcon() {
- val state =
- QSTile.State().apply {
- icon = ResourceIcon.get(R.drawable.android)
- iconSupplier = Supplier { QSTileImpl.DrawableIcon(TestStubDrawable()) }
- }
- val uiState = state.toUiState()
-
- assertThat(uiState.icon).isEqualTo(state.icon)
- }
-
- @Test
- fun iconOnly_hasIcon() {
- val state = QSTile.State().apply { icon = ResourceIcon.get(R.drawable.android) }
- val uiState = state.toUiState()
-
- assertThat(uiState.icon).isEqualTo(state.icon)
- }
-
- @Test
- fun supplierOnly_hasIcon() {
- val state =
- QSTile.State().apply {
- iconSupplier = Supplier { ResourceIcon.get(R.drawable.android) }
- }
- val uiState = state.toUiState()
-
- assertThat(uiState.icon).isEqualTo(state.iconSupplier.get())
- }
-
- @Test
- fun noIconOrSupplier_iconNull() {
- val state = QSTile.State()
- val uiState = state.toUiState()
-
- assertThat(uiState.icon).isNull()
- }
-
private fun QSTile.State.toUiState() = toUiState(resources)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
index c3089761effc..5bde7ad27b7a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
@@ -691,11 +691,11 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() {
var currentModel: TileDetailsViewModel? = null
val setCurrentModel = { model: TileDetailsViewModel? -> currentModel = model }
tiles!![0].tile.getDetailsViewModel(setCurrentModel)
- assertThat(currentModel?.getTitle()).isEqualTo("a")
+ assertThat(currentModel?.title).isEqualTo("a")
currentModel = null
tiles!![1].tile.getDetailsViewModel(setCurrentModel)
- assertThat(currentModel?.getTitle()).isEqualTo("b")
+ assertThat(currentModel?.title).isEqualTo("b")
currentModel = null
tiles!![2].tile.getDetailsViewModel(setCurrentModel)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractorTest.kt
index 00ee1c36590c..1b497a2b36ed 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractorTest.kt
@@ -24,6 +24,7 @@ import com.android.systemui.accessibility.hearingaid.HearingDevicesDialogManager
import com.android.systemui.accessibility.hearingaid.HearingDevicesUiEventLogger.Companion.LAUNCH_SOURCE_QS_TILE
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.shared.QSSettingsPackageRepository
import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject
import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
@@ -40,6 +41,7 @@ import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
@SmallTest
@EnabledOnRavenwood
@@ -53,14 +55,18 @@ class HearingDevicesTileUserActionInteractorTest : SysuiTestCase() {
@Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule()
@Mock private lateinit var dialogManager: HearingDevicesDialogManager
+ @Mock private lateinit var settingsPackageRepository: QSSettingsPackageRepository
@Before
fun setUp() {
+ whenever(settingsPackageRepository.getSettingsPackageName())
+ .thenReturn(SETTINGS_PACKAGE_NAME)
underTest =
HearingDevicesTileUserActionInteractor(
testScope.coroutineContext,
inputHandler,
dialogManager,
+ settingsPackageRepository,
)
}
@@ -91,6 +97,11 @@ class HearingDevicesTileUserActionInteractorTest : SysuiTestCase() {
QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
assertThat(it.intent.action).isEqualTo(Settings.ACTION_HEARING_DEVICES_SETTINGS)
+ assertThat(it.intent.`package`).isEqualTo(SETTINGS_PACKAGE_NAME)
}
}
+
+ companion object {
+ private const val SETTINGS_PACKAGE_NAME = "com.android.settings"
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index 9adf24f32c0c..1743e056b65c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -863,7 +863,7 @@ class SceneContainerStartableTest : SysuiTestCase() {
whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true)
val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
val playSuccessHaptic by
- collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic)
+ collectLastValue(deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry)
setupBiometricAuth(hasUdfps = true)
assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
@@ -885,7 +885,7 @@ class SceneContainerStartableTest : SysuiTestCase() {
whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true)
val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
val playSuccessHaptic by
- collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic)
+ collectLastValue(deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry)
setupBiometricAuth(hasUdfps = true)
assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
@@ -907,7 +907,7 @@ class SceneContainerStartableTest : SysuiTestCase() {
whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true)
val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
val playSuccessHaptic by
- collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic)
+ collectLastValue(deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry)
setupBiometricAuth(hasSfps = true)
assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
@@ -930,7 +930,7 @@ class SceneContainerStartableTest : SysuiTestCase() {
whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true)
val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
val playSuccessHaptic by
- collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic)
+ collectLastValue(deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry)
setupBiometricAuth(hasSfps = true)
assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
@@ -1033,7 +1033,7 @@ class SceneContainerStartableTest : SysuiTestCase() {
whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true)
val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
val playSuccessHaptic by
- collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic)
+ collectLastValue(deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry)
setupBiometricAuth(hasSfps = true)
assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
@@ -1056,7 +1056,7 @@ class SceneContainerStartableTest : SysuiTestCase() {
whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true)
val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
val playSuccessHaptic by
- collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic)
+ collectLastValue(deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry)
setupBiometricAuth(hasSfps = true)
assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
@@ -1079,7 +1079,7 @@ class SceneContainerStartableTest : SysuiTestCase() {
whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true)
val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
val playSuccessHaptic by
- collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic)
+ collectLastValue(deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry)
setupBiometricAuth(hasSfps = true)
assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
@@ -1102,7 +1102,7 @@ class SceneContainerStartableTest : SysuiTestCase() {
whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true)
val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
val playSuccessHaptic by
- collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic)
+ collectLastValue(deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry)
setupBiometricAuth(hasSfps = true)
assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
@@ -1160,7 +1160,7 @@ class SceneContainerStartableTest : SysuiTestCase() {
@Test
@DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
- fun playsFaceErrorHaptics_nonSfps_coEx() =
+ fun skipsFaceErrorHaptics_nonSfps_coEx() =
testScope.runTest {
val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
val playErrorHaptic by collectLastValue(deviceEntryHapticsInteractor.playErrorHaptic)
@@ -1172,15 +1172,14 @@ class SceneContainerStartableTest : SysuiTestCase() {
underTest.start()
updateFaceAuthStatus(isSuccess = false)
- assertThat(playErrorHaptic).isNotNull()
- assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
- verify(vibratorHelper).vibrateAuthError(anyString())
+ assertThat(playErrorHaptic).isNull()
+ verify(vibratorHelper, never()).vibrateAuthError(anyString())
verify(vibratorHelper, never()).vibrateAuthSuccess(anyString())
}
@Test
@EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
- fun playsMSDLFaceErrorHaptics_nonSfps_coEx() =
+ fun skipsMSDLFaceErrorHaptics_nonSfps_coEx() =
testScope.runTest {
val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
val playErrorHaptic by collectLastValue(deviceEntryHapticsInteractor.playErrorHaptic)
@@ -1192,10 +1191,9 @@ class SceneContainerStartableTest : SysuiTestCase() {
underTest.start()
updateFaceAuthStatus(isSuccess = false)
- assertThat(playErrorHaptic).isNotNull()
- assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
- assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.FAILURE)
- assertThat(msdlPlayer.latestPropertiesPlayed).isEqualTo(authInteractionProperties)
+ assertThat(playErrorHaptic).isNull()
+ assertThat(msdlPlayer.latestTokenPlayed).isNull()
+ assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index 3407cd50e76f..4a304071ee97 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -41,7 +41,6 @@ import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.platform.test.flag.junit.FlagsParameterization;
-import android.provider.Settings;
import android.testing.TestableLooper.RunWithLooper;
import android.view.View;
import android.view.WindowManager;
@@ -71,7 +70,6 @@ import com.android.systemui.statusbar.phone.ScrimController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
-import com.android.systemui.util.settings.FakeSettings;
import com.google.common.util.concurrent.MoreExecutors;
@@ -113,7 +111,6 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase {
@Captor private ArgumentCaptor<WindowManager.LayoutParams> mLayoutParameters;
@Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListener;
- private FakeSettings mSecureSettings;
private final Executor mMainExecutor = MoreExecutors.directExecutor();
private final Executor mBackgroundExecutor = MoreExecutors.directExecutor();
private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
@@ -135,9 +132,6 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase {
public void setUp() {
MockitoAnnotations.initMocks(this);
- mSecureSettings = new FakeSettings();
- mSecureSettings.putInt(Settings.Secure.DISABLE_SECURE_WINDOWS, 0);
-
// Preferred refresh rate is equal to the first displayMode's refresh rate
mPreferredRefreshRate = mContext.getDisplay().getSystemSupportedModes()[0].getRefreshRate();
overrideResource(
@@ -171,7 +165,6 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase {
() -> mSelectedUserInteractor,
mUserTracker,
mKosmos.getNotificationShadeWindowModel(),
- mSecureSettings,
mKosmos::getCommunalInteractor,
mKosmos.getShadeLayoutParams());
mNotificationShadeWindowController.setScrimsVisibilityListener((visibility) -> {});
@@ -355,19 +348,6 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase {
}
@Test
- public void setKeyguardShowingWithSecureWindowsDisabled_disablesSecureFlag() {
- mSecureSettings.putInt(Settings.Secure.DISABLE_SECURE_WINDOWS, 1);
- mNotificationShadeWindowController.setBouncerShowing(true);
-
- verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture());
- assertThat((mLayoutParameters.getValue().flags & FLAG_SECURE) == 0).isTrue();
- assertThat(
- (mLayoutParameters.getValue().inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_PRIVACY)
- != 0)
- .isTrue();
- }
-
- @Test
public void setKeyguardNotShowing_disablesSecureFlag() {
mNotificationShadeWindowController.setBouncerShowing(false);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicyTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicyTest.kt
index e43c46b36a06..dd0ba00994ce 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicyTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicyTest.kt
@@ -16,6 +16,7 @@
package com.android.systemui.shade.display
+import android.platform.test.annotations.EnableFlags
import android.view.Display
import android.view.Display.TYPE_EXTERNAL
import android.view.MotionEvent
@@ -31,6 +32,7 @@ import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.shade.data.repository.statusBarTouchShadeDisplayPolicy
import com.android.systemui.shade.domain.interactor.notificationElement
import com.android.systemui.shade.domain.interactor.qsElement
+import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
@@ -41,6 +43,7 @@ import org.mockito.kotlin.mock
@SmallTest
@RunWith(AndroidJUnit4::class)
+@EnableFlags(ShadeWindowGoesAround.FLAG_NAME)
class StatusBarTouchShadeDisplayPolicyTest : SysuiTestCase() {
private val kosmos = testKosmos().useUnconfinedTestDispatcher()
private val testScope = kosmos.testScope
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt
index 03f546b09faf..24593f4455e5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt
@@ -18,6 +18,7 @@ package com.android.systemui.shade.domain.interactor
import android.content.res.Configuration
import android.content.res.mockResources
+import android.platform.test.annotations.EnableFlags
import android.view.Display
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -27,6 +28,7 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.scene.ui.view.mockShadeRootView
import com.android.systemui.shade.data.repository.fakeShadeDisplaysRepository
+import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
import com.android.systemui.statusbar.notification.row.notificationRebindingTracker
@@ -47,6 +49,7 @@ import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class)
@SmallTest
+@EnableFlags(ShadeWindowGoesAround.FLAG_NAME)
class ShadeDisplaysInteractorTest : SysuiTestCase() {
private val kosmos = testKosmos().useUnconfinedTestDispatcher()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
index a832f486ef32..04eb709b8894 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
@@ -94,6 +94,36 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
}
@Test
+ fun showClock_wideLayout_returnsTrue() =
+ testScope.runTest {
+ kosmos.enableDualShade(wideLayout = true)
+
+ setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.NotificationsShade)
+ assertThat(underTest.showClock).isTrue()
+
+ setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.QuickSettingsShade)
+ assertThat(underTest.showClock).isTrue()
+ }
+
+ @Test
+ fun showClock_narrowLayoutOnNotificationsShade_returnsFalse() =
+ testScope.runTest {
+ kosmos.enableDualShade(wideLayout = false)
+ setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.NotificationsShade)
+
+ assertThat(underTest.showClock).isFalse()
+ }
+
+ @Test
+ fun showClock_narrowLayoutOnQuickSettingsShade_returnsTrue() =
+ testScope.runTest {
+ kosmos.enableDualShade(wideLayout = false)
+ setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.QuickSettingsShade)
+
+ assertThat(underTest.showClock).isTrue()
+ }
+
+ @Test
fun onShadeCarrierGroupClicked_launchesNetworkSettings() =
testScope.runTest {
val activityStarter = kosmos.activityStarter
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt
index fad66581682f..0642467a001b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt
@@ -19,6 +19,8 @@ package com.android.systemui.shared.clocks
import android.content.res.Resources
import android.graphics.Color
import android.graphics.drawable.Drawable
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.util.TypedValue
import android.view.LayoutInflater
import android.widget.FrameLayout
@@ -29,6 +31,7 @@ import com.android.systemui.customization.R
import com.android.systemui.plugins.clocks.ClockId
import com.android.systemui.plugins.clocks.ClockSettings
import com.android.systemui.plugins.clocks.ThemeConfig
+import com.android.systemui.shared.Flags
import com.android.systemui.shared.clocks.DefaultClockController.Companion.DOZE_COLOR
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
@@ -103,6 +106,26 @@ class DefaultClockProviderTest : SysuiTestCase() {
}
@Test
+ @DisableFlags(Flags.FLAG_AMBIENT_AOD)
+ fun defaultClock_initialize_flagOff() {
+ val clock = provider.createClock(DEFAULT_CLOCK_ID)
+ verify(mockSmallClockView).setColors(DOZE_COLOR, Color.MAGENTA)
+ verify(mockLargeClockView).setColors(DOZE_COLOR, Color.MAGENTA)
+
+ clock.initialize(true, 0f, 0f, null)
+
+ // This is the default darkTheme color
+ val expectedColor = context.resources.getColor(android.R.color.system_accent1_100)
+ verify(mockSmallClockView).setColors(DOZE_COLOR, expectedColor)
+ verify(mockLargeClockView).setColors(DOZE_COLOR, expectedColor)
+ verify(mockSmallClockView).onTimeZoneChanged(notNull())
+ verify(mockLargeClockView).onTimeZoneChanged(notNull())
+ verify(mockSmallClockView).refreshTime()
+ verify(mockLargeClockView).refreshTime()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_AMBIENT_AOD)
fun defaultClock_initialize() {
val clock = provider.createClock(DEFAULT_CLOCK_ID)
verify(mockSmallClockView).setColors(DOZE_COLOR, Color.MAGENTA)
@@ -110,7 +133,7 @@ class DefaultClockProviderTest : SysuiTestCase() {
clock.initialize(true, 0f, 0f, null)
- val expectedColor = 0
+ val expectedColor = Color.MAGENTA
verify(mockSmallClockView).setColors(DOZE_COLOR, expectedColor)
verify(mockLargeClockView).setColors(DOZE_COLOR, expectedColor)
verify(mockSmallClockView).onTimeZoneChanged(notNull())
@@ -165,8 +188,10 @@ class DefaultClockProviderTest : SysuiTestCase() {
}
@Test
- fun defaultClock_events_onThemeChanged_noSeed() {
- val expectedColor = 0
+ @DisableFlags(Flags.FLAG_AMBIENT_AOD)
+ fun defaultClock_events_onThemeChanged_noSeed_flagOff() {
+ // This is the default darkTheme color
+ val expectedColor = context.resources.getColor(android.R.color.system_accent1_100)
val clock = provider.createClock(DEFAULT_CLOCK_ID)
verify(mockSmallClockView).setColors(DOZE_COLOR, Color.MAGENTA)
@@ -180,6 +205,22 @@ class DefaultClockProviderTest : SysuiTestCase() {
}
@Test
+ @EnableFlags(Flags.FLAG_AMBIENT_AOD)
+ fun defaultClock_events_onThemeChanged_noSeedn() {
+ val expectedColor = Color.TRANSPARENT
+ val clock = provider.createClock(DEFAULT_CLOCK_ID)
+
+ verify(mockSmallClockView).setColors(DOZE_COLOR, Color.MAGENTA)
+ verify(mockLargeClockView).setColors(DOZE_COLOR, Color.MAGENTA)
+
+ clock.smallClock.events.onThemeChanged(ThemeConfig(true, null))
+ clock.largeClock.events.onThemeChanged(ThemeConfig(true, null))
+
+ verify(mockSmallClockView).setColors(DOZE_COLOR, Color.MAGENTA)
+ verify(mockLargeClockView).setColors(DOZE_COLOR, Color.MAGENTA)
+ }
+
+ @Test
fun defaultClock_events_onThemeChanged_newSeed() {
val initSeedColor = 10
val newSeedColor = 20
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
index 064fd485dab4..b80ff3466007 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
@@ -33,6 +33,7 @@ import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.log.LogAssertKt;
import com.android.systemui.plugins.Plugin;
import com.android.systemui.plugins.PluginLifecycleManager;
import com.android.systemui.plugins.PluginListener;
@@ -138,11 +139,12 @@ public class PluginInstanceTest extends SysuiTestCase {
mVersionCheckResult = false;
assertFalse(mPluginInstance.hasError());
-
mPluginInstanceFactory.create(
mContext, mAppInfo, wrongVersionTestPluginComponentName,
TestPlugin.class, mPluginListener);
- mPluginInstance.onCreate();
+ LogAssertKt.assertRunnableLogsWtf(()-> {
+ mPluginInstance.onCreate();
+ });
assertTrue(mPluginInstance.hasError());
assertNull(mPluginInstance.getPlugin());
}
@@ -193,8 +195,10 @@ public class PluginInstanceTest extends SysuiTestCase {
mPluginInstance.onCreate();
assertFalse(mPluginInstance.hasError());
- Object result = mPluginInstance.getPlugin().methodThrowsError();
- assertNotNull(result); // Wrapper function should return non-null;
+ LogAssertKt.assertRunnableLogsWtf(()-> {
+ Object result = mPluginInstance.getPlugin().methodThrowsError();
+ assertNotNull(result); // Wrapper function should return non-null;
+ });
assertTrue(mPluginInstance.hasError());
assertNull(mPluginInstance.getPlugin());
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationGroupingUtilTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationGroupingUtilTest.kt
new file mode 100644
index 000000000000..e04162bf990a
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationGroupingUtilTest.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar
+
+import android.platform.test.flag.junit.FlagsParameterization
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.row.NotificationTestHelper
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+
+@SmallTest
+@RunWith(ParameterizedAndroidJunit4::class)
+class NotificationGroupingUtilTest(flags: FlagsParameterization) : SysuiTestCase() {
+
+ private lateinit var underTest: NotificationGroupingUtil
+
+ private lateinit var testHelper: NotificationTestHelper
+
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf(NotificationBundleUi.FLAG_NAME)
+ }
+ }
+
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
+ }
+
+ @Before
+ fun setup() {
+ testHelper = NotificationTestHelper(mContext, mDependency)
+ }
+
+ @Test
+ fun showsTime() {
+ val row = testHelper.createRow()
+
+ underTest = NotificationGroupingUtil(row)
+ assertThat(underTest.showsTime(row)).isTrue()
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java
index c9d910c530ea..01046cd10d87 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java
@@ -20,16 +20,29 @@ import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer;
import static junit.framework.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.app.ActivityOptions;
import android.app.Notification;
+import android.app.PendingIntent;
import android.content.Context;
+import android.content.Intent;
import android.os.SystemClock;
import android.os.UserHandle;
+import android.platform.test.flag.junit.FlagsParameterization;
import android.testing.TestableLooper;
+import android.util.Pair;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.RemoteViews;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
@@ -42,19 +55,37 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
import com.android.systemui.statusbar.policy.RemoteInputUriController;
import com.android.systemui.util.kotlin.JavaAdapter;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedAndroidJunit4.class)
@TestableLooper.RunWithLooper
public class NotificationRemoteInputManagerTest extends SysuiTestCase {
+
+ @Parameters(name = "{0}")
+ public static List<FlagsParameterization> getParams() {
+ return FlagsParameterization.allCombinationsOf(NotificationBundleUi.FLAG_NAME);
+ }
+
private static final String TEST_PACKAGE_NAME = "test";
private static final int TEST_UID = 0;
@@ -69,14 +100,34 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase {
@Mock private NotificationClickNotifier mClickNotifier;
@Mock private NotificationLockscreenUserManager mLockscreenUserManager;
@Mock private PowerInteractor mPowerInteractor;
+ @Mock
+ NotificationRemoteInputManager.RemoteInputListener mRemoteInputListener;
+ private ActionClickLogger mActionClickLogger;
+ @Captor
+ ArgumentCaptor<NotificationRemoteInputManager.ClickHandler> mClickHandlerArgumentCaptor;
+ private Context mSpyContext;
+ private NotificationTestHelper mTestHelper;
private TestableNotificationRemoteInputManager mRemoteInputManager;
private NotificationEntry mEntry;
+ public NotificationRemoteInputManagerTest(FlagsParameterization flags) {
+ super();
+ mSetFlagsRule.setFlagsParameterization(flags);
+ }
+
@Before
- public void setUp() {
+ public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
+ mSpyContext = spy(mContext);
+ doNothing().when(mSpyContext).startIntentSender(
+ any(), any(), anyInt(), anyInt(), anyInt(), any());
+
+
+ mTestHelper = new NotificationTestHelper(mSpyContext, mDependency);
+ mActionClickLogger = spy(new ActionClickLogger(logcatLogBuffer()));
+
mRemoteInputManager = new TestableNotificationRemoteInputManager(mContext,
mock(NotifPipelineFlags.class),
mLockscreenUserManager,
@@ -87,9 +138,10 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase {
mRemoteInputUriController,
new RemoteInputControllerLogger(logcatLogBuffer()),
mClickNotifier,
- new ActionClickLogger(logcatLogBuffer()),
+ mActionClickLogger,
mock(JavaAdapter.class),
mock(ShadeInteractor.class));
+ mRemoteInputManager.setRemoteInputListener(mRemoteInputListener);
mEntry = new NotificationEntryBuilder()
.setPkg(TEST_PACKAGE_NAME)
.setOpPkg(TEST_PACKAGE_NAME)
@@ -133,6 +185,70 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase {
assertTrue(mRemoteInputManager.shouldKeepForSmartReplyHistory(mEntry));
}
+ @Test
+ public void testActionClick() throws Exception {
+ RemoteViews.RemoteResponse response = mock(RemoteViews.RemoteResponse.class);
+ when(response.getLaunchOptions(any())).thenReturn(
+ Pair.create(mock(Intent.class), mock(ActivityOptions.class)));
+ ExpandableNotificationRow row = getRowWithReplyAction();
+ View actionView = ((LinearLayout) row.getPrivateLayout().getExpandedChild().findViewById(
+ com.android.internal.R.id.actions)).getChildAt(0);
+ Notification n = getNotification(row);
+ CountDownLatch latch = new CountDownLatch(1);
+ Consumer<NotificationEntry> consumer = notificationEntry -> latch.countDown();
+ if (!NotificationBundleUi.isEnabled()) {
+ mRemoteInputManager.addActionPressListener(consumer);
+ }
+
+ mRemoteInputManager.getRemoteViewsOnClickHandler().onInteraction(
+ actionView,
+ n.actions[0].actionIntent,
+ response);
+
+ verify(mActionClickLogger).logInitialClick(row.getKey(), 0, n.actions[0].actionIntent);
+ verify(mClickNotifier).onNotificationActionClick(
+ eq(row.getKey()), eq(0), eq(n.actions[0]), any(), eq(false));
+ verify(mCallback).handleRemoteViewClick(eq(actionView), eq(n.actions[0].actionIntent),
+ eq(false), eq(0), mClickHandlerArgumentCaptor.capture());
+
+ mClickHandlerArgumentCaptor.getValue().handleClick();
+ verify(mActionClickLogger).logStartingIntentWithDefaultHandler(
+ row.getKey(), n.actions[0].actionIntent, 0);
+
+ verify(mRemoteInputListener).releaseNotificationIfKeptForRemoteInputHistory(row.getKey());
+ if (NotificationBundleUi.isEnabled()) {
+ verify(row.getEntryAdapter()).onNotificationActionClicked();
+ } else {
+ latch.await(10, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ private Notification getNotification(ExpandableNotificationRow row) {
+ if (NotificationBundleUi.isEnabled()) {
+ return row.getEntryAdapter().getSbn().getNotification();
+ } else {
+ return row.getEntry().getSbn().getNotification();
+ }
+ }
+
+ private ExpandableNotificationRow getRowWithReplyAction() throws Exception {
+ PendingIntent pi = PendingIntent.getBroadcast(getContext(), 0, new Intent("Action"),
+ PendingIntent.FLAG_IMMUTABLE);
+ Notification n = new Notification.Builder(mSpyContext, "")
+ .setSmallIcon(com.android.systemui.res.R.drawable.ic_person)
+ .addAction(new Notification.Action(com.android.systemui.res.R.drawable.ic_person,
+ "reply", pi))
+ .build();
+ ExpandableNotificationRow row = mTestHelper.createRow(n);
+ row.onNotificationUpdated();
+ row.getPrivateLayout().setExpandedChild(Notification.Builder.recoverBuilder(mSpyContext, n)
+ .createBigContentView().apply(
+ mSpyContext,
+ row.getPrivateLayout(),
+ mRemoteInputManager.getRemoteViewsOnClickHandler()));
+ return row;
+ }
+
private class TestableNotificationRemoteInputManager extends NotificationRemoteInputManager {
TestableNotificationRemoteInputManager(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
index fda4ab005446..485b9febc284 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
@@ -19,43 +19,51 @@ package com.android.systemui.statusbar.chips.call.ui.viewmodel
import android.app.PendingIntent
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.FlagsParameterization
import android.view.View
-import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.plugins.activityStarter
import com.android.systemui.res.R
import com.android.systemui.statusbar.StatusBarIconView
+import com.android.systemui.statusbar.chips.StatusBarChipsReturnAnimations
import com.android.systemui.statusbar.chips.ui.model.ColorsModel
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
+import com.android.systemui.statusbar.core.StatusBarRootModernization
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
import com.android.systemui.statusbar.phone.ongoingcall.DisableChipsModernization
import com.android.systemui.statusbar.phone.ongoingcall.EnableChipsModernization
+import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.addOngoingCallState
import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.removeOngoingCallState
import com.android.systemui.testKosmos
import com.android.systemui.util.time.fakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
-import kotlinx.coroutines.test.runTest
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
@SmallTest
-@RunWith(AndroidJUnit4::class)
-class CallChipViewModelTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+class CallChipViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
+ }
+
private val kosmos = testKosmos().useUnconfinedTestDispatcher()
private val chipBackgroundView = mock<ChipBackgroundContainer>()
@@ -71,7 +79,7 @@ class CallChipViewModelTest : SysuiTestCase() {
private val mockExpandable: Expandable =
mock<Expandable>().apply { whenever(dialogTransitionController(any())).thenReturn(mock()) }
- private val underTest by lazy { kosmos.callChipViewModel }
+ private val Kosmos.underTest by Kosmos.Fixture { callChipViewModel }
@Test
fun chip_noCall_isHidden() =
@@ -88,9 +96,10 @@ class CallChipViewModelTest : SysuiTestCase() {
kosmos.runTest {
val latest by collectLastValue(underTest.chip)
- addOngoingCallState(startTimeMs = 0)
+ addOngoingCallState(startTimeMs = 0, isAppVisible = false)
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
+ assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse()
}
@Test
@@ -98,9 +107,10 @@ class CallChipViewModelTest : SysuiTestCase() {
kosmos.runTest {
val latest by collectLastValue(underTest.chip)
- addOngoingCallState(startTimeMs = -2)
+ addOngoingCallState(startTimeMs = -2, isAppVisible = false)
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
+ assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse()
}
@Test
@@ -108,9 +118,82 @@ class CallChipViewModelTest : SysuiTestCase() {
kosmos.runTest {
val latest by collectLastValue(underTest.chip)
- addOngoingCallState(startTimeMs = 345)
+ addOngoingCallState(startTimeMs = 345, isAppVisible = false)
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java)
+ assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse()
+ }
+
+ @Test
+ @DisableFlags(StatusBarChipsReturnAnimations.FLAG_NAME)
+ fun chipLegacy_inCallWithVisibleApp_zeroStartTime_isHiddenAsInactive() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ addOngoingCallState(startTimeMs = 0, isAppVisible = true)
+
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Inactive::class.java)
+ }
+
+ @Test
+ @EnableFlags(StatusBarChipsReturnAnimations.FLAG_NAME)
+ @EnableChipsModernization
+ fun chipWithReturnAnimation_inCallWithVisibleApp_zeroStartTime_isHiddenAsIconOnly() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ addOngoingCallState(startTimeMs = 0, isAppVisible = true)
+
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
+ assertThat((latest as OngoingActivityChipModel.Active).isHidden).isTrue()
+ }
+
+ @Test
+ @DisableFlags(StatusBarChipsReturnAnimations.FLAG_NAME)
+ fun chipLegacy_inCallWithVisibleApp_negativeStartTime_isHiddenAsInactive() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ addOngoingCallState(startTimeMs = -2, isAppVisible = true)
+
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Inactive::class.java)
+ }
+
+ @Test
+ @EnableFlags(StatusBarChipsReturnAnimations.FLAG_NAME)
+ @EnableChipsModernization
+ fun chipWithReturnAnimation_inCallWithVisibleApp_negativeStartTime_isHiddenAsIconOnly() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ addOngoingCallState(startTimeMs = -2, isAppVisible = true)
+
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
+ assertThat((latest as OngoingActivityChipModel.Active).isHidden).isTrue()
+ }
+
+ @Test
+ @DisableFlags(StatusBarChipsReturnAnimations.FLAG_NAME)
+ fun chipLegacy_inCallWithVisibleApp_positiveStartTime_isHiddenAsInactive() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ addOngoingCallState(startTimeMs = 345, isAppVisible = true)
+
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Inactive::class.java)
+ }
+
+ @Test
+ @EnableFlags(StatusBarChipsReturnAnimations.FLAG_NAME)
+ @EnableChipsModernization
+ fun chipWithReturnAnimation_inCallWithVisibleApp_positiveStartTime_isHiddenAsTimer() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ addOngoingCallState(startTimeMs = 345, isAppVisible = true)
+
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java)
+ assertThat((latest as OngoingActivityChipModel.Active).isHidden).isTrue()
}
@Test
@@ -419,5 +502,18 @@ class CallChipViewModelTest : SysuiTestCase() {
private const val PROMOTED_BACKGROUND_COLOR = 65
private const val PROMOTED_PRIMARY_TEXT_COLOR = 98
+
+ @get:Parameters(name = "{0}")
+ @JvmStatic
+ val flags: List<FlagsParameterization>
+ get() = buildList {
+ addAll(
+ FlagsParameterization.allCombinationsOf(
+ StatusBarRootModernization.FLAG_NAME,
+ StatusBarChipsModernization.FLAG_NAME,
+ StatusBarChipsReturnAnimations.FLAG_NAME,
+ )
+ )
+ }
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
index 5d1950670777..7f8f5f43e775 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
@@ -27,6 +27,7 @@ import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.statusbar.StatusBarIconView
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
@@ -39,6 +40,7 @@ import org.mockito.kotlin.mock
@SmallTest
@RunWith(AndroidJUnit4::class)
+@EnableFlags(StatusBarNotifChips.FLAG_NAME)
class SingleNotificationChipInteractorTest : SysuiTestCase() {
private val kosmos = testKosmos().useUnconfinedTestDispatcher()
val factory = kosmos.singleNotificationChipInteractorFactory
diff --git a/packages/SystemUI/multivalentTests/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 e2d1498270c8..e39fa7099953 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
@@ -137,7 +137,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() {
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.Recording
- addOngoingCallState()
+ addOngoingCallState(isAppVisible = false)
val latest by collectLastValue(underTest.primaryChip)
@@ -163,7 +163,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() {
screenRecordState.value = ScreenRecordModel.DoingNothing
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
- addOngoingCallState()
+ addOngoingCallState(isAppVisible = false)
val latest by collectLastValue(underTest.primaryChip)
@@ -178,7 +178,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() {
// MediaProjection covers both share-to-app and cast-to-other-device
mediaProjectionState.value = MediaProjectionState.NotProjecting
- addOngoingCallState(key = notificationKey)
+ addOngoingCallState(key = notificationKey, isAppVisible = false)
val latest by collectLastValue(underTest.primaryChip)
@@ -190,7 +190,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() {
kosmos.runTest {
// Start with just the lowest priority chip shown
val callNotificationKey = "call"
- addOngoingCallState(key = callNotificationKey)
+ addOngoingCallState(key = callNotificationKey, isAppVisible = false)
// And everything else hidden
mediaProjectionState.value = MediaProjectionState.NotProjecting
screenRecordState.value = ScreenRecordModel.DoingNothing
@@ -225,7 +225,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() {
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
val callNotificationKey = "call"
- addOngoingCallState(key = callNotificationKey)
+ addOngoingCallState(key = callNotificationKey, isAppVisible = false)
val latest by collectLastValue(underTest.primaryChip)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
index 96c4a59f752d..f06244f4f637 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
@@ -62,6 +62,7 @@ import com.android.systemui.statusbar.notification.data.repository.ActiveNotific
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
import com.android.systemui.statusbar.notification.data.repository.addNotif
import com.android.systemui.statusbar.notification.data.repository.addNotifs
+import com.android.systemui.statusbar.notification.data.repository.removeNotif
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
import com.android.systemui.statusbar.phone.SystemUIDialog
@@ -235,7 +236,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
fun primaryChip_screenRecordShowAndCallShow_screenRecordShown() =
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.Recording
- addOngoingCallState("call")
+ addOngoingCallState("call", isAppVisible = false)
val latest by collectLastValue(underTest.primaryChip)
@@ -248,7 +249,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
kosmos.runTest {
val callNotificationKey = "call"
screenRecordState.value = ScreenRecordModel.Recording
- addOngoingCallState(callNotificationKey)
+ addOngoingCallState(callNotificationKey, isAppVisible = false)
val latest by collectLastValue(underTest.chipsLegacy)
val unused by collectLastValue(underTest.chips)
@@ -295,7 +296,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
@Test
fun chipsLegacy_oneChip_notSquished() =
kosmos.runTest {
- addOngoingCallState()
+ addOngoingCallState(isAppVisible = false)
val latest by collectLastValue(underTest.chipsLegacy)
@@ -322,7 +323,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
fun chipsLegacy_twoTimerChips_isSmallPortrait_bothSquished() =
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.Recording
- addOngoingCallState(key = "call")
+ addOngoingCallState(key = "call", isAppVisible = false)
val latest by collectLastValue(underTest.chipsLegacy)
@@ -349,12 +350,42 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
.isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
}
+ @EnableChipsModernization
+ @Test
+ fun chips_threeChips_isSmallPortrait_allSquished() =
+ kosmos.runTest {
+ screenRecordState.value = ScreenRecordModel.Recording
+ addOngoingCallState(key = "call")
+
+ val promotedContentBuilder =
+ PromotedNotificationContentModel.Builder("notif").apply {
+ this.shortCriticalText = "Some text here"
+ }
+ activeNotificationListRepository.addNotif(
+ activeNotificationModel(
+ key = "notif",
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
+ promotedContent = promotedContentBuilder.build(),
+ )
+ )
+
+ val latest by collectLastValue(underTest.chips)
+
+ // Squished chips are icon only
+ assertThat(latest!!.active[0])
+ .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
+ assertThat(latest!!.active[1])
+ .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
+ assertThat(latest!!.active[2])
+ .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
+ }
+
@DisableChipsModernization
@Test
fun chipsLegacy_countdownChipAndTimerChip_countdownNotSquished_butTimerSquished() =
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.Starting(millisUntilStarted = 2000)
- addOngoingCallState(key = "call")
+ addOngoingCallState(key = "call", isAppVisible = false)
val latest by collectLastValue(underTest.chipsLegacy)
@@ -400,7 +431,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
.isInstanceOf(OngoingActivityChipModel.Inactive::class.java)
// WHEN there's 2 chips
- addOngoingCallState(key = "call")
+ addOngoingCallState(key = "call", isAppVisible = false)
// THEN they both become squished
assertThat(latest!!.primary)
@@ -456,7 +487,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
fun chipsLegacy_twoChips_isLandscape_notSquished() =
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.Recording
- addOngoingCallState(key = "call")
+ addOngoingCallState(key = "call", isAppVisible = false)
// WHEN we're in landscape
val config =
@@ -502,7 +533,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
fun chipsLegacy_twoChips_isLargeScreen_notSquished() =
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.Recording
- addOngoingCallState(key = "call")
+ addOngoingCallState(key = "call", isAppVisible = false)
// WHEN we're on a large screen
kosmos.displayStateRepository.setIsLargeScreen(true)
@@ -596,7 +627,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
screenRecordState.value = ScreenRecordModel.DoingNothing
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
- addOngoingCallState(key = "call")
+ addOngoingCallState(key = "call", isAppVisible = false)
val latest by collectLastValue(underTest.primaryChip)
@@ -611,7 +642,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
screenRecordState.value = ScreenRecordModel.DoingNothing
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
- addOngoingCallState(key = "call")
+ addOngoingCallState(key = "call", isAppVisible = false)
val latest by collectLastValue(underTest.chipsLegacy)
val unused by collectLastValue(underTest.chips)
@@ -650,7 +681,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
mediaProjectionState.value = MediaProjectionState.NotProjecting
val callNotificationKey = "call"
- addOngoingCallState(key = callNotificationKey)
+ addOngoingCallState(key = callNotificationKey, isAppVisible = false)
val latest by collectLastValue(underTest.primaryChip)
@@ -666,7 +697,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
// MediaProjection covers both share-to-app and cast-to-other-device
mediaProjectionState.value = MediaProjectionState.NotProjecting
- addOngoingCallState(key = callNotificationKey)
+ addOngoingCallState(key = callNotificationKey, isAppVisible = false)
val latest by collectLastValue(underTest.chipsLegacy)
val unused by collectLastValue(underTest.chips)
@@ -851,7 +882,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
@EnableChipsModernization
@Test
- fun chips_threePromotedNotifs_topTwoActiveThirdInOverflow() =
+ fun chips_fourPromotedNotifs_topThreeActiveFourthInOverflow() =
kosmos.runTest {
val latest by collectLastValue(underTest.chips)
val unused by collectLastValue(underTest.chipsLegacy)
@@ -859,6 +890,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
val firstIcon = createStatusBarIconViewOrNull()
val secondIcon = createStatusBarIconViewOrNull()
val thirdIcon = createStatusBarIconViewOrNull()
+ val fourthIcon = createStatusBarIconViewOrNull()
setNotifs(
listOf(
activeNotificationModel(
@@ -879,20 +911,27 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
promotedContent =
PromotedNotificationContentModel.Builder("thirdNotif").build(),
),
+ activeNotificationModel(
+ key = "fourthNotif",
+ statusBarChipIcon = fourthIcon,
+ promotedContent =
+ PromotedNotificationContentModel.Builder("fourthNotif").build(),
+ ),
)
)
- assertThat(latest!!.active.size).isEqualTo(2)
+ assertThat(latest!!.active.size).isEqualTo(3)
assertIsNotifChip(latest!!.active[0], context, firstIcon, "firstNotif")
assertIsNotifChip(latest!!.active[1], context, secondIcon, "secondNotif")
+ assertIsNotifChip(latest!!.active[2], context, thirdIcon, "thirdNotif")
assertThat(latest!!.overflow.size).isEqualTo(1)
- assertIsNotifChip(latest!!.overflow[0], context, thirdIcon, "thirdNotif")
+ assertIsNotifChip(latest!!.overflow[0], context, fourthIcon, "fourthNotif")
assertThat(latest!!.inactive.size).isEqualTo(4)
assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy())
}
@Test
- fun visibleChipKeys_threePromotedNotifs_topTwoInList() =
+ fun visibleChipKeys_fourPromotedNotifs_topThreeInList() =
kosmos.runTest {
val latest by collectLastValue(underTest.visibleChipKeys)
@@ -916,10 +955,16 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
promotedContent =
PromotedNotificationContentModel.Builder("thirdNotif").build(),
),
+ activeNotificationModel(
+ key = "fourthNotif",
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
+ promotedContent =
+ PromotedNotificationContentModel.Builder("fourthNotif").build(),
+ ),
)
)
- assertThat(latest).containsExactly("firstNotif", "secondNotif").inOrder()
+ assertThat(latest).containsExactly("firstNotif", "secondNotif", "thirdNotif").inOrder()
}
@DisableChipsModernization
@@ -930,7 +975,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
val unused by collectLastValue(underTest.chips)
val callNotificationKey = "call"
- addOngoingCallState(callNotificationKey)
+ addOngoingCallState(callNotificationKey, isAppVisible = false)
val firstIcon = createStatusBarIconViewOrNull()
activeNotificationListRepository.addNotifs(
@@ -957,7 +1002,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
@EnableChipsModernization
@Test
- fun chips_callAndPromotedNotifs_callAndFirstNotifActiveSecondNotifInOverflow() =
+ fun chips_callAndPromotedNotifs_callAndFirstTwoNotifsActive_thirdNotifInOverflow() =
kosmos.runTest {
val latest by collectLastValue(underTest.chips)
val unused by collectLastValue(underTest.chipsLegacy)
@@ -965,6 +1010,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
val callNotificationKey = "call"
val firstIcon = createStatusBarIconViewOrNull()
val secondIcon = createStatusBarIconViewOrNull()
+ val thirdIcon = createStatusBarIconViewOrNull()
addOngoingCallState(key = callNotificationKey)
activeNotificationListRepository.addNotifs(
listOf(
@@ -980,27 +1026,34 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
promotedContent =
PromotedNotificationContentModel.Builder("secondNotif").build(),
),
+ activeNotificationModel(
+ key = "thirdNotif",
+ statusBarChipIcon = thirdIcon,
+ promotedContent =
+ PromotedNotificationContentModel.Builder("thirdNotif").build(),
+ ),
)
)
- assertThat(latest!!.active.size).isEqualTo(2)
+ assertThat(latest!!.active.size).isEqualTo(3)
assertIsCallChip(latest!!.active[0], callNotificationKey, context)
assertIsNotifChip(latest!!.active[1], context, firstIcon, "firstNotif")
+ assertIsNotifChip(latest!!.active[2], context, secondIcon, "secondNotif")
assertThat(latest!!.overflow.size).isEqualTo(1)
- assertIsNotifChip(latest!!.overflow[0], context, secondIcon, "secondNotif")
+ assertIsNotifChip(latest!!.overflow[0], context, thirdIcon, "thirdNotif")
assertThat(latest!!.inactive.size).isEqualTo(3)
assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy())
}
@DisableChipsModernization
@Test
- fun chipsLegacy_screenRecordAndCallAndPromotedNotifs_notifsNotShown() =
+ fun chipsLegacy_screenRecordAndCallAndPromotedNotif_notifNotShown() =
kosmos.runTest {
val callNotificationKey = "call"
val latest by collectLastValue(underTest.chipsLegacy)
val unused by collectLastValue(underTest.chips)
- addOngoingCallState(callNotificationKey)
+ addOngoingCallState(callNotificationKey, isAppVisible = false)
screenRecordState.value = ScreenRecordModel.Recording
activeNotificationListRepository.addNotif(
activeNotificationModel(
@@ -1016,7 +1069,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
}
@Test
- fun visibleChipKeys_screenRecordAndCallAndPromotedNotifs_topTwoInList() =
+ fun visibleChipKeys_screenRecordAndCallAndPromotedNotifs_topThreeInList() =
kosmos.runTest {
val latest by collectLastValue(underTest.visibleChipKeys)
@@ -1025,20 +1078,27 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
screenRecordState.value = ScreenRecordModel.Recording
activeNotificationListRepository.addNotif(
activeNotificationModel(
- key = "notif",
+ key = "notif1",
statusBarChipIcon = createStatusBarIconViewOrNull(),
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+ promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
+ )
+ )
+ activeNotificationListRepository.addNotif(
+ activeNotificationModel(
+ key = "notif2",
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
+ promotedContent = PromotedNotificationContentModel.Builder("notif2").build(),
)
)
assertThat(latest)
- .containsExactly(ScreenRecordChipViewModel.KEY, callNotificationKey)
+ .containsExactly(ScreenRecordChipViewModel.KEY, callNotificationKey, "notif1")
.inOrder()
}
@EnableChipsModernization
@Test
- fun chips_screenRecordAndCallAndPromotedNotif_notifInOverflow() =
+ fun chips_screenRecordAndCallAndPromotedNotifs_secondNotifInOverflow() =
kosmos.runTest {
val latest by collectLastValue(underTest.chips)
val unused by collectLastValue(underTest.chipsLegacy)
@@ -1055,11 +1115,22 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
)
addOngoingCallState(key = callNotificationKey)
- assertThat(latest!!.active.size).isEqualTo(2)
+ // This is the overflow notif
+ val notifIcon2 = createStatusBarIconViewOrNull()
+ activeNotificationListRepository.addNotif(
+ activeNotificationModel(
+ key = "notif2",
+ statusBarChipIcon = notifIcon2,
+ promotedContent = PromotedNotificationContentModel.Builder("notif2").build(),
+ )
+ )
+
+ assertThat(latest!!.active.size).isEqualTo(3)
assertIsScreenRecordChip(latest!!.active[0])
assertIsCallChip(latest!!.active[1], callNotificationKey, context)
+ assertIsNotifChip(latest!!.active[2], context, notifIcon, "notif")
assertThat(latest!!.overflow.size).isEqualTo(1)
- assertIsNotifChip(latest!!.overflow[0], context, notifIcon, "notif")
+ assertIsNotifChip(latest!!.overflow[0], context, notifIcon2, "notif2")
assertThat(latest!!.inactive.size).isEqualTo(2)
assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy())
}
@@ -1089,7 +1160,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
assertIsNotifChip(latest, context, notifIcon, "notif")
// WHEN the higher priority call chip is added
- addOngoingCallState(callNotificationKey)
+ addOngoingCallState(callNotificationKey, isAppVisible = false)
// THEN the higher priority call chip is used
assertIsCallChip(latest, callNotificationKey, context)
@@ -1120,7 +1191,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
screenRecordState.value = ScreenRecordModel.Recording
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
- addOngoingCallState(callNotificationKey)
+ addOngoingCallState(callNotificationKey, isAppVisible = false)
val notifIcon = createStatusBarIconViewOrNull()
activeNotificationListRepository.addNotif(
activeNotificationModel(
@@ -1182,7 +1253,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel())
// WHEN the higher priority call chip is added
- addOngoingCallState(callNotificationKey)
+ addOngoingCallState(callNotificationKey, isAppVisible = false)
// THEN the higher priority call chip is used as primary and notif is demoted to
// secondary
@@ -1234,15 +1305,16 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
@Test
fun chips_movesChipsAroundAccordingToPriority() =
kosmos.runTest {
+ systemClock.setCurrentTimeMillis(10_000)
val callNotificationKey = "call"
// Start with just the lowest priority chip active
- val notifIcon = createStatusBarIconViewOrNull()
+ val notif1Icon = createStatusBarIconViewOrNull()
setNotifs(
listOf(
activeNotificationModel(
- key = "notif",
- statusBarChipIcon = notifIcon,
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+ key = "notif1",
+ statusBarChipIcon = notif1Icon,
+ promotedContent = PromotedNotificationContentModel.Builder("notif1").build(),
)
)
)
@@ -1254,7 +1326,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
val unused by collectLastValue(underTest.chipsLegacy)
assertThat(latest!!.active.size).isEqualTo(1)
- assertIsNotifChip(latest!!.active[0], context, notifIcon, "notif")
+ assertIsNotifChip(latest!!.active[0], context, notif1Icon, "notif1")
assertThat(latest!!.overflow).isEmpty()
assertThat(latest!!.inactive.size).isEqualTo(4)
assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy())
@@ -1262,10 +1334,10 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
// WHEN the higher priority call chip is added
addOngoingCallState(key = callNotificationKey)
- // THEN the higher priority call chip and notif are active in that order
+ // THEN the higher priority call chip and notif1 are active in that order
assertThat(latest!!.active.size).isEqualTo(2)
assertIsCallChip(latest!!.active[0], callNotificationKey, context)
- assertIsNotifChip(latest!!.active[1], context, notifIcon, "notif")
+ assertIsNotifChip(latest!!.active[1], context, notif1Icon, "notif1")
assertThat(latest!!.overflow).isEmpty()
assertThat(latest!!.inactive.size).isEqualTo(3)
assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy())
@@ -1278,56 +1350,63 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
createTask(taskId = 1),
)
- // THEN the higher priority media projection chip and call are active in that order, and
- // notif is demoted to overflow
- assertThat(latest!!.active.size).isEqualTo(2)
+ // THEN media projection, then call, then notif1 are active
+ assertThat(latest!!.active.size).isEqualTo(3)
assertIsShareToAppChip(latest!!.active[0])
assertIsCallChip(latest!!.active[1], callNotificationKey, context)
- assertThat(latest!!.overflow.size).isEqualTo(1)
- assertIsNotifChip(latest!!.overflow[0], context, notifIcon, "notif")
+ assertIsNotifChip(latest!!.active[2], context, notif1Icon, "notif1")
+ assertThat(latest!!.overflow).isEmpty()
assertThat(latest!!.inactive.size).isEqualTo(2)
assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy())
- // WHEN the higher priority screen record chip is added
+ // WHEN the screen record chip is added, which replaces media projection
screenRecordState.value = ScreenRecordModel.Recording
+ // AND another notification is added
+ systemClock.advanceTime(2_000)
+ val notif2Icon = createStatusBarIconViewOrNull()
+ activeNotificationListRepository.addNotif(
+ activeNotificationModel(
+ key = "notif2",
+ statusBarChipIcon = notif2Icon,
+ promotedContent = PromotedNotificationContentModel.Builder("notif2").build(),
+ )
+ )
- // THEN the higher priority screen record chip and call are active in that order, and
- // media projection and notif are demoted in overflow
- assertThat(latest!!.active.size).isEqualTo(2)
+ // THEN screen record, then call, then notif2 are active
+ assertThat(latest!!.active.size).isEqualTo(3)
assertIsScreenRecordChip(latest!!.active[0])
assertIsCallChip(latest!!.active[1], callNotificationKey, context)
+ assertIsNotifChip(latest!!.active[2], context, notif2Icon, "notif2")
+
+ // AND notif1 and media projection is demoted in overflow
assertThat(latest!!.overflow.size).isEqualTo(2)
assertIsShareToAppChip(latest!!.overflow[0])
- assertIsNotifChip(latest!!.overflow[1], context, notifIcon, "notif")
+ assertIsNotifChip(latest!!.overflow[1], context, notif1Icon, "notif1")
assertThat(latest!!.inactive.size).isEqualTo(1)
assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy())
- // WHEN screen record and call is dropped
+ // WHEN screen record and call are dropped
screenRecordState.value = ScreenRecordModel.DoingNothing
- setNotifs(
- listOf(
- activeNotificationModel(
- key = "notif",
- statusBarChipIcon = notifIcon,
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
- )
- )
- )
+ removeOngoingCallState(callNotificationKey)
- // THEN media projection and notif remain
- assertThat(latest!!.active.size).isEqualTo(2)
+ // THEN media projection, notif2, and notif1 remain
+ assertThat(latest!!.active.size).isEqualTo(3)
assertIsShareToAppChip(latest!!.active[0])
- assertIsNotifChip(latest!!.active[1], context, notifIcon, "notif")
+ assertIsNotifChip(latest!!.active[1], context, notif2Icon, "notif2")
+ assertIsNotifChip(latest!!.active[2], context, notif1Icon, "notif1")
assertThat(latest!!.overflow).isEmpty()
assertThat(latest!!.inactive.size).isEqualTo(3)
assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy())
// WHEN media projection is dropped
mediaProjectionState.value = MediaProjectionState.NotProjecting
+ // AND notif2 is dropped
+ systemClock.advanceTime(2_000)
+ activeNotificationListRepository.removeNotif("notif2")
- // THEN only notif is active
+ // THEN only notif1 is active
assertThat(latest!!.active.size).isEqualTo(1)
- assertIsNotifChip(latest!!.active[0], context, notifIcon, "notif")
+ assertIsNotifChip(latest!!.active[0], context, notif1Icon, "notif1")
assertThat(latest!!.overflow).isEmpty()
assertThat(latest!!.inactive.size).isEqualTo(4)
assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy())
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationCloseButtonTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationCloseButtonTest.kt
new file mode 100644
index 000000000000..e4b2e6c9b359
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationCloseButtonTest.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification
+
+import android.app.Notification
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.testing.TestableLooper
+import android.view.MotionEvent
+import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.row.NotificationTestHelper
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.stub
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import org.junit.Before
+
+private fun getCloseButton(row: ExpandableNotificationRow): View {
+ val contractedView = row.showingLayout?.contractedChild!!
+ return contractedView.findViewById(com.android.internal.R.id.close_button)
+}
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NotificationCloseButtonTest : SysuiTestCase() {
+ private lateinit var helper: NotificationTestHelper
+
+ @Before
+ fun setUp() {
+ helper = NotificationTestHelper(
+ mContext,
+ mDependency,
+ TestableLooper.get(this)
+ )
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_NOTIFICATION_ADD_X_ON_HOVER_TO_DISMISS)
+ fun verifyWhenFeatureDisabled() {
+ // Enable the notification row to dismiss.
+ helper.dismissibilityProvider.stub {
+ on { isDismissable(any()) } doReturn true
+ }
+
+ // By default, the close button should be gone.
+ val row = createNotificationRow()
+ val closeButton = getCloseButton(row)
+ assertThat(closeButton).isNotNull()
+ assertThat(closeButton.visibility).isEqualTo(View.GONE)
+
+ val hoverEnterEvent = MotionEvent.obtain(
+ 0/*downTime=*/,
+ 0/*eventTime=*/,
+ MotionEvent.ACTION_HOVER_ENTER,
+ 0f/*x=*/,
+ 0f/*y=*/,
+ 0/*metaState*/
+ )
+
+ // The close button should not show if the feature is disabled.
+ row.onInterceptHoverEvent(hoverEnterEvent)
+ assertThat(closeButton.visibility).isEqualTo(View.GONE)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NOTIFICATION_ADD_X_ON_HOVER_TO_DISMISS)
+ fun verifyOnDismissableNotification() {
+ // Enable the notification row to dismiss.
+ helper.dismissibilityProvider.stub {
+ on { isDismissable(any()) } doReturn true
+ }
+
+ // By default, the close button should be gone.
+ val row = createNotificationRow()
+ val closeButton = getCloseButton(row)
+ assertThat(closeButton).isNotNull()
+ assertThat(closeButton.visibility).isEqualTo(View.GONE)
+
+ val hoverEnterEvent = MotionEvent.obtain(
+ 0/*downTime=*/,
+ 0/*eventTime=*/,
+ MotionEvent.ACTION_HOVER_ENTER,
+ 0f/*x=*/,
+ 0f/*y=*/,
+ 0/*metaState*/
+ )
+
+ // When the row is hovered, the close button should show.
+ row.onInterceptHoverEvent(hoverEnterEvent)
+ assertThat(closeButton.visibility).isEqualTo(View.VISIBLE)
+
+ val hoverExitEvent = MotionEvent.obtain(
+ 0/*downTime=*/,
+ 0/*eventTime=*/,
+ MotionEvent.ACTION_HOVER_EXIT,
+ 0f/*x=*/,
+ 0f/*y=*/,
+ 0/*metaState*/
+ )
+
+ // When hover exits the row, the close button should be gone again.
+ row.onInterceptHoverEvent(hoverExitEvent)
+ assertThat(closeButton.visibility).isEqualTo(View.GONE)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NOTIFICATION_ADD_X_ON_HOVER_TO_DISMISS)
+ fun verifyOnUndismissableNotification() {
+ // By default, the close button should be gone.
+ val row = createNotificationRow()
+ val closeButton = getCloseButton(row)
+ assertThat(closeButton).isNotNull()
+ assertThat(closeButton.visibility).isEqualTo(View.GONE)
+
+ val hoverEnterEvent = MotionEvent.obtain(
+ 0/*downTime=*/,
+ 0/*eventTime=*/,
+ MotionEvent.ACTION_HOVER_ENTER,
+ 0f/*x=*/,
+ 0f/*y=*/,
+ 0/*metaState*/
+ )
+
+ // Because the host notification cannot be dismissed, the close button should not show.
+ row.onInterceptHoverEvent(hoverEnterEvent)
+ assertThat(closeButton.visibility).isEqualTo(View.GONE)
+ }
+
+ private fun createNotificationRow(): ExpandableNotificationRow {
+ val notification = Notification.Builder(context, "channel")
+ .setContentTitle("title")
+ .setContentText("text")
+ .setSmallIcon(R.drawable.ic_person)
+ .build()
+
+ return helper.createRow(notification)
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt
index b6889afa4e8a..faafa073be4c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt
@@ -29,8 +29,10 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.res.R
import com.android.systemui.statusbar.RankingBuilder
+import com.android.systemui.statusbar.notification.mockNotificationActivityStarter
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.entryAdapterFactory
+import com.android.systemui.statusbar.notification.row.mockNotificationActionClickManager
import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING
import com.android.systemui.testKosmos
@@ -355,16 +357,27 @@ class NotificationEntryAdapterTest : SysuiTestCase() {
val notification: Notification =
Notification.Builder(mContext, "").setSmallIcon(R.drawable.ic_person).build()
- val entry =
- NotificationEntryBuilder()
- .setNotification(notification)
- .setImportance(NotificationManager.IMPORTANCE_MIN)
- .build()
+ val entry = NotificationEntryBuilder().setNotification(notification).build()
underTest = factory.create(entry) as NotificationEntryAdapter
underTest.onNotificationBubbleIconClicked()
- verify((factory as? EntryAdapterFactoryImpl)?.getNotificationActivityStarter())
- ?.onNotificationBubbleIconClicked(entry)
+ verify(kosmos.mockNotificationActivityStarter).onNotificationBubbleIconClicked(entry)
+ }
+
+ @Test
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
+ fun onNotificationActionClicked() {
+ val notification: Notification =
+ Notification.Builder(mContext, "")
+ .setSmallIcon(R.drawable.ic_person)
+ .addAction(Mockito.mock(Notification.Action::class.java))
+ .build()
+
+ val entry = NotificationEntryBuilder().setNotification(notification).build()
+
+ underTest = factory.create(entry) as NotificationEntryAdapter
+ underTest.onNotificationActionClicked()
+ verify(kosmos.mockNotificationActionClickManager).onNotificationActionClicked(entry)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinatorTest.kt
index 8a9720ea3cb0..732180810880 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinatorTest.kt
@@ -34,6 +34,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
+import kotlin.test.assertEquals
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -87,6 +88,18 @@ class BundleCoordinatorTest : SysuiTestCase() {
isFalse()
}
+ @Test
+ fun testBundler_getBundleIdOrNull_returnBundleId() {
+ val classifiedEntry = makeEntryOfChannelType(PROMOTIONS_ID)
+ assertEquals(coordinator.bundler.getBundleIdOrNull(classifiedEntry), PROMOTIONS_ID)
+ }
+
+ @Test
+ fun testBundler_getBundleIdOrNull_returnNull() {
+ val unclassifiedEntry = makeEntryOfChannelType("not system channel")
+ assertEquals(coordinator.bundler.getBundleIdOrNull(unclassifiedEntry), null)
+ }
+
private fun makeEntryOfChannelType(
type: String,
buildBlock: NotificationEntryBuilder.() -> Unit = {}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
index 30983550f0f9..44d88c31c5f1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
@@ -50,6 +50,8 @@ import com.android.systemui.statusbar.notification.interruption.NotificationInte
import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderWrapper.DecisionImpl
import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderWrapper.FullScreenIntentDecisionImpl
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider
+import com.android.systemui.statusbar.notification.row.mockNotificationActionClickManager
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
import com.android.systemui.statusbar.phone.NotificationGroupTestHelper
import com.android.systemui.testKosmos
import com.android.systemui.util.concurrency.FakeExecutor
@@ -138,6 +140,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
headsUpViewBinder,
visualInterruptionDecisionProvider,
remoteInputManager,
+ kosmos.mockNotificationActionClickManager,
launchFullScreenIntentProvider,
flags,
statusBarNotificationChipsInteractor,
@@ -161,8 +164,14 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
verify(notifPipeline).addOnBeforeFinalizeFilterListener(capture())
}
onHeadsUpChangedListener = withArgCaptor { verify(headsUpManager).addListener(capture()) }
- actionPressListener = withArgCaptor {
- verify(remoteInputManager).addActionPressListener(capture())
+ actionPressListener = if (NotificationBundleUi.isEnabled) {
+ withArgCaptor {
+ verify(kosmos.mockNotificationActionClickManager).addActionClickListener(capture())
+ }
+ } else {
+ withArgCaptor {
+ verify(remoteInputManager).addActionPressListener(capture())
+ }
}
given(headsUpManager.allEntries).willAnswer { huns.stream() }
given(headsUpManager.isHeadsUpEntry(anyString())).willAnswer { invocation ->
@@ -260,7 +269,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
addHUN(entry)
actionPressListener.accept(entry)
- verify(headsUpManager, times(1)).setUserActionMayIndirectlyRemove(entry)
+ verify(headsUpManager, times(1)).setUserActionMayIndirectlyRemove(entry.key)
whenever(headsUpManager.canRemoveImmediately(anyString())).thenReturn(true)
assertFalse(notifLifetimeExtender.maybeExtendLifetime(entry, 0))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt
index a90539413adb..e28e587d2cdc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt
@@ -28,6 +28,7 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.statusbar.statusBarStateController
import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.statusbar.SysuiStatusBarStateController
+import com.android.systemui.statusbar.notification.collection.BundleEntry
import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.NotificationEntry
@@ -280,6 +281,8 @@ class LockScreenMinimalismCoordinatorTest : SysuiTestCase() {
val group = GroupEntryBuilder().setSummary(parent).addChild(child1).addChild(child2).build()
val listEntryList = listOf(group, solo1, solo2)
val notificationEntryList = listOf(solo1, solo2, parent, child1, child2)
+ val bundle = BundleEntry("bundleKey")
+ val bundleList = listOf(bundle)
runCoordinatorTest {
// All entries are added (and now unseen)
@@ -300,6 +303,11 @@ class LockScreenMinimalismCoordinatorTest : SysuiTestCase() {
assertThatTopOngoingKey().isEqualTo(null)
assertThatTopUnseenKey().isEqualTo(solo1.key)
+ // TEST: bundle is not picked
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(bundleList)
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(null)
+
// TEST: if top-ranked unseen is colorized, fall back to #2 ranked unseen
solo1.setColorizedFgs(true)
onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
index e22acd53e584..8560b66d961f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
@@ -37,6 +37,7 @@ import com.android.systemui.flags.andSceneContainer
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.log.assertLogsWtfs
import com.android.systemui.res.R
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.shade.shadeTestUtil
@@ -205,8 +206,7 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() {
fun pinnedHeadsUpStatuses_pinnedByUser_butFlagOff_returnsNotPinned() {
val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
entry.row = testHelper.createRow()
- underTest.showNotification(entry, isPinnedByUser = true)
-
+ assertLogsWtfs { underTest.showNotification(entry, isPinnedByUser = true) }
assertThat(underTest.hasPinnedHeadsUp()).isFalse()
assertThat(underTest.pinnedHeadsUpStatus()).isEqualTo(PinnedStatus.NotPinned)
}
@@ -1071,7 +1071,7 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() {
assertThat(underTest.canRemoveImmediately(notifEntry.key)).isFalse()
- underTest.setUserActionMayIndirectlyRemove(notifEntry)
+ underTest.setUserActionMayIndirectlyRemove(notifEntry.key)
assertThat(underTest.canRemoveImmediately(notifEntry.key)).isTrue()
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index 8e7733bb23ca..e6b2c2541447 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
@@ -29,6 +29,7 @@ import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -60,11 +61,11 @@ import com.android.systemui.TestableDependency;
import com.android.systemui.classifier.FalsingManagerFake;
import com.android.systemui.flags.FakeFeatureFlagsClassic;
import com.android.systemui.flags.FeatureFlagsClassic;
+import com.android.systemui.media.NotificationMediaManager;
import com.android.systemui.media.controls.util.MediaFeatureFlag;
import com.android.systemui.media.dialog.MediaOutputDialogManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.res.R;
-import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.SmartReplyController;
@@ -363,6 +364,7 @@ public class NotificationTestHelper {
.setUid(UID)
.setInitialPid(2000)
.setNotification(summary)
+ .setUser(USER_HANDLE)
.setParent(GroupEntry.ROOT_ENTRY)
.build();
GroupEntryBuilder groupEntry = new GroupEntryBuilder()
@@ -743,11 +745,12 @@ public class NotificationTestHelper {
mock(MetricsLogger.class),
mock(PeopleNotificationIdentifier.class),
mock(NotificationIconStyleProvider.class),
- mock(VisualStabilityCoordinator.class)
+ mock(VisualStabilityCoordinator.class),
+ mock(NotificationActionClickManager.class)
).create(entry);
row.initialize(
- entryAdapter,
+ spy(entryAdapter),
entry,
mock(RemoteInputViewSubcomponent.Factory.class),
APP_NAME,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt
index f6c031f54818..8e3bdc48398f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt
@@ -219,7 +219,6 @@ class StackStateAnimatorTest : SysuiTestCase() {
)
}
- @DisableFlags(Flags.FLAG_PHYSICAL_NOTIFICATION_MOVEMENT)
@Test
@EnableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME)
fun startAnimationForEvents_headsUpFromBottom_startsHeadsUpAppearAnim_flagOn() {
@@ -246,7 +245,7 @@ class StackStateAnimatorTest : SysuiTestCase() {
}
@Test
- @DisableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME)
+ @DisableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME, Flags.FLAG_PHYSICAL_NOTIFICATION_MOVEMENT)
fun startAnimationForEvents_startsHeadsUpDisappearAnim_flagOff() {
val disappearDuration = ANIMATION_DURATION_HEADS_UP_DISAPPEAR.toLong()
val event = AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR)
@@ -277,6 +276,7 @@ class StackStateAnimatorTest : SysuiTestCase() {
@Test
@EnableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME)
+ @DisableFlags(Flags.FLAG_PHYSICAL_NOTIFICATION_MOVEMENT)
fun startAnimationForEvents_startsHeadsUpDisappearAnim_flagOn() {
val disappearDuration = ANIMATION_DURATION_HEADS_UP_DISAPPEAR.toLong()
val event = AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt
index 14e7cdc50227..3b836b774788 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt
@@ -16,11 +16,11 @@
package com.android.systemui.statusbar.notification.stack.ui.viewmodel
-import android.testing.TestableLooper.RunWithLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.kosmos.testScope
import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor
import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds
@@ -32,7 +32,7 @@ import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidJUnit4::class)
-@RunWithLooper
+@EnableSceneContainer
class NotificationsPlaceholderViewModelTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val underTest by lazy { kosmos.notificationsPlaceholderViewModel }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
index e2330f448a1b..1ea41de63e64 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
@@ -61,8 +61,8 @@ import com.android.systemui.keyguard.shared.model.KeyguardState;
import com.android.systemui.keyguard.shared.model.TransitionState;
import com.android.systemui.keyguard.shared.model.TransitionStep;
import com.android.systemui.log.SessionTracker;
+import com.android.systemui.media.NotificationMediaManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.statusbar.policy.KeyguardStateController;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
index bab349aa7a74..f4204af7829b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
@@ -82,6 +82,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
statusBarChipIconView = testIconView,
contentIntent = testIntent,
promotedContent = testPromotedContent,
+ isAppVisible = false,
)
// Verify model is InCall and has the correct icon, intent, and promoted content.
@@ -92,12 +93,12 @@ class OngoingCallInteractorTest : SysuiTestCase() {
assertThat(model.intent).isSameInstanceAs(testIntent)
assertThat(model.notificationKey).isEqualTo(key)
assertThat(model.promotedContent).isSameInstanceAs(testPromotedContent)
+ assertThat(model.isAppVisible).isFalse()
}
@Test
fun ongoingCallNotification_setsAllFields_withAppVisible() =
kosmos.runTest {
- kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = true
val latest by collectLastValue(underTest.ongoingCallState)
// Set up notification with icon view and intent
@@ -112,17 +113,19 @@ class OngoingCallInteractorTest : SysuiTestCase() {
statusBarChipIconView = testIconView,
contentIntent = testIntent,
promotedContent = testPromotedContent,
+ isAppVisible = true,
)
- // Verify model is InCallWithVisibleApp and has the correct icon, intent, and promoted
- // content.
- assertThat(latest).isInstanceOf(OngoingCallModel.InCallWithVisibleApp::class.java)
- val model = latest as OngoingCallModel.InCallWithVisibleApp
+ // Verify model is InCall with visible app and has the correct icon, intent, and
+ // promoted content.
+ assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java)
+ val model = latest as OngoingCallModel.InCall
assertThat(model.startTimeMs).isEqualTo(startTimeMs)
assertThat(model.notificationIconView).isSameInstanceAs(testIconView)
assertThat(model.intent).isSameInstanceAs(testIntent)
assertThat(model.notificationKey).isEqualTo(key)
assertThat(model.promotedContent).isSameInstanceAs(testPromotedContent)
+ assertThat(model.isAppVisible).isTrue()
}
@Test
@@ -139,23 +142,23 @@ class OngoingCallInteractorTest : SysuiTestCase() {
@Test
fun ongoingCallNotification_appVisibleInitially_emitsInCallWithVisibleApp() =
kosmos.runTest {
- kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = true
val latest by collectLastValue(underTest.ongoingCallState)
- addOngoingCallState(uid = UID)
+ addOngoingCallState(uid = UID, isAppVisible = true)
- assertThat(latest).isInstanceOf(OngoingCallModel.InCallWithVisibleApp::class.java)
+ assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java)
+ assertThat((latest as OngoingCallModel.InCall).isAppVisible).isTrue()
}
@Test
fun ongoingCallNotification_appNotVisibleInitially_emitsInCall() =
kosmos.runTest {
- kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false
val latest by collectLastValue(underTest.ongoingCallState)
- addOngoingCallState(uid = UID)
+ addOngoingCallState(uid = UID, isAppVisible = false)
assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java)
+ assertThat((latest as OngoingCallModel.InCall).isAppVisible).isFalse()
}
@Test
@@ -164,17 +167,19 @@ class OngoingCallInteractorTest : SysuiTestCase() {
val latest by collectLastValue(underTest.ongoingCallState)
// Start with notification and app not visible
- kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false
- addOngoingCallState(uid = UID)
+ addOngoingCallState(uid = UID, isAppVisible = false)
assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java)
+ assertThat((latest as OngoingCallModel.InCall).isAppVisible).isFalse()
// App becomes visible
kosmos.activityManagerRepository.fake.setIsAppVisible(UID, true)
- assertThat(latest).isInstanceOf(OngoingCallModel.InCallWithVisibleApp::class.java)
+ assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java)
+ assertThat((latest as OngoingCallModel.InCall).isAppVisible).isTrue()
// App becomes invisible again
kosmos.activityManagerRepository.fake.setIsAppVisible(UID, false)
assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java)
+ assertThat((latest as OngoingCallModel.InCall).isAppVisible).isFalse()
}
@Test
@@ -238,18 +243,17 @@ class OngoingCallInteractorTest : SysuiTestCase() {
.ongoingProcessRequiresStatusBarVisible
)
- kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false
-
- addOngoingCallState(uid = UID)
+ addOngoingCallState(uid = UID, isAppVisible = false)
assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java)
+ assertThat((ongoingCallState as OngoingCallModel.InCall).isAppVisible).isFalse()
assertThat(requiresStatusBarVisibleInRepository).isTrue()
assertThat(requiresStatusBarVisibleInWindowController).isTrue()
kosmos.activityManagerRepository.fake.setIsAppVisible(UID, true)
- assertThat(ongoingCallState)
- .isInstanceOf(OngoingCallModel.InCallWithVisibleApp::class.java)
+ assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java)
+ assertThat((ongoingCallState as OngoingCallModel.InCall).isAppVisible).isTrue()
assertThat(requiresStatusBarVisibleInRepository).isFalse()
assertThat(requiresStatusBarVisibleInWindowController).isFalse()
}
@@ -265,6 +269,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
addOngoingCallState()
assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java)
+ assertThat((ongoingCallState as OngoingCallModel.InCall).isAppVisible).isFalse()
verify(kosmos.swipeStatusBarAwayGestureHandler, never())
.addOnGestureDetectedCallback(any(), any())
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosTest.kt
index c89dc5722c7a..33689931e6ed 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 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.
@@ -26,789 +26,701 @@ import com.android.settingslib.mobile.MobileIconCarrierIdOverrides
import com.android.settingslib.mobile.MobileIconCarrierIdOverridesImpl
import com.android.settingslib.mobile.TelephonyIcons
import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS
+import com.android.systemui.flags.fake
+import com.android.systemui.flags.featureFlagsClassic
+import com.android.systemui.kairos.ActivatedKairosFixture
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosTestScope
+import com.android.systemui.kairos.MutableState
+import com.android.systemui.kairos.kairos
+import com.android.systemui.kairos.map
+import com.android.systemui.kairos.runKairosTest
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.log.table.logcatTableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.CarrierMergedNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepositoryKairos
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileMappingsProxy
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.FIVE_G_OVERRIDE
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.FOUR_G
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.THREE_G
import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel
import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
-import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyString
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+@OptIn(ExperimentalKairosApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class MobileIconInteractorKairosTest : SysuiTestCase() {
- private val kosmos = testKosmos()
+ private val kosmos =
+ testKosmos().apply {
+ useUnconfinedTestDispatcher()
+ featureFlagsClassic.fake.apply { setDefault(FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS) }
+ }
- private lateinit var underTest: MobileIconInteractorKairos
- private val mobileMappingsProxy = FakeMobileMappingsProxy()
- private val mobileIconsInteractor = FakeMobileIconsInteractor(mobileMappingsProxy, mock())
+ private val Kosmos.tableLogBuffer by Fixture {
+ logcatTableLogBuffer(this, "MobileIconInteractorKairosTest")
+ }
- private val connectionRepository =
- FakeMobileConnectionRepository(
- SUB_1_ID,
- logcatTableLogBuffer(kosmos, "MobileIconInteractorTest"),
- )
+ private var Kosmos.overrides: MobileIconCarrierIdOverrides by Fixture {
+ MobileIconCarrierIdOverridesImpl()
+ }
- private val testDispatcher = UnconfinedTestDispatcher()
- private val testScope = TestScope(testDispatcher)
+ private val Kosmos.defaultSubscriptionHasDataEnabled by Fixture { MutableState(kairos, true) }
- @Before
- fun setUp() {
- underTest = createInteractor()
+ private val Kosmos.alwaysShowDataRatIcon by Fixture { MutableState(kairos, false) }
- mobileIconsInteractor.activeDataConnectionHasDataEnabled.value = true
- connectionRepository.isInService.value = true
- }
+ private val Kosmos.alwaysUseCdmaLevel by Fixture { MutableState(kairos, false) }
- @Test
- fun gsm_usesGsmLevel() =
- testScope.runTest {
- connectionRepository.isGsm.value = true
- connectionRepository.primaryLevel.value = GSM_LEVEL
- connectionRepository.cdmaLevel.value = CDMA_LEVEL
+ private val Kosmos.isSingleCarrier by Fixture { MutableState(kairos, true) }
- var latest: Int? = null
- val job = underTest.signalLevelIcon.onEach { latest = it.level }.launchIn(this)
+ private val Kosmos.mobileIsDefault by Fixture { MutableState(kairos, false) }
- assertThat(latest).isEqualTo(GSM_LEVEL)
+ private val Kosmos.defaultMobileIconMapping by Fixture {
+ MutableState(kairos, fakeMobileIconsInteractor.TEST_MAPPING)
+ }
- job.cancel()
- }
+ private val Kosmos.defaultMobileIconGroup by Fixture { MutableState(kairos, TelephonyIcons.G) }
- @Test
- fun gsm_alwaysShowCdmaTrue_stillUsesGsmLevel() =
- testScope.runTest {
- connectionRepository.isGsm.value = true
- connectionRepository.primaryLevel.value = GSM_LEVEL
- connectionRepository.cdmaLevel.value = CDMA_LEVEL
- mobileIconsInteractor.alwaysUseCdmaLevel.value = true
+ private val Kosmos.isDefaultConnectionFailed by Fixture { MutableState(kairos, false) }
- var latest: Int? = null
- val job = underTest.signalLevelIcon.onEach { latest = it.level }.launchIn(this)
+ private val Kosmos.isForceHidden by Fixture { MutableState(kairos, false) }
- assertThat(latest).isEqualTo(GSM_LEVEL)
+ private val Kosmos.underTest by ActivatedKairosFixture {
+ MobileIconInteractorKairosImpl(
+ defaultSubscriptionHasDataEnabled,
+ alwaysShowDataRatIcon,
+ alwaysUseCdmaLevel,
+ isSingleCarrier,
+ mobileIsDefault,
+ defaultMobileIconMapping,
+ defaultMobileIconGroup,
+ isDefaultConnectionFailed,
+ isForceHidden,
+ connectionRepository = connectionRepo,
+ context = context,
+ carrierIdOverrides = overrides,
+ )
+ }
- job.cancel()
+ private val Kosmos.connectionRepo by Fixture {
+ FakeMobileConnectionRepositoryKairos(SUB_1_ID, kairos, tableLogBuffer).apply {
+ dataEnabled.setValue(true)
+ isInService.setValue(true)
}
+ }
+
+ private fun runTest(block: suspend KairosTestScope.() -> Unit) =
+ kosmos.run { runKairosTest { block() } }
@Test
- fun notGsm_level_default_unknown() =
- testScope.runTest {
- connectionRepository.isGsm.value = false
+ fun gsm_usesGsmLevel() = runTest {
+ connectionRepo.isGsm.setValue(true)
+ connectionRepo.primaryLevel.setValue(GSM_LEVEL)
+ connectionRepo.cdmaLevel.setValue(CDMA_LEVEL)
- var latest: Int? = null
- val job = underTest.signalLevelIcon.onEach { latest = it.level }.launchIn(this)
+ val latest by underTest.signalLevelIcon.collectLastValue()
- assertThat(latest).isEqualTo(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN)
- job.cancel()
- }
+ assertThat(latest?.level).isEqualTo(GSM_LEVEL)
+ }
@Test
- fun notGsm_alwaysShowCdmaTrue_usesCdmaLevel() =
- testScope.runTest {
- connectionRepository.isGsm.value = false
- connectionRepository.primaryLevel.value = GSM_LEVEL
- connectionRepository.cdmaLevel.value = CDMA_LEVEL
- mobileIconsInteractor.alwaysUseCdmaLevel.value = true
+ fun gsm_alwaysShowCdmaTrue_stillUsesGsmLevel() = runTest {
+ connectionRepo.isGsm.setValue(true)
+ connectionRepo.primaryLevel.setValue(GSM_LEVEL)
+ connectionRepo.cdmaLevel.setValue(CDMA_LEVEL)
+ // mobileIconsInteractor.alwaysUseCdmaLevel.setValue(true)
+ alwaysUseCdmaLevel.setValue(true)
- var latest: Int? = null
- val job = underTest.signalLevelIcon.onEach { latest = it.level }.launchIn(this)
+ val latest by underTest.signalLevelIcon.collectLastValue()
- assertThat(latest).isEqualTo(CDMA_LEVEL)
-
- job.cancel()
- }
+ assertThat(latest?.level).isEqualTo(GSM_LEVEL)
+ }
@Test
- fun notGsm_alwaysShowCdmaFalse_usesPrimaryLevel() =
- testScope.runTest {
- connectionRepository.isGsm.value = false
- connectionRepository.primaryLevel.value = GSM_LEVEL
- connectionRepository.cdmaLevel.value = CDMA_LEVEL
- mobileIconsInteractor.alwaysUseCdmaLevel.value = false
-
- var latest: Int? = null
- val job = underTest.signalLevelIcon.onEach { latest = it.level }.launchIn(this)
+ fun notGsm_level_default_unknown() = runTest {
+ connectionRepo.isGsm.setValue(false)
- assertThat(latest).isEqualTo(GSM_LEVEL)
+ val latest by underTest.signalLevelIcon.collectLastValue()
- job.cancel()
- }
+ assertThat(latest?.level).isEqualTo(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN)
+ }
@Test
- fun numberOfLevels_comesFromRepo_whenApplicable() =
- testScope.runTest {
- var latest: Int? = null
- val job =
- underTest.signalLevelIcon
- .onEach { latest = (it as? SignalIconModel.Cellular)?.numberOfLevels }
- .launchIn(this)
-
- connectionRepository.numberOfLevels.value = 5
- assertThat(latest).isEqualTo(5)
+ fun notGsm_alwaysShowCdmaTrue_usesCdmaLevel() = runTest {
+ connectionRepo.isGsm.setValue(false)
+ connectionRepo.primaryLevel.setValue(GSM_LEVEL)
+ connectionRepo.cdmaLevel.setValue(CDMA_LEVEL)
+ // mobileIconsInteractor.alwaysUseCdmaLevel.setValue(true)
+ alwaysUseCdmaLevel.setValue(true)
- connectionRepository.numberOfLevels.value = 4
- assertThat(latest).isEqualTo(4)
+ val latest by underTest.signalLevelIcon.collectLastValue()
- job.cancel()
- }
+ assertThat(latest?.level).isEqualTo(CDMA_LEVEL)
+ }
@Test
- fun inflateSignalStrength_arbitrarilyAddsOneToTheReportedLevel() =
- testScope.runTest {
- connectionRepository.inflateSignalStrength.value = false
- val latest by collectLastValue(underTest.signalLevelIcon)
+ fun notGsm_alwaysShowCdmaFalse_usesPrimaryLevel() = runTest {
+ connectionRepo.isGsm.setValue(false)
+ connectionRepo.primaryLevel.setValue(GSM_LEVEL)
+ connectionRepo.cdmaLevel.setValue(CDMA_LEVEL)
+ // mobileIconsInteractor.alwaysUseCdmaLevel.setValue(false)
+ alwaysUseCdmaLevel.setValue(false)
- connectionRepository.primaryLevel.value = 4
- assertThat(latest!!.level).isEqualTo(4)
+ val latest by underTest.signalLevelIcon.collectLastValue()
- connectionRepository.inflateSignalStrength.value = true
- connectionRepository.primaryLevel.value = 4
-
- // when INFLATE_SIGNAL_STRENGTH is true, we add 1 to the reported signal level
- assertThat(latest!!.level).isEqualTo(5)
- }
+ assertThat(latest?.level).isEqualTo(GSM_LEVEL)
+ }
@Test
- fun networkSlice_configOn_hasPrioritizedCaps_showsSlice() =
- testScope.runTest {
- connectionRepository.allowNetworkSliceIndicator.value = true
- val latest by collectLastValue(underTest.showSliceAttribution)
+ fun numberOfLevels_comesFromRepo_whenApplicable() = runTest {
+ val latest by
+ underTest.signalLevelIcon
+ .map { (it as? SignalIconModel.Cellular)?.numberOfLevels }
+ .collectLastValue()
- connectionRepository.hasPrioritizedNetworkCapabilities.value = true
+ connectionRepo.numberOfLevels.setValue(5)
+ assertThat(latest).isEqualTo(5)
- assertThat(latest).isTrue()
- }
+ connectionRepo.numberOfLevels.setValue(4)
+ assertThat(latest).isEqualTo(4)
+ }
@Test
- fun networkSlice_configOn_noPrioritizedCaps_noSlice() =
- testScope.runTest {
- connectionRepository.allowNetworkSliceIndicator.value = true
- val latest by collectLastValue(underTest.showSliceAttribution)
+ fun inflateSignalStrength_arbitrarilyAddsOneToTheReportedLevel() = runTest {
+ connectionRepo.inflateSignalStrength.setValue(false)
+ val latest by underTest.signalLevelIcon.collectLastValue()
- connectionRepository.hasPrioritizedNetworkCapabilities.value = false
+ connectionRepo.primaryLevel.setValue(4)
+ assertThat(latest!!.level).isEqualTo(4)
- assertThat(latest).isFalse()
- }
+ connectionRepo.inflateSignalStrength.setValue(true)
+ connectionRepo.primaryLevel.setValue(4)
+
+ // when INFLATE_SIGNAL_STRENGTH is true, we add 1 to the reported signal level
+ assertThat(latest!!.level).isEqualTo(5)
+ }
@Test
- fun networkSlice_configOff_hasPrioritizedCaps_noSlice() =
- testScope.runTest {
- connectionRepository.allowNetworkSliceIndicator.value = false
- val latest by collectLastValue(underTest.showSliceAttribution)
+ fun networkSlice_configOn_hasPrioritizedCaps_showsSlice() = runTest {
+ connectionRepo.allowNetworkSliceIndicator.setValue(true)
+ val latest by underTest.showSliceAttribution.collectLastValue()
- connectionRepository.hasPrioritizedNetworkCapabilities.value = true
+ connectionRepo.hasPrioritizedNetworkCapabilities.setValue(true)
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isTrue()
+ }
@Test
- fun networkSlice_configOff_noPrioritizedCaps_noSlice() =
- testScope.runTest {
- connectionRepository.allowNetworkSliceIndicator.value = false
- val latest by collectLastValue(underTest.showSliceAttribution)
+ fun networkSlice_configOn_noPrioritizedCaps_noSlice() = runTest {
+ connectionRepo.allowNetworkSliceIndicator.setValue(true)
+ val latest by underTest.showSliceAttribution.collectLastValue()
- connectionRepository.hasPrioritizedNetworkCapabilities.value = false
+ connectionRepo.hasPrioritizedNetworkCapabilities.setValue(false)
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isFalse()
+ }
@Test
- fun iconGroup_three_g() =
- testScope.runTest {
- connectionRepository.resolvedNetworkType.value =
- DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G))
-
- var latest: NetworkTypeIconModel? = null
- val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+ fun networkSlice_configOff_hasPrioritizedCaps_noSlice() = runTest {
+ connectionRepo.allowNetworkSliceIndicator.setValue(false)
+ val latest by underTest.showSliceAttribution.collectLastValue()
- assertThat(latest).isEqualTo(NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G))
+ connectionRepo.hasPrioritizedNetworkCapabilities.setValue(true)
- job.cancel()
- }
+ assertThat(latest).isFalse()
+ }
@Test
- fun iconGroup_updates_on_change() =
- testScope.runTest {
- connectionRepository.resolvedNetworkType.value =
- DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G))
+ fun networkSlice_configOff_noPrioritizedCaps_noSlice() = runTest {
+ connectionRepo.allowNetworkSliceIndicator.setValue(false)
+ val latest by underTest.showSliceAttribution.collectLastValue()
- var latest: NetworkTypeIconModel? = null
- val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+ connectionRepo.hasPrioritizedNetworkCapabilities.setValue(false)
- connectionRepository.resolvedNetworkType.value =
- DefaultNetworkType(mobileMappingsProxy.toIconKey(FOUR_G))
-
- assertThat(latest).isEqualTo(NetworkTypeIconModel.DefaultIcon(TelephonyIcons.FOUR_G))
-
- job.cancel()
- }
+ assertThat(latest).isFalse()
+ }
@Test
- fun iconGroup_5g_override_type() =
- testScope.runTest {
- connectionRepository.resolvedNetworkType.value =
- OverrideNetworkType(mobileMappingsProxy.toIconKeyOverride(FIVE_G_OVERRIDE))
-
- var latest: NetworkTypeIconModel? = null
- val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+ fun iconGroup_three_g() = runTest {
+ connectionRepo.resolvedNetworkType.setValue(
+ DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G))
+ )
- assertThat(latest).isEqualTo(NetworkTypeIconModel.DefaultIcon(TelephonyIcons.NR_5G))
+ val latest by underTest.networkTypeIconGroup.collectLastValue()
- job.cancel()
- }
+ assertThat(latest).isEqualTo(NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G))
+ }
@Test
- fun iconGroup_default_if_no_lookup() =
- testScope.runTest {
- connectionRepository.resolvedNetworkType.value =
- DefaultNetworkType(mobileMappingsProxy.toIconKey(NETWORK_TYPE_UNKNOWN))
+ fun iconGroup_updates_on_change() = runTest {
+ connectionRepo.resolvedNetworkType.setValue(
+ DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G))
+ )
- var latest: NetworkTypeIconModel? = null
- val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+ val latest by underTest.networkTypeIconGroup.collectLastValue()
- assertThat(latest)
- .isEqualTo(NetworkTypeIconModel.DefaultIcon(FakeMobileIconsInteractor.DEFAULT_ICON))
+ connectionRepo.resolvedNetworkType.setValue(
+ DefaultNetworkType(mobileMappingsProxy.toIconKey(FOUR_G))
+ )
- job.cancel()
- }
+ assertThat(latest).isEqualTo(NetworkTypeIconModel.DefaultIcon(TelephonyIcons.FOUR_G))
+ }
@Test
- fun iconGroup_carrierMerged_usesOverride() =
- testScope.runTest {
- connectionRepository.resolvedNetworkType.value = CarrierMergedNetworkType
-
- var latest: NetworkTypeIconModel? = null
- val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+ fun iconGroup_5g_override_type() = runTest {
+ connectionRepo.resolvedNetworkType.setValue(
+ OverrideNetworkType(mobileMappingsProxy.toIconKeyOverride(FIVE_G_OVERRIDE))
+ )
- assertThat(latest)
- .isEqualTo(
- NetworkTypeIconModel.DefaultIcon(CarrierMergedNetworkType.iconGroupOverride)
- )
+ val latest by underTest.networkTypeIconGroup.collectLastValue()
- job.cancel()
- }
+ assertThat(latest).isEqualTo(NetworkTypeIconModel.DefaultIcon(TelephonyIcons.NR_5G))
+ }
@Test
- fun overrideIcon_usesCarrierIdOverride() =
- testScope.runTest {
- val overrides =
- mock<MobileIconCarrierIdOverrides>().also {
- whenever(it.carrierIdEntryExists(anyInt())).thenReturn(true)
- whenever(it.getOverrideFor(anyInt(), anyString(), any())).thenReturn(1234)
- }
+ fun iconGroup_default_if_no_lookup() = runTest {
+ connectionRepo.resolvedNetworkType.setValue(
+ DefaultNetworkType(mobileMappingsProxy.toIconKey(NETWORK_TYPE_UNKNOWN))
+ )
- underTest = createInteractor(overrides)
+ val latest by underTest.networkTypeIconGroup.collectLastValue()
- connectionRepository.resolvedNetworkType.value =
- DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G))
+ assertThat(latest)
+ .isEqualTo(NetworkTypeIconModel.DefaultIcon(FakeMobileIconsInteractor.DEFAULT_ICON))
+ }
- var latest: NetworkTypeIconModel? = null
- val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+ @Test
+ fun iconGroup_carrierMerged_usesOverride() = runTest {
+ connectionRepo.resolvedNetworkType.setValue(CarrierMergedNetworkType)
- assertThat(latest)
- .isEqualTo(NetworkTypeIconModel.OverriddenIcon(TelephonyIcons.THREE_G, 1234))
+ val latest by underTest.networkTypeIconGroup.collectLastValue()
- job.cancel()
- }
+ assertThat(latest)
+ .isEqualTo(NetworkTypeIconModel.DefaultIcon(CarrierMergedNetworkType.iconGroupOverride))
+ }
@Test
- fun alwaysShowDataRatIcon_matchesParent() =
- testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.alwaysShowDataRatIcon.onEach { latest = it }.launchIn(this)
+ fun overrideIcon_usesCarrierIdOverride() = runTest {
+ overrides =
+ mock<MobileIconCarrierIdOverrides> {
+ on { carrierIdEntryExists(anyInt()) } doReturn true
+ on { getOverrideFor(anyInt(), anyString(), any()) } doReturn 1234
+ }
- mobileIconsInteractor.alwaysShowDataRatIcon.value = true
- assertThat(latest).isTrue()
+ connectionRepo.resolvedNetworkType.setValue(
+ DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G))
+ )
- mobileIconsInteractor.alwaysShowDataRatIcon.value = false
- assertThat(latest).isFalse()
+ val latest by underTest.networkTypeIconGroup.collectLastValue()
- job.cancel()
- }
+ assertThat(latest)
+ .isEqualTo(NetworkTypeIconModel.OverriddenIcon(TelephonyIcons.THREE_G, 1234))
+ }
@Test
- fun dataState_connected() =
- testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isDataConnected.onEach { latest = it }.launchIn(this)
+ fun alwaysShowDataRatIcon_matchesParent() = runTest {
+ val latest by underTest.alwaysShowDataRatIcon.collectLastValue()
- connectionRepository.dataConnectionState.value = DataConnectionState.Connected
+ // mobileIconsInteractor.alwaysShowDataRatIcon.setValue(true)
+ alwaysShowDataRatIcon.setValue(true)
- assertThat(latest).isTrue()
+ assertThat(latest).isTrue()
- job.cancel()
- }
+ // mobileIconsInteractor.alwaysShowDataRatIcon.setValue(false)
+ alwaysShowDataRatIcon.setValue(false)
- @Test
- fun dataState_notConnected() =
- testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isDataConnected.onEach { latest = it }.launchIn(this)
+ assertThat(latest).isFalse()
+ }
- connectionRepository.dataConnectionState.value = DataConnectionState.Disconnected
+ @Test
+ fun dataState_connected() = runTest {
+ val latest by underTest.isDataConnected.collectLastValue()
- assertThat(latest).isFalse()
+ connectionRepo.dataConnectionState.setValue(DataConnectionState.Connected)
- job.cancel()
- }
+ assertThat(latest).isTrue()
+ }
@Test
- fun isInService_usesRepositoryValue() =
- testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isInService.onEach { latest = it }.launchIn(this)
+ fun dataState_notConnected() = runTest {
+ val latest by underTest.isDataConnected.collectLastValue()
- connectionRepository.isInService.value = true
+ connectionRepo.dataConnectionState.setValue(DataConnectionState.Disconnected)
- assertThat(latest).isTrue()
+ assertThat(latest).isFalse()
+ }
- connectionRepository.isInService.value = false
+ @Test
+ fun isInService_usesRepositoryValue() = runTest {
+ val latest by underTest.isInService.collectLastValue()
- assertThat(latest).isFalse()
+ connectionRepo.isInService.setValue(true)
- job.cancel()
- }
+ assertThat(latest).isTrue()
- @Test
- fun roaming_isGsm_usesConnectionModel() =
- testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isRoaming.onEach { latest = it }.launchIn(this)
+ connectionRepo.isInService.setValue(false)
- connectionRepository.cdmaRoaming.value = true
- connectionRepository.isGsm.value = true
- connectionRepository.isRoaming.value = false
+ assertThat(latest).isFalse()
+ }
- assertThat(latest).isFalse()
+ @Test
+ fun roaming_isGsm_usesConnectionModel() = runTest {
+ val latest by underTest.isRoaming.collectLastValue()
- connectionRepository.isRoaming.value = true
+ connectionRepo.cdmaRoaming.setValue(true)
+ connectionRepo.isGsm.setValue(true)
+ connectionRepo.isRoaming.setValue(false)
- assertThat(latest).isTrue()
+ assertThat(latest).isFalse()
- job.cancel()
- }
+ connectionRepo.isRoaming.setValue(true)
- @Test
- fun roaming_isCdma_usesCdmaRoamingBit() =
- testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isRoaming.onEach { latest = it }.launchIn(this)
+ assertThat(latest).isTrue()
+ }
- connectionRepository.cdmaRoaming.value = false
- connectionRepository.isGsm.value = false
- connectionRepository.isRoaming.value = true
+ @Test
+ fun roaming_isCdma_usesCdmaRoamingBit() = runTest {
+ val latest by underTest.isRoaming.collectLastValue()
- assertThat(latest).isFalse()
+ connectionRepo.cdmaRoaming.setValue(false)
+ connectionRepo.isGsm.setValue(false)
+ connectionRepo.isRoaming.setValue(true)
- connectionRepository.cdmaRoaming.value = true
- connectionRepository.isGsm.value = false
- connectionRepository.isRoaming.value = false
+ assertThat(latest).isFalse()
- assertThat(latest).isTrue()
+ connectionRepo.cdmaRoaming.setValue(true)
+ connectionRepo.isGsm.setValue(false)
+ connectionRepo.isRoaming.setValue(false)
- job.cancel()
- }
+ assertThat(latest).isTrue()
+ }
@Test
- fun roaming_falseWhileCarrierNetworkChangeActive() =
- testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isRoaming.onEach { latest = it }.launchIn(this)
+ fun roaming_falseWhileCarrierNetworkChangeActive() = runTest {
+ val latest by underTest.isRoaming.collectLastValue()
- connectionRepository.cdmaRoaming.value = true
- connectionRepository.isGsm.value = false
- connectionRepository.isRoaming.value = true
- connectionRepository.carrierNetworkChangeActive.value = true
+ connectionRepo.cdmaRoaming.setValue(true)
+ connectionRepo.isGsm.setValue(false)
+ connectionRepo.isRoaming.setValue(true)
+ connectionRepo.carrierNetworkChangeActive.setValue(true)
- assertThat(latest).isFalse()
+ assertThat(latest).isFalse()
- connectionRepository.cdmaRoaming.value = true
- connectionRepository.isGsm.value = true
+ connectionRepo.cdmaRoaming.setValue(true)
+ connectionRepo.isGsm.setValue(true)
- assertThat(latest).isFalse()
-
- job.cancel()
- }
+ assertThat(latest).isFalse()
+ }
@Test
- fun networkName_usesOperatorAlphaShortWhenNonNullAndRepoIsDefault() =
- testScope.runTest {
- var latest: NetworkNameModel? = null
- val job = underTest.networkName.onEach { latest = it }.launchIn(this)
-
- val testOperatorName = "operatorAlphaShort"
+ fun networkName_usesOperatorAlphaShortWhenNonNullAndRepoIsDefault() = runTest {
+ val latest by underTest.networkName.collectLastValue()
- // Default network name, operator name is non-null, uses the operator name
- connectionRepository.networkName.value = DEFAULT_NAME_MODEL
- connectionRepository.operatorAlphaShort.value = testOperatorName
+ val testOperatorName = "operatorAlphaShort"
- assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived(testOperatorName))
+ // Default network name, operator name is non-null, uses the operator name
+ connectionRepo.networkName.setValue(DEFAULT_NAME_MODEL)
+ connectionRepo.operatorAlphaShort.setValue(testOperatorName)
- // Default network name, operator name is null, uses the default
- connectionRepository.operatorAlphaShort.value = null
+ assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived(testOperatorName))
- assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL)
+ // Default network name, operator name is null, uses the default
+ connectionRepo.operatorAlphaShort.setValue(null)
- // Derived network name, operator name non-null, uses the derived name
- connectionRepository.networkName.value = DERIVED_NAME_MODEL
- connectionRepository.operatorAlphaShort.value = testOperatorName
+ assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL)
- assertThat(latest).isEqualTo(DERIVED_NAME_MODEL)
+ // Derived network name, operator name non-null, uses the derived name
+ connectionRepo.networkName.setValue(DERIVED_NAME_MODEL)
+ connectionRepo.operatorAlphaShort.setValue(testOperatorName)
- job.cancel()
- }
+ assertThat(latest).isEqualTo(DERIVED_NAME_MODEL)
+ }
@Test
- fun networkNameForSubId_usesOperatorAlphaShortWhenNonNullAndRepoIsDefault() =
- testScope.runTest {
- var latest: String? = null
- val job = underTest.carrierName.onEach { latest = it }.launchIn(this)
-
- val testOperatorName = "operatorAlphaShort"
+ fun networkNameForSubId_usesOperatorAlphaShortWhenNonNullAndRepoIsDefault() = runTest {
+ val latest by underTest.carrierName.collectLastValue()
- // Default network name, operator name is non-null, uses the operator name
- connectionRepository.carrierName.value = DEFAULT_NAME_MODEL
- connectionRepository.operatorAlphaShort.value = testOperatorName
+ val testOperatorName = "operatorAlphaShort"
- assertThat(latest).isEqualTo(testOperatorName)
+ // Default network name, operator name is non-null, uses the operator name
+ connectionRepo.carrierName.setValue(DEFAULT_NAME_MODEL)
+ connectionRepo.operatorAlphaShort.setValue(testOperatorName)
- // Default network name, operator name is null, uses the default
- connectionRepository.operatorAlphaShort.value = null
+ assertThat(latest).isEqualTo(testOperatorName)
- assertThat(latest).isEqualTo(DEFAULT_NAME)
+ // Default network name, operator name is null, uses the default
+ connectionRepo.operatorAlphaShort.setValue(null)
- // Derived network name, operator name non-null, uses the derived name
- connectionRepository.carrierName.value =
- NetworkNameModel.SubscriptionDerived(DERIVED_NAME)
- connectionRepository.operatorAlphaShort.value = testOperatorName
+ assertThat(latest).isEqualTo(DEFAULT_NAME)
- assertThat(latest).isEqualTo(DERIVED_NAME)
+ // Derived network name, operator name non-null, uses the derived name
+ connectionRepo.carrierName.setValue(NetworkNameModel.SubscriptionDerived(DERIVED_NAME))
+ connectionRepo.operatorAlphaShort.setValue(testOperatorName)
- job.cancel()
- }
+ assertThat(latest).isEqualTo(DERIVED_NAME)
+ }
@Test
- fun isSingleCarrier_matchesParent() =
- testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isSingleCarrier.onEach { latest = it }.launchIn(this)
+ fun isSingleCarrier_matchesParent() = runTest {
+ val latest by underTest.isSingleCarrier.collectLastValue()
- mobileIconsInteractor.isSingleCarrier.value = true
- assertThat(latest).isTrue()
+ // mobileIconsInteractor.isSingleCarrier.setValue(true)
+ isSingleCarrier.setValue(true)
+ assertThat(latest).isTrue()
- mobileIconsInteractor.isSingleCarrier.value = false
- assertThat(latest).isFalse()
-
- job.cancel()
- }
+ // mobileIconsInteractor.isSingleCarrier.setValue(false)
+ isSingleCarrier.setValue(false)
+ assertThat(latest).isFalse()
+ }
@Test
- fun isForceHidden_matchesParent() =
- testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isForceHidden.onEach { latest = it }.launchIn(this)
-
- mobileIconsInteractor.isForceHidden.value = true
- assertThat(latest).isTrue()
+ fun isForceHidden_matchesParent() = runTest {
+ val latest by underTest.isForceHidden.collectLastValue()
- mobileIconsInteractor.isForceHidden.value = false
- assertThat(latest).isFalse()
+ // mobileIconsInteractor.isForceHidden.setValue(true)
+ isForceHidden.setValue(true)
+ assertThat(latest).isTrue()
- job.cancel()
- }
+ // mobileIconsInteractor.isForceHidden.setValue(false)
+ isForceHidden.setValue(false)
+ assertThat(latest).isFalse()
+ }
@Test
- fun isAllowedDuringAirplaneMode_matchesRepo() =
- testScope.runTest {
- val latest by collectLastValue(underTest.isAllowedDuringAirplaneMode)
+ fun isAllowedDuringAirplaneMode_matchesRepo() = runTest {
+ val latest by underTest.isAllowedDuringAirplaneMode.collectLastValue()
- connectionRepository.isAllowedDuringAirplaneMode.value = true
- assertThat(latest).isTrue()
+ connectionRepo.isAllowedDuringAirplaneMode.setValue(true)
+ assertThat(latest).isTrue()
- connectionRepository.isAllowedDuringAirplaneMode.value = false
- assertThat(latest).isFalse()
- }
+ connectionRepo.isAllowedDuringAirplaneMode.setValue(false)
+ assertThat(latest).isFalse()
+ }
@Test
- fun cellBasedIconId_correctLevel_notCutout() =
- testScope.runTest {
- connectionRepository.isNonTerrestrial.value = false
- connectionRepository.isInService.value = true
- connectionRepository.primaryLevel.value = 1
- connectionRepository.setDataEnabled(false)
- connectionRepository.isNonTerrestrial.value = false
+ fun cellBasedIconId_correctLevel_notCutout() = runTest {
+ connectionRepo.isNonTerrestrial.setValue(false)
+ connectionRepo.isInService.setValue(true)
+ connectionRepo.primaryLevel.setValue(1)
+ connectionRepo.dataEnabled.setValue(true)
+ connectionRepo.isNonTerrestrial.setValue(false)
- var latest: SignalIconModel.Cellular? = null
- val job =
- underTest.signalLevelIcon
- .onEach { latest = it as? SignalIconModel.Cellular }
- .launchIn(this)
+ val latest by
+ underTest.signalLevelIcon.map { it as? SignalIconModel.Cellular }.collectLastValue()
- assertThat(latest?.level).isEqualTo(1)
- assertThat(latest?.showExclamationMark).isFalse()
+ assertThat(latest?.level).isEqualTo(1)
- job.cancel()
- }
+ // TODO: need to provision MobileIconsInteractorKairos#isDefaultConnectionFailed +
+ // defaultSubscriptionHasDataEnabled?
+ assertThat(latest?.showExclamationMark).isEqualTo(false)
+ }
@Test
- fun icon_usesLevelFromInteractor() =
- testScope.runTest {
- connectionRepository.isNonTerrestrial.value = false
- connectionRepository.isInService.value = true
+ fun icon_usesLevelFromInteractor() = runTest {
+ connectionRepo.isNonTerrestrial.setValue(false)
+ connectionRepo.isInService.setValue(true)
- var latest: SignalIconModel? = null
- val job = underTest.signalLevelIcon.onEach { latest = it }.launchIn(this)
+ val latest by underTest.signalLevelIcon.collectLastValue()
- connectionRepository.primaryLevel.value = 3
- assertThat(latest!!.level).isEqualTo(3)
+ connectionRepo.primaryLevel.setValue(3)
+ assertThat(latest!!.level).isEqualTo(3)
- connectionRepository.primaryLevel.value = 1
- assertThat(latest!!.level).isEqualTo(1)
-
- job.cancel()
- }
+ connectionRepo.primaryLevel.setValue(1)
+ assertThat(latest!!.level).isEqualTo(1)
+ }
@Test
- fun cellBasedIcon_usesNumberOfLevelsFromInteractor() =
- testScope.runTest {
- connectionRepository.isNonTerrestrial.value = false
-
- var latest: SignalIconModel.Cellular? = null
- val job =
- underTest.signalLevelIcon
- .onEach { latest = it as? SignalIconModel.Cellular }
- .launchIn(this)
+ fun cellBasedIcon_usesNumberOfLevelsFromInteractor() = runTest {
+ connectionRepo.isNonTerrestrial.setValue(false)
- connectionRepository.numberOfLevels.value = 5
- assertThat(latest!!.numberOfLevels).isEqualTo(5)
+ val latest by
+ underTest.signalLevelIcon.map { it as? SignalIconModel.Cellular }.collectLastValue()
- connectionRepository.numberOfLevels.value = 2
- assertThat(latest!!.numberOfLevels).isEqualTo(2)
+ connectionRepo.numberOfLevels.setValue(5)
+ assertThat(latest!!.numberOfLevels).isEqualTo(5)
- job.cancel()
- }
+ connectionRepo.numberOfLevels.setValue(2)
+ assertThat(latest!!.numberOfLevels).isEqualTo(2)
+ }
@Test
- fun cellBasedIcon_defaultDataDisabled_showExclamationTrue() =
- testScope.runTest {
- connectionRepository.isNonTerrestrial.value = false
- mobileIconsInteractor.activeDataConnectionHasDataEnabled.value = false
-
- var latest: SignalIconModel.Cellular? = null
- val job =
- underTest.signalLevelIcon
- .onEach { latest = it as? SignalIconModel.Cellular }
- .launchIn(this)
+ fun cellBasedIcon_defaultDataDisabled_showExclamationTrue() = runTest {
+ connectionRepo.isNonTerrestrial.setValue(false)
+ connectionRepo.dataEnabled.setValue(false)
+ defaultSubscriptionHasDataEnabled.setValue(false)
- assertThat(latest!!.showExclamationMark).isTrue()
+ val latest by underTest.signalLevelIcon.collectLastValue()
- job.cancel()
- }
+ assertThat((latest!! as SignalIconModel.Cellular).showExclamationMark).isTrue()
+ }
@Test
- fun cellBasedIcon_defaultConnectionFailed_showExclamationTrue() =
- testScope.runTest {
- connectionRepository.isNonTerrestrial.value = false
- mobileIconsInteractor.isDefaultConnectionFailed.value = true
+ fun cellBasedIcon_defaultConnectionFailed_showExclamationTrue() = runTest {
+ connectionRepo.isNonTerrestrial.setValue(false)
+ // mobileIconsInteractor.isDefaultConnectionFailed.setValue(true)
+ isDefaultConnectionFailed.setValue(true)
- var latest: SignalIconModel.Cellular? = null
- val job =
- underTest.signalLevelIcon
- .onEach { latest = it as? SignalIconModel.Cellular }
- .launchIn(this)
+ val latest by underTest.signalLevelIcon.collectLastValue()
- assertThat(latest!!.showExclamationMark).isTrue()
-
- job.cancel()
- }
+ assertThat((latest!! as SignalIconModel.Cellular).showExclamationMark).isTrue()
+ }
@Test
- fun cellBasedIcon_enabledAndNotFailed_showExclamationFalse() =
- testScope.runTest {
- connectionRepository.isNonTerrestrial.value = false
- connectionRepository.isInService.value = true
- mobileIconsInteractor.activeDataConnectionHasDataEnabled.value = true
- mobileIconsInteractor.isDefaultConnectionFailed.value = false
-
- var latest: SignalIconModel.Cellular? = null
- val job =
- underTest.signalLevelIcon
- .onEach { latest = it as? SignalIconModel.Cellular }
- .launchIn(this)
+ fun cellBasedIcon_enabledAndNotFailed_showExclamationFalse() = runTest {
+ connectionRepo.isNonTerrestrial.setValue(false)
+ connectionRepo.isInService.setValue(true)
+ connectionRepo.dataEnabled.setValue(true)
+ // mobileIconsInteractor.isDefaultConnectionFailed.setValue(false)
+ isDefaultConnectionFailed.setValue(false)
- assertThat(latest!!.showExclamationMark).isFalse()
+ val latest by underTest.signalLevelIcon.collectLastValue()
- job.cancel()
- }
+ assertThat((latest!! as SignalIconModel.Cellular).showExclamationMark).isFalse()
+ }
@Test
- fun cellBasedIcon_usesEmptyState_whenNotInService() =
- testScope.runTest {
- var latest: SignalIconModel.Cellular? = null
- val job =
- underTest.signalLevelIcon
- .onEach { latest = it as? SignalIconModel.Cellular }
- .launchIn(this)
+ fun cellBasedIcon_usesEmptyState_whenNotInService() = runTest {
+ val latest by
+ underTest.signalLevelIcon.map { it as SignalIconModel.Cellular }.collectLastValue()
- connectionRepository.isNonTerrestrial.value = false
- connectionRepository.isInService.value = false
+ connectionRepo.isNonTerrestrial.setValue(false)
+ connectionRepo.isInService.setValue(false)
- assertThat(latest?.level).isEqualTo(0)
- assertThat(latest?.showExclamationMark).isTrue()
+ assertThat(latest?.level).isEqualTo(0)
+ assertThat(latest?.showExclamationMark).isTrue()
- // Changing the level doesn't overwrite the disabled state
- connectionRepository.primaryLevel.value = 2
- assertThat(latest?.level).isEqualTo(0)
- assertThat(latest?.showExclamationMark).isTrue()
+ // Changing the level doesn't overwrite the disabled state
+ connectionRepo.primaryLevel.setValue(2)
+ assertThat(latest?.level).isEqualTo(0)
+ assertThat(latest?.showExclamationMark).isTrue()
- // Once back in service, the regular icon appears
- connectionRepository.isInService.value = true
- assertThat(latest?.level).isEqualTo(2)
- assertThat(latest?.showExclamationMark).isFalse()
-
- job.cancel()
- }
+ // Once back in service, the regular icon appears
+ connectionRepo.isInService.setValue(true)
+ assertThat(latest?.level).isEqualTo(2)
+ assertThat(latest?.showExclamationMark).isFalse()
+ }
@Test
- fun cellBasedIcon_usesCarrierNetworkState_whenInCarrierNetworkChangeMode() =
- testScope.runTest {
- var latest: SignalIconModel.Cellular? = null
- val job =
- underTest.signalLevelIcon
- .onEach { latest = it as? SignalIconModel.Cellular? }
- .launchIn(this)
-
- connectionRepository.isNonTerrestrial.value = false
- connectionRepository.isInService.value = true
- connectionRepository.carrierNetworkChangeActive.value = true
- connectionRepository.primaryLevel.value = 1
- connectionRepository.cdmaLevel.value = 1
+ fun cellBasedIcon_usesCarrierNetworkState_whenInCarrierNetworkChangeMode() = runTest {
+ val latest by
+ underTest.signalLevelIcon.map { it as SignalIconModel.Cellular }.collectLastValue()
- assertThat(latest!!.level).isEqualTo(1)
- assertThat(latest!!.carrierNetworkChange).isTrue()
+ connectionRepo.isNonTerrestrial.setValue(false)
+ connectionRepo.isInService.setValue(true)
+ connectionRepo.carrierNetworkChangeActive.setValue(true)
+ connectionRepo.primaryLevel.setValue(1)
+ connectionRepo.cdmaLevel.setValue(1)
- // SignalIconModel respects the current level
- connectionRepository.primaryLevel.value = 2
+ assertThat(latest!!.level).isEqualTo(1)
+ assertThat(latest!!.carrierNetworkChange).isTrue()
- assertThat(latest!!.level).isEqualTo(2)
- assertThat(latest!!.carrierNetworkChange).isTrue()
+ // SignalIconModel respects the current level
+ connectionRepo.primaryLevel.setValue(2)
- job.cancel()
- }
+ assertThat(latest!!.level).isEqualTo(2)
+ assertThat(latest!!.carrierNetworkChange).isTrue()
+ }
@Test
- fun satBasedIcon_isUsedWhenNonTerrestrial() =
- testScope.runTest {
- val latest by collectLastValue(underTest.signalLevelIcon)
+ fun satBasedIcon_isUsedWhenNonTerrestrial() = runTest {
+ val latest by underTest.signalLevelIcon.collectLastValue()
- // Start off using cellular
- assertThat(latest).isInstanceOf(SignalIconModel.Cellular::class.java)
+ // Start off using cellular
+ assertThat(latest).isInstanceOf(SignalIconModel.Cellular::class.java)
- connectionRepository.isNonTerrestrial.value = true
+ connectionRepo.isNonTerrestrial.setValue(true)
- assertThat(latest).isInstanceOf(SignalIconModel.Satellite::class.java)
- }
+ assertThat(latest).isInstanceOf(SignalIconModel.Satellite::class.java)
+ }
@DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
@Test
// See b/346904529 for more context
- fun satBasedIcon_doesNotInflateSignalStrength_flagOff() =
- testScope.runTest {
- val latest by collectLastValue(underTest.signalLevelIcon)
+ fun satBasedIcon_doesNotInflateSignalStrength_flagOff() = runTest {
+ val latest by underTest.signalLevelIcon.collectLastValue()
- // GIVEN a satellite connection
- connectionRepository.isNonTerrestrial.value = true
- // GIVEN this carrier has set INFLATE_SIGNAL_STRENGTH
- connectionRepository.inflateSignalStrength.value = true
+ // GIVEN a satellite connection
+ connectionRepo.isNonTerrestrial.setValue(true)
+ // GIVEN this carrier has set INFLATE_SIGNAL_STRENGTH
+ connectionRepo.inflateSignalStrength.setValue(true)
- connectionRepository.primaryLevel.value = 4
- assertThat(latest!!.level).isEqualTo(4)
+ connectionRepo.primaryLevel.setValue(4)
+ assertThat(latest!!.level).isEqualTo(4)
- connectionRepository.inflateSignalStrength.value = true
- connectionRepository.primaryLevel.value = 4
+ connectionRepo.inflateSignalStrength.setValue(true)
+ connectionRepo.primaryLevel.setValue(4)
- // Icon level is unaffected
- assertThat(latest!!.level).isEqualTo(4)
- }
+ // Icon level is unaffected
+ assertThat(latest!!.level).isEqualTo(4)
+ }
@EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
@Test
// See b/346904529 for more context
- fun satBasedIcon_doesNotInflateSignalStrength_flagOn() =
- testScope.runTest {
- val latest by collectLastValue(underTest.signalLevelIcon)
+ fun satBasedIcon_doesNotInflateSignalStrength_flagOn() = runTest {
+ val latest by underTest.signalLevelIcon.collectLastValue()
- // GIVEN a satellite connection
- connectionRepository.isNonTerrestrial.value = true
- // GIVEN this carrier has set INFLATE_SIGNAL_STRENGTH
- connectionRepository.inflateSignalStrength.value = true
+ // GIVEN a satellite connection
+ connectionRepo.isNonTerrestrial.setValue(true)
+ // GIVEN this carrier has set INFLATE_SIGNAL_STRENGTH
+ connectionRepo.inflateSignalStrength.setValue(true)
- connectionRepository.satelliteLevel.value = 4
- assertThat(latest!!.level).isEqualTo(4)
+ connectionRepo.satelliteLevel.setValue(4)
+ assertThat(latest!!.level).isEqualTo(4)
- connectionRepository.inflateSignalStrength.value = true
- connectionRepository.primaryLevel.value = 4
+ connectionRepo.inflateSignalStrength.setValue(true)
+ connectionRepo.primaryLevel.setValue(4)
- // Icon level is unaffected
- assertThat(latest!!.level).isEqualTo(4)
- }
+ // Icon level is unaffected
+ assertThat(latest!!.level).isEqualTo(4)
+ }
@DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
@Test
- fun satBasedIcon_usesPrimaryLevel_flagOff() =
- testScope.runTest {
- val latest by collectLastValue(underTest.signalLevelIcon)
+ fun satBasedIcon_usesPrimaryLevel_flagOff() = runTest {
+ val latest by underTest.signalLevelIcon.collectLastValue()
- // GIVEN a satellite connection
- connectionRepository.isNonTerrestrial.value = true
+ // GIVEN a satellite connection
+ connectionRepo.isNonTerrestrial.setValue(true)
- // GIVEN primary level is set
- connectionRepository.primaryLevel.value = 4
- connectionRepository.satelliteLevel.value = 0
+ // GIVEN primary level is set
+ connectionRepo.primaryLevel.setValue(4)
+ connectionRepo.satelliteLevel.setValue(0)
- // THEN icon uses the primary level because the flag is off
- assertThat(latest!!.level).isEqualTo(4)
- }
+ // THEN icon uses the primary level because the flag is off
+ assertThat(latest!!.level).isEqualTo(4)
+ }
@EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
@Test
- fun satBasedIcon_usesSatelliteLevel_flagOn() =
- testScope.runTest {
- val latest by collectLastValue(underTest.signalLevelIcon)
+ fun satBasedIcon_usesSatelliteLevel_flagOn() = runTest {
+ val latest by underTest.signalLevelIcon.collectLastValue()
- // GIVEN a satellite connection
- connectionRepository.isNonTerrestrial.value = true
+ // GIVEN a satellite connection
+ connectionRepo.isNonTerrestrial.setValue(true)
- // GIVEN satellite level is set
- connectionRepository.satelliteLevel.value = 4
- connectionRepository.primaryLevel.value = 0
+ // GIVEN satellite level is set
+ connectionRepo.satelliteLevel.setValue(4)
+ connectionRepo.primaryLevel.setValue(0)
- // THEN icon uses the satellite level because the flag is on
- assertThat(latest!!.level).isEqualTo(4)
- }
+ // THEN icon uses the satellite level because the flag is on
+ assertThat(latest!!.level).isEqualTo(4)
+ }
/**
* Context (b/377518113), this test will not be needed after FLAG_CARRIER_ROAMING_NB_IOT_NTN is
@@ -816,43 +728,23 @@ class MobileIconInteractorKairosTest : SysuiTestCase() {
*/
@DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
@Test
- fun satBasedIcon_reportsLevelZeroWhenOutOfService() =
- testScope.runTest {
- val latest by collectLastValue(underTest.signalLevelIcon)
-
- // GIVEN a satellite connection
- connectionRepository.isNonTerrestrial.value = true
- // GIVEN this carrier has set INFLATE_SIGNAL_STRENGTH
- connectionRepository.inflateSignalStrength.value = true
+ fun satBasedIcon_reportsLevelZeroWhenOutOfService() = runTest {
+ val latest by underTest.signalLevelIcon.collectLastValue()
- connectionRepository.primaryLevel.value = 4
- assertThat(latest!!.level).isEqualTo(4)
+ // GIVEN a satellite connection
+ connectionRepo.isNonTerrestrial.setValue(true)
+ // GIVEN this carrier has set INFLATE_SIGNAL_STRENGTH
+ connectionRepo.inflateSignalStrength.setValue(true)
- connectionRepository.isInService.value = false
- connectionRepository.primaryLevel.value = 4
+ connectionRepo.primaryLevel.setValue(4)
+ assertThat(latest!!.level).isEqualTo(4)
- // THEN level reports 0, by policy
- assertThat(latest!!.level).isEqualTo(0)
- }
+ connectionRepo.isInService.setValue(false)
+ connectionRepo.primaryLevel.setValue(4)
- private fun createInteractor(
- overrides: MobileIconCarrierIdOverrides = MobileIconCarrierIdOverridesImpl()
- ) =
- MobileIconInteractorKairosImpl(
- testScope.backgroundScope,
- mobileIconsInteractor.activeDataConnectionHasDataEnabled,
- mobileIconsInteractor.alwaysShowDataRatIcon,
- mobileIconsInteractor.alwaysUseCdmaLevel,
- mobileIconsInteractor.isSingleCarrier,
- mobileIconsInteractor.mobileIsDefault,
- mobileIconsInteractor.defaultMobileIconMapping,
- mobileIconsInteractor.defaultMobileIconGroup,
- mobileIconsInteractor.isDefaultConnectionFailed,
- mobileIconsInteractor.isForceHidden,
- connectionRepository,
- context,
- overrides,
- )
+ // THEN level reports 0, by policy
+ assertThat(latest!!.level).isEqualTo(0)
+ }
companion object {
private const val GSM_LEVEL = 1
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosTest.kt
index a9360d139a3d..4f6439d2d0b2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 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.
@@ -28,186 +28,164 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.Flags
import com.android.systemui.flags.fake
import com.android.systemui.flags.featureFlagsClassic
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosTestScope
+import com.android.systemui.kairos.runKairosTest
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runCurrent
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.statusbar.core.NewStatusBarIcons
import com.android.systemui.statusbar.core.StatusBarRootModernization
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.fake
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepository
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepositoryLogbufferName
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepositoryKairos
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepositoryKairos
import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
import com.android.systemui.statusbar.pipeline.shared.data.repository.connectivityRepository
import com.android.systemui.statusbar.pipeline.shared.data.repository.fake
-import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository
import com.android.systemui.testKosmos
-import com.android.systemui.util.CarrierConfigTracker
+import com.android.systemui.util.carrierConfigTracker
import com.google.common.truth.Truth.assertThat
import java.util.UUID
import kotlinx.coroutines.test.advanceTimeBy
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
+@OptIn(ExperimentalKairosApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class MobileIconsInteractorKairosTest : SysuiTestCase() {
- private val kosmos by lazy {
+
+ private val kosmos =
testKosmos().apply {
- mobileConnectionsRepositoryLogbufferName = "MobileIconsInteractorTest"
- mobileConnectionsRepository.fake.run {
- setMobileConnectionRepositoryMap(
- mapOf(
- SUB_1_ID to FakeMobileConnectionRepository(SUB_1_ID, mock()),
- SUB_2_ID to FakeMobileConnectionRepository(SUB_2_ID, mock()),
- SUB_3_ID to FakeMobileConnectionRepository(SUB_3_ID, mock()),
- SUB_4_ID to FakeMobileConnectionRepository(SUB_4_ID, mock()),
- )
- )
- setActiveMobileDataSubscriptionId(SUB_1_ID)
- }
+ useUnconfinedTestDispatcher()
+ mobileConnectionsRepositoryKairos =
+ fakeMobileConnectionsRepositoryKairos.apply {
+ setActiveMobileDataSubscriptionId(SUB_1_ID)
+ subscriptions.setValue(listOf(SUB_1, SUB_2, SUB_3_OPP, SUB_4_OPP))
+ }
featureFlagsClassic.fake.set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, true)
}
- }
- // shortcut rename
- private val Kosmos.connectionsRepository by Fixture { mobileConnectionsRepository.fake }
-
- private val Kosmos.carrierConfigTracker by Fixture { mock<CarrierConfigTracker>() }
-
- private val Kosmos.underTest by Fixture {
- MobileIconsInteractorKairosImpl(
- mobileConnectionsRepository,
- carrierConfigTracker,
- tableLogger = mock(),
- connectivityRepository,
- FakeUserSetupRepository(),
- testScope.backgroundScope,
- context,
- featureFlagsClassic,
- )
- }
+ private val Kosmos.underTest
+ get() = mobileIconsInteractorKairos
+
+ private fun runTest(block: suspend KairosTestScope.() -> Unit) =
+ kosmos.run { runKairosTest { block() } }
@Test
- fun filteredSubscriptions_default() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ fun filteredSubscriptions_default() = runTest {
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(emptyList())
+ val latest by underTest.filteredSubscriptions.collectLastValue()
- assertThat(latest).isEqualTo(listOf<SubscriptionModel>())
- }
+ assertThat(latest).isEqualTo(emptyList<SubscriptionModel>())
+ }
// Based on the logic from the old pipeline, we'll never filter subs when there are more than 2
@Test
- fun filteredSubscriptions_moreThanTwo_doesNotFilter() =
- kosmos.runTest {
- connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP, SUB_4_OPP))
- connectionsRepository.setActiveMobileDataSubscriptionId(SUB_4_ID)
+ fun filteredSubscriptions_moreThanTwo_doesNotFilter() = runTest {
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(
+ listOf(SUB_1, SUB_3_OPP, SUB_4_OPP)
+ )
+ mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(SUB_4_ID)
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ val latest by underTest.filteredSubscriptions.collectLastValue()
- assertThat(latest).isEqualTo(listOf(SUB_1, SUB_3_OPP, SUB_4_OPP))
- }
+ assertThat(latest).isEqualTo(listOf(SUB_1, SUB_3_OPP, SUB_4_OPP))
+ }
@Test
- fun filteredSubscriptions_nonOpportunistic_updatesWithMultipleSubs() =
- kosmos.runTest {
- connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
+ fun filteredSubscriptions_nonOpportunistic_updatesWithMultipleSubs() = runTest {
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1, SUB_2))
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ val latest by underTest.filteredSubscriptions.collectLastValue()
- assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2))
- }
+ assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2))
+ }
@Test
- fun filteredSubscriptions_opportunistic_differentGroups_doesNotFilter() =
- kosmos.runTest {
- connectionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP))
- connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
+ fun filteredSubscriptions_opportunistic_differentGroups_doesNotFilter() = runTest {
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_3_OPP, SUB_4_OPP))
+ mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(SUB_3_ID)
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ val latest by underTest.filteredSubscriptions.collectLastValue()
- assertThat(latest).isEqualTo(listOf(SUB_3_OPP, SUB_4_OPP))
- }
+ assertThat(latest).isEqualTo(listOf(SUB_3_OPP, SUB_4_OPP))
+ }
@Test
- fun filteredSubscriptions_opportunistic_nonGrouped_doesNotFilter() =
- kosmos.runTest {
- val (sub1, sub2) =
- createSubscriptionPair(
- subscriptionIds = Pair(SUB_1_ID, SUB_2_ID),
- opportunistic = Pair(true, true),
- grouped = false,
- )
- connectionsRepository.setSubscriptions(listOf(sub1, sub2))
- connectionsRepository.setActiveMobileDataSubscriptionId(SUB_1_ID)
+ fun filteredSubscriptions_opportunistic_nonGrouped_doesNotFilter() = runTest {
+ val (sub1, sub2) =
+ createSubscriptionPair(
+ subscriptionIds = Pair(SUB_1_ID, SUB_2_ID),
+ opportunistic = Pair(true, true),
+ grouped = false,
+ )
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub1, sub2))
+ mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(SUB_1_ID)
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ val latest by underTest.filteredSubscriptions.collectLastValue()
- assertThat(latest).isEqualTo(listOf(sub1, sub2))
- }
+ assertThat(latest).isEqualTo(listOf(sub1, sub2))
+ }
@Test
- fun filteredSubscriptions_opportunistic_grouped_configFalse_showsActive_3() =
- kosmos.runTest {
- val (sub3, sub4) =
- createSubscriptionPair(
- subscriptionIds = Pair(SUB_3_ID, SUB_4_ID),
- opportunistic = Pair(true, true),
- grouped = true,
- )
- connectionsRepository.setSubscriptions(listOf(sub3, sub4))
- connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
- whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
- .thenReturn(false)
+ fun filteredSubscriptions_opportunistic_grouped_configFalse_showsActive_3() = runTest {
+ val (sub3, sub4) =
+ createSubscriptionPair(
+ subscriptionIds = Pair(SUB_3_ID, SUB_4_ID),
+ opportunistic = Pair(true, true),
+ grouped = true,
+ )
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub3, sub4))
+ mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(SUB_3_ID)
+ whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
+ .thenReturn(false)
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ val latest by underTest.filteredSubscriptions.collectLastValue()
- // Filtered subscriptions should show the active one when the config is false
- assertThat(latest).isEqualTo(listOf(sub3))
- }
+ // Filtered subscriptions should show the active one when the config is false
+ assertThat(latest).isEqualTo(listOf(sub3))
+ }
@Test
- fun filteredSubscriptions_opportunistic_grouped_configFalse_showsActive_4() =
- kosmos.runTest {
- val (sub3, sub4) =
- createSubscriptionPair(
- subscriptionIds = Pair(SUB_3_ID, SUB_4_ID),
- opportunistic = Pair(true, true),
- grouped = true,
- )
- connectionsRepository.setSubscriptions(listOf(sub3, sub4))
- connectionsRepository.setActiveMobileDataSubscriptionId(SUB_4_ID)
- whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
- .thenReturn(false)
+ fun filteredSubscriptions_opportunistic_grouped_configFalse_showsActive_4() = runTest {
+ val (sub3, sub4) =
+ createSubscriptionPair(
+ subscriptionIds = Pair(SUB_3_ID, SUB_4_ID),
+ opportunistic = Pair(true, true),
+ grouped = true,
+ )
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub3, sub4))
+ mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(SUB_4_ID)
+ whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
+ .thenReturn(false)
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ val latest by underTest.filteredSubscriptions.collectLastValue()
- // Filtered subscriptions should show the active one when the config is false
- assertThat(latest).isEqualTo(listOf(sub4))
- }
+ // Filtered subscriptions should show the active one when the config is false
+ assertThat(latest).isEqualTo(listOf(sub4))
+ }
@Test
fun filteredSubscriptions_oneOpportunistic_grouped_configTrue_showsPrimary_active_1() =
- kosmos.runTest {
+ runTest {
val (sub1, sub3) =
createSubscriptionPair(
subscriptionIds = Pair(SUB_1_ID, SUB_3_ID),
opportunistic = Pair(false, true),
grouped = true,
)
- connectionsRepository.setSubscriptions(listOf(sub1, sub3))
- connectionsRepository.setActiveMobileDataSubscriptionId(SUB_1_ID)
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub1, sub3))
+ mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(SUB_1_ID)
whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
.thenReturn(true)
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ val latest by underTest.filteredSubscriptions.collectLastValue()
// Filtered subscriptions should show the primary (non-opportunistic) if the config is
// true
@@ -216,19 +194,19 @@ class MobileIconsInteractorKairosTest : SysuiTestCase() {
@Test
fun filteredSubscriptions_oneOpportunistic_grouped_configTrue_showsPrimary_nonActive_1() =
- kosmos.runTest {
+ runTest {
val (sub1, sub3) =
createSubscriptionPair(
subscriptionIds = Pair(SUB_1_ID, SUB_3_ID),
opportunistic = Pair(false, true),
grouped = true,
)
- connectionsRepository.setSubscriptions(listOf(sub1, sub3))
- connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub1, sub3))
+ mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(SUB_3_ID)
whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
.thenReturn(true)
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ val latest by underTest.filteredSubscriptions.collectLastValue()
// Filtered subscriptions should show the primary (non-opportunistic) if the config is
// true
@@ -236,135 +214,130 @@ class MobileIconsInteractorKairosTest : SysuiTestCase() {
}
@Test
- fun filteredSubscriptions_vcnSubId_agreesWithActiveSubId_usesActiveAkaVcnSub() =
- kosmos.runTest {
- val (sub1, sub3) =
- createSubscriptionPair(
- subscriptionIds = Pair(SUB_1_ID, SUB_3_ID),
- opportunistic = Pair(true, true),
- grouped = true,
- )
- connectionsRepository.setSubscriptions(listOf(sub1, sub3))
- connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
- kosmos.connectivityRepository.fake.vcnSubId.value = SUB_3_ID
- whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
- .thenReturn(false)
+ fun filteredSubscriptions_vcnSubId_agreesWithActiveSubId_usesActiveAkaVcnSub() = runTest {
+ val (sub1, sub3) =
+ createSubscriptionPair(
+ subscriptionIds = Pair(SUB_1_ID, SUB_3_ID),
+ opportunistic = Pair(true, true),
+ grouped = true,
+ )
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub1, sub3))
+ mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(SUB_3_ID)
+ kosmos.connectivityRepository.fake.vcnSubId.value = SUB_3_ID
+ whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
+ .thenReturn(false)
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ val latest by underTest.filteredSubscriptions.collectLastValue()
- assertThat(latest).isEqualTo(listOf(sub3))
- }
+ assertThat(latest).isEqualTo(listOf(sub3))
+ }
@Test
- fun filteredSubscriptions_vcnSubId_disagreesWithActiveSubId_usesVcnSub() =
- kosmos.runTest {
- val (sub1, sub3) =
- createSubscriptionPair(
- subscriptionIds = Pair(SUB_1_ID, SUB_3_ID),
- opportunistic = Pair(true, true),
- grouped = true,
- )
- connectionsRepository.setSubscriptions(listOf(sub1, sub3))
- connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
- kosmos.connectivityRepository.fake.vcnSubId.value = SUB_1_ID
- whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
- .thenReturn(false)
+ fun filteredSubscriptions_vcnSubId_disagreesWithActiveSubId_usesVcnSub() = runTest {
+ val (sub1, sub3) =
+ createSubscriptionPair(
+ subscriptionIds = Pair(SUB_1_ID, SUB_3_ID),
+ opportunistic = Pair(true, true),
+ grouped = true,
+ )
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub1, sub3))
+ mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(SUB_3_ID)
+ kosmos.connectivityRepository.fake.vcnSubId.value = SUB_1_ID
+ whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
+ .thenReturn(false)
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ val latest by underTest.filteredSubscriptions.collectLastValue()
- assertThat(latest).isEqualTo(listOf(sub1))
- }
+ assertThat(latest).isEqualTo(listOf(sub1))
+ }
@Test
- fun filteredSubscriptions_doesNotFilterProvisioningWhenFlagIsFalse() =
- kosmos.runTest {
- // GIVEN the flag is false
- featureFlagsClassic.fake.set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, false)
+ fun filteredSubscriptions_doesNotFilterProvisioningWhenFlagIsFalse() = runTest {
+ // GIVEN the flag is false
+ featureFlagsClassic.fake.set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, false)
- // GIVEN 1 sub that is in PROFILE_CLASS_PROVISIONING
- val sub1 =
- SubscriptionModel(
- subscriptionId = SUB_1_ID,
- isOpportunistic = false,
- carrierName = "Carrier 1",
- profileClass = PROFILE_CLASS_PROVISIONING,
- )
+ // GIVEN 1 sub that is in PROFILE_CLASS_PROVISIONING
+ val sub1 =
+ SubscriptionModel(
+ subscriptionId = SUB_1_ID,
+ isOpportunistic = false,
+ carrierName = "Carrier 1",
+ profileClass = PROFILE_CLASS_PROVISIONING,
+ )
- connectionsRepository.setSubscriptions(listOf(sub1))
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub1))
- // WHEN filtering is applied
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ // WHEN filtering is applied
+ val latest by underTest.filteredSubscriptions.collectLastValue()
- // THEN the provisioning sub is still present (unfiltered)
- assertThat(latest).isEqualTo(listOf(sub1))
- }
+ // THEN the provisioning sub is still present (unfiltered)
+ assertThat(latest).isEqualTo(listOf(sub1))
+ }
@Test
- fun filteredSubscriptions_filtersOutProvisioningSubs() =
- kosmos.runTest {
- val sub1 =
- SubscriptionModel(
- subscriptionId = SUB_1_ID,
- isOpportunistic = false,
- carrierName = "Carrier 1",
- profileClass = PROFILE_CLASS_UNSET,
- )
- val sub2 =
- SubscriptionModel(
- subscriptionId = SUB_2_ID,
- isOpportunistic = false,
- carrierName = "Carrier 2",
- profileClass = PROFILE_CLASS_PROVISIONING,
- )
+ fun filteredSubscriptions_filtersOutProvisioningSubs() = runTest {
+ val sub1 =
+ SubscriptionModel(
+ subscriptionId = SUB_1_ID,
+ isOpportunistic = false,
+ carrierName = "Carrier 1",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+ val sub2 =
+ SubscriptionModel(
+ subscriptionId = SUB_2_ID,
+ isOpportunistic = false,
+ carrierName = "Carrier 2",
+ profileClass = PROFILE_CLASS_PROVISIONING,
+ )
- connectionsRepository.setSubscriptions(listOf(sub1, sub2))
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub1, sub2))
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ val latest by underTest.filteredSubscriptions.collectLastValue()
- assertThat(latest).isEqualTo(listOf(sub1))
- }
+ assertThat(latest).isEqualTo(listOf(sub1))
+ }
/** Note: I'm not sure if this will ever be the case, but we can test it at least */
@Test
- fun filteredSubscriptions_filtersOutProvisioningSubsBeforeOpportunistic() =
- kosmos.runTest {
- // This is a contrived test case, where the active subId is the one that would
- // also be filtered by opportunistic filtering.
-
- // GIVEN grouped, opportunistic subscriptions
- val groupUuid = ParcelUuid(UUID.randomUUID())
- val sub1 =
- SubscriptionModel(
- subscriptionId = 1,
- isOpportunistic = true,
- groupUuid = groupUuid,
- carrierName = "Carrier 1",
- profileClass = PROFILE_CLASS_PROVISIONING,
- )
+ fun filteredSubscriptions_filtersOutProvisioningSubsBeforeOpportunistic() = runTest {
+ // This is a contrived test case, where the active subId is the one that would
+ // also be filtered by opportunistic filtering.
- val sub2 =
- SubscriptionModel(
- subscriptionId = 2,
- isOpportunistic = true,
- groupUuid = groupUuid,
- carrierName = "Carrier 2",
- profileClass = PROFILE_CLASS_UNSET,
- )
+ // GIVEN grouped, opportunistic subscriptions
+ val groupUuid = ParcelUuid(UUID.randomUUID())
+ val sub1 =
+ SubscriptionModel(
+ subscriptionId = 1,
+ isOpportunistic = true,
+ groupUuid = groupUuid,
+ carrierName = "Carrier 1",
+ profileClass = PROFILE_CLASS_PROVISIONING,
+ )
- // GIVEN active subId is 1
- connectionsRepository.setSubscriptions(listOf(sub1, sub2))
- connectionsRepository.setActiveMobileDataSubscriptionId(1)
+ val sub2 =
+ SubscriptionModel(
+ subscriptionId = 2,
+ isOpportunistic = true,
+ groupUuid = groupUuid,
+ carrierName = "Carrier 2",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
- // THEN filtering of provisioning subs takes place first, and we result in sub2
+ // GIVEN active subId is 1
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub1, sub2))
+ mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(1)
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ // THEN filtering of provisioning subs takes place first, and we result in sub2
- assertThat(latest).isEqualTo(listOf(sub2))
- }
+ val latest by underTest.filteredSubscriptions.collectLastValue()
+
+ assertThat(latest).isEqualTo(listOf(sub2))
+ }
@Test
fun filteredSubscriptions_groupedPairAndNonProvisioned_groupedFilteringStillHappens() =
- kosmos.runTest {
+ runTest {
// Grouped filtering only happens when the list of subs is length 2. In this case
// we'll show that filtering of provisioning subs happens before, and thus grouped
// filtering happens even though the unfiltered list is length 3
@@ -384,87 +357,88 @@ class MobileIconsInteractorKairosTest : SysuiTestCase() {
profileClass = PROFILE_CLASS_PROVISIONING,
)
- connectionsRepository.setSubscriptions(listOf(sub1, sub2, sub3))
- connectionsRepository.setActiveMobileDataSubscriptionId(1)
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub1, sub2, sub3))
+ mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(1)
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ val latest by underTest.filteredSubscriptions.collectLastValue()
assertThat(latest).isEqualTo(listOf(sub1))
}
@Test
- fun filteredSubscriptions_subNotExclusivelyNonTerrestrial_hasSub() =
- kosmos.runTest {
- val notExclusivelyNonTerrestrialSub =
- SubscriptionModel(
- isExclusivelyNonTerrestrial = false,
- subscriptionId = 5,
- carrierName = "Carrier 5",
- profileClass = PROFILE_CLASS_UNSET,
- )
+ fun filteredSubscriptions_subNotExclusivelyNonTerrestrial_hasSub() = runTest {
+ val notExclusivelyNonTerrestrialSub =
+ SubscriptionModel(
+ isExclusivelyNonTerrestrial = false,
+ subscriptionId = 5,
+ carrierName = "Carrier 5",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
- connectionsRepository.setSubscriptions(listOf(notExclusivelyNonTerrestrialSub))
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(
+ listOf(notExclusivelyNonTerrestrialSub)
+ )
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ val latest by underTest.filteredSubscriptions.collectLastValue()
- assertThat(latest).isEqualTo(listOf(notExclusivelyNonTerrestrialSub))
- }
+ assertThat(latest).isEqualTo(listOf(notExclusivelyNonTerrestrialSub))
+ }
@Test
- fun filteredSubscriptions_subExclusivelyNonTerrestrial_doesNotHaveSub() =
- kosmos.runTest {
- val exclusivelyNonTerrestrialSub =
- SubscriptionModel(
- isExclusivelyNonTerrestrial = true,
- subscriptionId = 5,
- carrierName = "Carrier 5",
- profileClass = PROFILE_CLASS_UNSET,
- )
+ fun filteredSubscriptions_subExclusivelyNonTerrestrial_doesNotHaveSub() = runTest {
+ val exclusivelyNonTerrestrialSub =
+ SubscriptionModel(
+ isExclusivelyNonTerrestrial = true,
+ subscriptionId = 5,
+ carrierName = "Carrier 5",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
- connectionsRepository.setSubscriptions(listOf(exclusivelyNonTerrestrialSub))
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(
+ listOf(exclusivelyNonTerrestrialSub)
+ )
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ val latest by underTest.filteredSubscriptions.collectLastValue()
- assertThat(latest).isEmpty()
- }
+ assertThat(latest).isEmpty()
+ }
@Test
- fun filteredSubscription_mixOfExclusivelyNonTerrestrialAndOther_hasOtherSubsOnly() =
- kosmos.runTest {
- val exclusivelyNonTerrestrialSub =
- SubscriptionModel(
- isExclusivelyNonTerrestrial = true,
- subscriptionId = 5,
- carrierName = "Carrier 5",
- profileClass = PROFILE_CLASS_UNSET,
- )
- val otherSub1 =
- SubscriptionModel(
- isExclusivelyNonTerrestrial = false,
- subscriptionId = 1,
- carrierName = "Carrier 1",
- profileClass = PROFILE_CLASS_UNSET,
- )
- val otherSub2 =
- SubscriptionModel(
- isExclusivelyNonTerrestrial = false,
- subscriptionId = 2,
- carrierName = "Carrier 2",
- profileClass = PROFILE_CLASS_UNSET,
- )
-
- connectionsRepository.setSubscriptions(
- listOf(otherSub1, exclusivelyNonTerrestrialSub, otherSub2)
+ fun filteredSubscription_mixOfExclusivelyNonTerrestrialAndOther_hasOtherSubsOnly() = runTest {
+ val exclusivelyNonTerrestrialSub =
+ SubscriptionModel(
+ isExclusivelyNonTerrestrial = true,
+ subscriptionId = 5,
+ carrierName = "Carrier 5",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+ val otherSub1 =
+ SubscriptionModel(
+ isExclusivelyNonTerrestrial = false,
+ subscriptionId = 1,
+ carrierName = "Carrier 1",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+ val otherSub2 =
+ SubscriptionModel(
+ isExclusivelyNonTerrestrial = false,
+ subscriptionId = 2,
+ carrierName = "Carrier 2",
+ profileClass = PROFILE_CLASS_UNSET,
)
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(
+ listOf(otherSub1, exclusivelyNonTerrestrialSub, otherSub2)
+ )
+
+ val latest by underTest.filteredSubscriptions.collectLastValue()
- assertThat(latest).isEqualTo(listOf(otherSub1, otherSub2))
- }
+ assertThat(latest).isEqualTo(listOf(otherSub1, otherSub2))
+ }
@Test
fun filteredSubscriptions_exclusivelyNonTerrestrialSub_andOpportunistic_bothFiltersHappen() =
- kosmos.runTest {
+ runTest {
// Exclusively non-terrestrial sub
val exclusivelyNonTerrestrialSub =
SubscriptionModel(
@@ -483,10 +457,12 @@ class MobileIconsInteractorKairosTest : SysuiTestCase() {
)
// WHEN both an exclusively non-terrestrial sub and opportunistic sub pair is included
- connectionsRepository.setSubscriptions(listOf(sub3, sub4, exclusivelyNonTerrestrialSub))
- connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(
+ listOf(sub3, sub4, exclusivelyNonTerrestrialSub)
+ )
+ mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(SUB_3_ID)
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ val latest by underTest.filteredSubscriptions.collectLastValue()
// THEN both the only-non-terrestrial sub and the non-active sub are filtered out,
// leaving only sub3.
@@ -494,484 +470,427 @@ class MobileIconsInteractorKairosTest : SysuiTestCase() {
}
@Test
- fun activeDataConnection_turnedOn() =
- kosmos.runTest {
- (fakeMobileConnectionsRepository.getRepoForSubId(SUB_1_ID)
- as FakeMobileConnectionRepository)
- .dataEnabled
- .value = true
+ fun activeDataConnection_turnedOn() = runTest {
+ val connection1 =
+ mobileConnectionsRepositoryKairos.fake.mobileConnectionsBySubId.sample()[SUB_1_ID]!!
- val latest by collectLastValue(underTest.activeDataConnectionHasDataEnabled)
+ connection1.dataEnabled.setValue(true)
- assertThat(latest).isTrue()
- }
+ val latest by underTest.activeDataConnectionHasDataEnabled.collectLastValue()
+
+ assertThat(latest).isTrue()
+ }
@Test
- fun activeDataConnection_turnedOff() =
- kosmos.runTest {
- (fakeMobileConnectionsRepository.getRepoForSubId(SUB_1_ID)
- as FakeMobileConnectionRepository)
- .dataEnabled
- .value = true
+ fun activeDataConnection_turnedOff() = runTest {
+ val connection1 =
+ mobileConnectionsRepositoryKairos.fake.mobileConnectionsBySubId.sample()[SUB_1_ID]!!
- val latest by collectLastValue(underTest.activeDataConnectionHasDataEnabled)
+ connection1.dataEnabled.setValue(true)
+ val latest by underTest.activeDataConnectionHasDataEnabled.collectLastValue()
- (fakeMobileConnectionsRepository.getRepoForSubId(SUB_1_ID)
- as FakeMobileConnectionRepository)
- .dataEnabled
- .value = false
+ connection1.dataEnabled.setValue(false)
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isFalse()
+ }
@Test
- fun activeDataConnection_invalidSubId() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.activeDataConnectionHasDataEnabled)
+ fun activeDataConnection_invalidSubId() = runTest {
+ val latest by underTest.activeDataConnectionHasDataEnabled.collectLastValue()
- connectionsRepository.setActiveMobileDataSubscriptionId(INVALID_SUBSCRIPTION_ID)
+ mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(
+ INVALID_SUBSCRIPTION_ID
+ )
- // An invalid active subId should tell us that data is off
- assertThat(latest).isFalse()
- }
+ // An invalid active subId should tell us that data is off
+ assertThat(latest).isFalse()
+ }
@Test
- fun failedConnection_default_validated_notFailed() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isDefaultConnectionFailed)
+ fun failedConnection_default_validated_notFailed() = runTest {
+ val latest by underTest.isDefaultConnectionFailed.collectLastValue()
- connectionsRepository.mobileIsDefault.value = true
- connectionsRepository.defaultConnectionIsValidated.value = true
+ mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true)
+ mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(true)
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isFalse()
+ }
@Test
- fun failedConnection_notDefault_notValidated_notFailed() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isDefaultConnectionFailed)
+ fun failedConnection_notDefault_notValidated_notFailed() = runTest {
+ val latest by underTest.isDefaultConnectionFailed.collectLastValue()
- connectionsRepository.mobileIsDefault.value = false
- connectionsRepository.defaultConnectionIsValidated.value = false
+ mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(false)
+ mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(false)
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isFalse()
+ }
@Test
- fun failedConnection_default_notValidated_failed() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isDefaultConnectionFailed)
+ fun failedConnection_default_notValidated_failed() = runTest {
+ val latest by underTest.isDefaultConnectionFailed.collectLastValue()
- connectionsRepository.mobileIsDefault.value = true
- connectionsRepository.defaultConnectionIsValidated.value = false
+ mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true)
+ mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(false)
- assertThat(latest).isTrue()
- }
+ assertThat(latest).isTrue()
+ }
@Test
- fun failedConnection_carrierMergedDefault_notValidated_failed() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isDefaultConnectionFailed)
+ fun failedConnection_carrierMergedDefault_notValidated_failed() = runTest {
+ val latest by underTest.isDefaultConnectionFailed.collectLastValue()
- connectionsRepository.hasCarrierMergedConnection.value = true
- connectionsRepository.defaultConnectionIsValidated.value = false
+ mobileConnectionsRepositoryKairos.fake.hasCarrierMergedConnection.setValue(true)
+ mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(false)
- assertThat(latest).isTrue()
- }
+ assertThat(latest).isTrue()
+ }
/** Regression test for b/275076959. */
@Test
- fun failedConnection_dataSwitchInSameGroup_notFailed() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isDefaultConnectionFailed)
+ fun failedConnection_dataSwitchInSameGroup_notFailed() = runTest {
+ val latest by underTest.isDefaultConnectionFailed.collectLastValue()
- connectionsRepository.mobileIsDefault.value = true
- connectionsRepository.defaultConnectionIsValidated.value = true
- runCurrent()
+ mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true)
+ mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(true)
+ runCurrent()
- // WHEN there's a data change in the same subscription group
- connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
- connectionsRepository.defaultConnectionIsValidated.value = false
- runCurrent()
+ // WHEN there's a data change in the same subscription group
+ mobileConnectionsRepositoryKairos.fake.activeSubChangedInGroupEvent.emit(Unit)
+ mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(false)
+ runCurrent()
- // THEN the default connection is *not* marked as failed because of forced validation
- assertThat(latest).isFalse()
- }
+ // THEN the default connection is *not* marked as failed because of forced validation
+ assertThat(latest).isFalse()
+ }
@Test
- fun failedConnection_dataSwitchNotInSameGroup_isFailed() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isDefaultConnectionFailed)
+ fun failedConnection_dataSwitchNotInSameGroup_isFailed() = runTest {
+ val latest by underTest.isDefaultConnectionFailed.collectLastValue()
- connectionsRepository.mobileIsDefault.value = true
- connectionsRepository.defaultConnectionIsValidated.value = true
- runCurrent()
+ mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true)
+ mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(true)
+ runCurrent()
- // WHEN the connection is invalidated without a activeSubChangedInGroupEvent
- connectionsRepository.defaultConnectionIsValidated.value = false
+ // WHEN the connection is invalidated without a activeSubChangedInGroupEvent
+ mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(false)
- // THEN the connection is immediately marked as failed
- assertThat(latest).isTrue()
- }
+ // THEN the connection is immediately marked as failed
+ assertThat(latest).isTrue()
+ }
@Test
- fun alwaysShowDataRatIcon_configHasTrue() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.alwaysShowDataRatIcon)
+ fun alwaysShowDataRatIcon_configHasTrue() = runTest {
+ val latest by underTest.alwaysShowDataRatIcon.collectLastValue()
- val config = MobileMappings.Config()
- config.alwaysShowDataRatIcon = true
- connectionsRepository.defaultDataSubRatConfig.value = config
+ val config = MobileMappings.Config()
+ config.alwaysShowDataRatIcon = true
+ mobileConnectionsRepositoryKairos.fake.defaultDataSubRatConfig.setValue(config)
- assertThat(latest).isTrue()
- }
+ assertThat(latest).isTrue()
+ }
@Test
- fun alwaysShowDataRatIcon_configHasFalse() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.alwaysShowDataRatIcon)
+ fun alwaysShowDataRatIcon_configHasFalse() = runTest {
+ val latest by underTest.alwaysShowDataRatIcon.collectLastValue()
- val config = MobileMappings.Config()
- config.alwaysShowDataRatIcon = false
- connectionsRepository.defaultDataSubRatConfig.value = config
+ val config = MobileMappings.Config()
+ config.alwaysShowDataRatIcon = false
+ mobileConnectionsRepositoryKairos.fake.defaultDataSubRatConfig.setValue(config)
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isFalse()
+ }
@Test
- fun alwaysUseCdmaLevel_configHasTrue() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.alwaysUseCdmaLevel)
+ fun alwaysUseCdmaLevel_configHasTrue() = runTest {
+ val latest by underTest.alwaysUseCdmaLevel.collectLastValue()
- val config = MobileMappings.Config()
- config.alwaysShowCdmaRssi = true
- connectionsRepository.defaultDataSubRatConfig.value = config
+ val config = MobileMappings.Config()
+ config.alwaysShowCdmaRssi = true
+ mobileConnectionsRepositoryKairos.fake.defaultDataSubRatConfig.setValue(config)
- assertThat(latest).isTrue()
- }
+ assertThat(latest).isTrue()
+ }
@Test
- fun alwaysUseCdmaLevel_configHasFalse() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.alwaysUseCdmaLevel)
+ fun alwaysUseCdmaLevel_configHasFalse() = runTest {
+ val latest by underTest.alwaysUseCdmaLevel.collectLastValue()
- val config = MobileMappings.Config()
- config.alwaysShowCdmaRssi = false
- connectionsRepository.defaultDataSubRatConfig.value = config
+ val config = MobileMappings.Config()
+ config.alwaysShowCdmaRssi = false
+ mobileConnectionsRepositoryKairos.fake.defaultDataSubRatConfig.setValue(config)
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isFalse()
+ }
@Test
- fun isSingleCarrier_zeroSubscriptions_false() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isSingleCarrier)
+ fun isSingleCarrier_zeroSubscriptions_false() = runTest {
+ val latest by underTest.isSingleCarrier.collectLastValue()
- connectionsRepository.setSubscriptions(emptyList())
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(emptyList())
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isFalse()
+ }
@Test
- fun isSingleCarrier_oneSubscription_true() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isSingleCarrier)
+ fun isSingleCarrier_oneSubscription_true() = runTest {
+ val latest by underTest.isSingleCarrier.collectLastValue()
- connectionsRepository.setSubscriptions(listOf(SUB_1))
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1))
- assertThat(latest).isTrue()
- }
+ assertThat(latest).isTrue()
+ }
@Test
- fun isSingleCarrier_twoSubscriptions_false() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isSingleCarrier)
+ fun isSingleCarrier_twoSubscriptions_false() = runTest {
+ val latest by underTest.isSingleCarrier.collectLastValue()
- connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1, SUB_2))
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isFalse()
+ }
@Test
- fun isSingleCarrier_updates() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isSingleCarrier)
+ fun isSingleCarrier_updates() = runTest {
+ val latest by underTest.isSingleCarrier.collectLastValue()
- connectionsRepository.setSubscriptions(listOf(SUB_1))
- assertThat(latest).isTrue()
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1))
+ assertThat(latest).isTrue()
- connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
- assertThat(latest).isFalse()
- }
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1, SUB_2))
+ assertThat(latest).isFalse()
+ }
@Test
- fun mobileIsDefault_mobileFalseAndCarrierMergedFalse_false() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.mobileIsDefault)
+ fun mobileIsDefault_mobileFalseAndCarrierMergedFalse_false() = runTest {
+ val latest by underTest.mobileIsDefault.collectLastValue()
- connectionsRepository.mobileIsDefault.value = false
- connectionsRepository.hasCarrierMergedConnection.value = false
+ mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(false)
+ mobileConnectionsRepositoryKairos.fake.hasCarrierMergedConnection.setValue(false)
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isFalse()
+ }
@Test
- fun mobileIsDefault_mobileTrueAndCarrierMergedFalse_true() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.mobileIsDefault)
+ fun mobileIsDefault_mobileTrueAndCarrierMergedFalse_true() = runTest {
+ val latest by underTest.mobileIsDefault.collectLastValue()
- connectionsRepository.mobileIsDefault.value = true
- connectionsRepository.hasCarrierMergedConnection.value = false
+ mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true)
+ mobileConnectionsRepositoryKairos.fake.hasCarrierMergedConnection.setValue(false)
- assertThat(latest).isTrue()
- }
+ assertThat(latest).isTrue()
+ }
/** Regression test for b/272586234. */
@Test
- fun mobileIsDefault_mobileFalseAndCarrierMergedTrue_true() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.mobileIsDefault)
+ fun mobileIsDefault_mobileFalseAndCarrierMergedTrue_true() = runTest {
+ val latest by underTest.mobileIsDefault.collectLastValue()
- connectionsRepository.mobileIsDefault.value = false
- connectionsRepository.hasCarrierMergedConnection.value = true
+ mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(false)
+ mobileConnectionsRepositoryKairos.fake.hasCarrierMergedConnection.setValue(true)
- assertThat(latest).isTrue()
- }
+ assertThat(latest).isTrue()
+ }
@Test
- fun mobileIsDefault_updatesWhenRepoUpdates() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.mobileIsDefault)
+ fun mobileIsDefault_updatesWhenRepoUpdates() = runTest {
+ val latest by underTest.mobileIsDefault.collectLastValue()
- connectionsRepository.mobileIsDefault.value = true
- assertThat(latest).isTrue()
+ mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true)
+ assertThat(latest).isTrue()
- connectionsRepository.mobileIsDefault.value = false
- assertThat(latest).isFalse()
+ mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(false)
+ assertThat(latest).isFalse()
- connectionsRepository.hasCarrierMergedConnection.value = true
- assertThat(latest).isTrue()
- }
+ mobileConnectionsRepositoryKairos.fake.hasCarrierMergedConnection.setValue(true)
+ assertThat(latest).isTrue()
+ }
// The data switch tests are mostly testing the [forcingCellularValidation] flow, but that flow
// is private and can only be tested by looking at [isDefaultConnectionFailed].
@Test
- fun dataSwitch_inSameGroup_validatedMatchesPreviousValue_expiresAfter2s() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isDefaultConnectionFailed)
-
- connectionsRepository.mobileIsDefault.value = true
- connectionsRepository.defaultConnectionIsValidated.value = true
- runCurrent()
-
- // Trigger a data change in the same subscription group that's not yet validated
- connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
- connectionsRepository.defaultConnectionIsValidated.value = false
- runCurrent()
-
- // After 1s, the force validation bit is still present, so the connection is not marked
- // as failed
- testScope.advanceTimeBy(1000)
- assertThat(latest).isFalse()
-
- // After 2s, the force validation expires so the connection updates to failed
- testScope.advanceTimeBy(1001)
- assertThat(latest).isTrue()
- }
+ fun dataSwitch_inSameGroup_validatedMatchesPreviousValue_expiresAfter2s() = runTest {
+ val latest by underTest.isDefaultConnectionFailed.collectLastValue()
- @Test
- fun dataSwitch_inSameGroup_notValidated_immediatelyMarkedAsFailed() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isDefaultConnectionFailed)
+ mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true)
+ mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(true)
+ runCurrent()
- connectionsRepository.mobileIsDefault.value = true
- connectionsRepository.defaultConnectionIsValidated.value = false
- runCurrent()
+ // Trigger a data change in the same subscription group that's not yet validated
+ mobileConnectionsRepositoryKairos.fake.activeSubChangedInGroupEvent.emit(Unit)
+ mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(false)
+ runCurrent()
- connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
+ // After 1s, the force validation bit is still present, so the connection is not marked
+ // as failed
+ testScope.advanceTimeBy(1000)
+ assertThat(latest).isFalse()
- assertThat(latest).isTrue()
- }
+ // After 2s, the force validation expires so the connection updates to failed
+ testScope.advanceTimeBy(1001)
+ assertThat(latest).isTrue()
+ }
@Test
- fun dataSwitch_loseValidation_thenSwitchHappens_clearsForcedBit() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isDefaultConnectionFailed)
+ fun dataSwitch_inSameGroup_notValidated_immediatelyMarkedAsFailed() = runTest {
+ val latest by underTest.isDefaultConnectionFailed.collectLastValue()
- // GIVEN the network starts validated
- connectionsRepository.mobileIsDefault.value = true
- connectionsRepository.defaultConnectionIsValidated.value = true
- runCurrent()
+ mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true)
+ mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(false)
+ runCurrent()
- // WHEN a data change happens in the same group
- connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
+ mobileConnectionsRepositoryKairos.fake.activeSubChangedInGroupEvent.emit(Unit)
- // WHEN the validation bit is lost
- connectionsRepository.defaultConnectionIsValidated.value = false
- runCurrent()
-
- // WHEN another data change happens in the same group
- connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
-
- // THEN the forced validation bit is still used...
- assertThat(latest).isFalse()
-
- testScope.advanceTimeBy(1000)
- assertThat(latest).isFalse()
-
- // ... but expires after 2s
- testScope.advanceTimeBy(1001)
- assertThat(latest).isTrue()
- }
+ assertThat(latest).isTrue()
+ }
@Test
- fun dataSwitch_whileAlreadyForcingValidation_resetsClock() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isDefaultConnectionFailed)
- connectionsRepository.mobileIsDefault.value = true
- connectionsRepository.defaultConnectionIsValidated.value = true
- runCurrent()
+ fun dataSwitch_loseValidation_thenSwitchHappens_clearsForcedBit() = runTest {
+ val latest by underTest.isDefaultConnectionFailed.collectLastValue()
- connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
+ // GIVEN the network starts validated
+ mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true)
+ mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(true)
+ runCurrent()
- testScope.advanceTimeBy(1000)
+ // WHEN a data change happens in the same group
+ mobileConnectionsRepositoryKairos.fake.activeSubChangedInGroupEvent.emit(Unit)
- // WHEN another change in same group event happens
- connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
- connectionsRepository.defaultConnectionIsValidated.value = false
- runCurrent()
+ // WHEN the validation bit is lost
+ mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(false)
+ runCurrent()
- // THEN the forced validation remains for exactly 2 more seconds from now
+ // WHEN another data change happens in the same group
+ mobileConnectionsRepositoryKairos.fake.activeSubChangedInGroupEvent.emit(Unit)
- // 1.500s from second event
- testScope.advanceTimeBy(1500)
- assertThat(latest).isFalse()
+ // THEN the forced validation bit is still used...
+ assertThat(latest).isFalse()
- // 2.001s from the second event
- testScope.advanceTimeBy(501)
- assertThat(latest).isTrue()
- }
+ testScope.advanceTimeBy(1000)
+ assertThat(latest).isFalse()
- @Test
- fun isForceHidden_repoHasMobileHidden_true() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isForceHidden)
+ // ... but expires after 2s
+ testScope.advanceTimeBy(1001)
+ assertThat(latest).isTrue()
+ }
- kosmos.connectivityRepository.fake.setForceHiddenIcons(setOf(ConnectivitySlot.MOBILE))
+ @Test
+ fun dataSwitch_whileAlreadyForcingValidation_resetsClock() = runTest {
+ val latest by underTest.isDefaultConnectionFailed.collectLastValue()
+ mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true)
+ mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(true)
+ runCurrent()
- assertThat(latest).isTrue()
- }
+ mobileConnectionsRepositoryKairos.fake.activeSubChangedInGroupEvent.emit(Unit)
- @Test
- fun isForceHidden_repoDoesNotHaveMobileHidden_false() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isForceHidden)
+ testScope.advanceTimeBy(1000)
- kosmos.connectivityRepository.fake.setForceHiddenIcons(setOf(ConnectivitySlot.WIFI))
+ // WHEN another change in same group event happens
+ mobileConnectionsRepositoryKairos.fake.activeSubChangedInGroupEvent.emit(Unit)
+ mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(false)
+ runCurrent()
- assertThat(latest).isFalse()
- }
+ // THEN the forced validation remains for exactly 2 more seconds from now
- @Test
- fun iconInteractor_cachedPerSubId() =
- kosmos.runTest {
- val interactor1 = underTest.getMobileConnectionInteractorForSubId(SUB_1_ID)
- val interactor2 = underTest.getMobileConnectionInteractorForSubId(SUB_1_ID)
+ // 1.500s from second event
+ testScope.advanceTimeBy(1500)
+ assertThat(latest).isFalse()
- assertThat(interactor1).isNotNull()
- assertThat(interactor1).isSameInstanceAs(interactor2)
- }
+ // 2.001s from the second event
+ testScope.advanceTimeBy(501)
+ assertThat(latest).isTrue()
+ }
@Test
- fun deviceBasedEmergencyMode_emergencyCallsOnly_followsDeviceServiceStateFromRepo() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isDeviceInEmergencyCallsOnlyMode)
+ fun isForceHidden_repoHasMobileHidden_true() = runTest {
+ val latest by underTest.isForceHidden.collectLastValue()
- connectionsRepository.isDeviceEmergencyCallCapable.value = true
+ kosmos.connectivityRepository.fake.setForceHiddenIcons(setOf(ConnectivitySlot.MOBILE))
- assertThat(latest).isTrue()
+ assertThat(latest).isTrue()
+ }
+
+ @Test
+ fun isForceHidden_repoDoesNotHaveMobileHidden_false() = runTest {
+ val latest by underTest.isForceHidden.collectLastValue()
- connectionsRepository.isDeviceEmergencyCallCapable.value = false
+ kosmos.connectivityRepository.fake.setForceHiddenIcons(setOf(ConnectivitySlot.WIFI))
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isFalse()
+ }
@Test
- fun defaultDataSubId_tracksRepo() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.defaultDataSubId)
+ fun deviceBasedEmergencyMode_emergencyCallsOnly_followsDeviceServiceStateFromRepo() = runTest {
+ val latest by underTest.isDeviceInEmergencyCallsOnlyMode.collectLastValue()
- connectionsRepository.defaultDataSubId.value = 1
+ mobileConnectionsRepositoryKairos.fake.isDeviceEmergencyCallCapable.setValue(true)
- assertThat(latest).isEqualTo(1)
+ assertThat(latest).isTrue()
- connectionsRepository.defaultDataSubId.value = 2
+ mobileConnectionsRepositoryKairos.fake.isDeviceEmergencyCallCapable.setValue(false)
- assertThat(latest).isEqualTo(2)
- }
+ assertThat(latest).isFalse()
+ }
@Test
@EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
- fun isStackable_tracksNumberOfSubscriptions() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isStackable)
+ fun isStackable_tracksNumberOfSubscriptions() = runTest {
+ val latest by underTest.isStackable.collectLastValue()
- connectionsRepository.setSubscriptions(listOf(SUB_1))
- assertThat(latest).isFalse()
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1))
+ assertThat(latest).isFalse()
- connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
- assertThat(latest).isTrue()
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1, SUB_2))
+ assertThat(latest).isTrue()
- connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2, SUB_3_OPP))
- assertThat(latest).isFalse()
- }
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(
+ listOf(SUB_1, SUB_2, SUB_3_OPP)
+ )
+ assertThat(latest).isFalse()
+ }
@Test
@EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
- fun isStackable_checksForTerrestrialConnections() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isStackable)
+ fun isStackable_checksForTerrestrialConnections() = runTest {
+ val latest by underTest.isStackable.collectLastValue()
- connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
- setNumberOfLevelsForSubId(SUB_1_ID, 5)
- setNumberOfLevelsForSubId(SUB_2_ID, 5)
- assertThat(latest).isTrue()
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1, SUB_2))
+ setNumberOfLevelsForSubId(SUB_1_ID, 5)
+ setNumberOfLevelsForSubId(SUB_2_ID, 5)
+ assertThat(latest).isTrue()
- (fakeMobileConnectionsRepository.getRepoForSubId(SUB_1_ID)
- as FakeMobileConnectionRepository)
- .isNonTerrestrial
- .value = true
+ mobileConnectionsRepositoryKairos.fake.mobileConnectionsBySubId
+ .sample()[SUB_1_ID]!!
+ .isNonTerrestrial
+ .setValue(true)
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isFalse()
+ }
@Test
@EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
- fun isStackable_checksForNumberOfBars() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isStackable)
+ fun isStackable_checksForNumberOfBars() = runTest {
+ val latest by underTest.isStackable.collectLastValue()
- // Number of levels is the same for both
- connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
- setNumberOfLevelsForSubId(SUB_1_ID, 5)
- setNumberOfLevelsForSubId(SUB_2_ID, 5)
+ // Number of levels is the same for both
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1, SUB_2))
+ setNumberOfLevelsForSubId(SUB_1_ID, 5)
+ setNumberOfLevelsForSubId(SUB_2_ID, 5)
- assertThat(latest).isTrue()
+ assertThat(latest).isTrue()
- // Change the number of levels to be different than SUB_2
- setNumberOfLevelsForSubId(SUB_1_ID, 6)
+ // Change the number of levels to be different than SUB_2
+ setNumberOfLevelsForSubId(SUB_1_ID, 6)
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isFalse()
+ }
- private fun setNumberOfLevelsForSubId(subId: Int, numberOfLevels: Int) {
- with(kosmos) {
- (fakeMobileConnectionsRepository.getRepoForSubId(subId)
- as FakeMobileConnectionRepository)
- .numberOfLevels
- .value = numberOfLevels
- }
+ private suspend fun KairosTestScope.setNumberOfLevelsForSubId(subId: Int, numberOfLevels: Int) {
+ mobileConnectionsRepositoryKairos.fake.mobileConnectionsBySubId
+ .sample()[subId]!!
+ .numberOfLevels
+ .setValue(numberOfLevels)
}
/**
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelKairosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelKairosTest.kt
new file mode 100644
index 000000000000..57e63a595b8f
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelKairosTest.kt
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2022 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.pipeline.mobile.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlagsClassic
+import com.android.systemui.flags.Flags
+import com.android.systemui.log.table.logcatTableLogBuffer
+import com.android.systemui.statusbar.connectivity.MobileIconCarrierIdOverridesFake
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
+import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractorImpl
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorImpl
+import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
+import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
+import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository
+import com.android.systemui.testKosmos
+import com.android.systemui.util.CarrierConfigTracker
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class LocationBasedMobileIconViewModelKairosTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+
+ private lateinit var commonImpl: MobileIconViewModelCommonKairos
+ private lateinit var homeIcon: HomeMobileIconViewModelKairos
+ private lateinit var qsIcon: QsMobileIconViewModelKairos
+ private lateinit var keyguardIcon: KeyguardMobileIconViewModelKairos
+ private lateinit var iconsInteractor: MobileIconsInteractor
+ private lateinit var interactor: MobileIconInteractor
+ private val connectionsRepository = kosmos.fakeMobileConnectionsRepository
+ private lateinit var repository: FakeMobileConnectionRepository
+ private lateinit var airplaneModeInteractor: AirplaneModeInteractor
+
+ private val connectivityRepository = FakeConnectivityRepository()
+ private val flags =
+ FakeFeatureFlagsClassic().also {
+ it.set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, true)
+ }
+
+ @Mock private lateinit var constants: ConnectivityConstants
+ private val tableLogBuffer =
+ logcatTableLogBuffer(kosmos, "LocationBasedMobileIconViewModelTest")
+ @Mock private lateinit var carrierConfigTracker: CarrierConfigTracker
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ airplaneModeInteractor =
+ AirplaneModeInteractor(
+ FakeAirplaneModeRepository(),
+ FakeConnectivityRepository(),
+ connectionsRepository,
+ )
+ repository =
+ FakeMobileConnectionRepository(SUB_1_ID, tableLogBuffer).apply {
+ isInService.value = true
+ cdmaLevel.value = 1
+ primaryLevel.value = 1
+ isEmergencyOnly.value = false
+ numberOfLevels.value = 4
+ resolvedNetworkType.value = ResolvedNetworkType.DefaultNetworkType(lookupKey = "3G")
+ dataConnectionState.value = DataConnectionState.Connected
+ }
+
+ connectionsRepository.activeMobileDataRepository.value = repository
+
+ connectivityRepository.apply { setMobileConnected() }
+
+ iconsInteractor =
+ MobileIconsInteractorImpl(
+ connectionsRepository,
+ carrierConfigTracker,
+ tableLogBuffer,
+ connectivityRepository,
+ FakeUserSetupRepository(),
+ testScope.backgroundScope,
+ context,
+ flags,
+ )
+
+ interactor =
+ MobileIconInteractorImpl(
+ testScope.backgroundScope,
+ iconsInteractor.activeDataConnectionHasDataEnabled,
+ iconsInteractor.alwaysShowDataRatIcon,
+ iconsInteractor.alwaysUseCdmaLevel,
+ iconsInteractor.isSingleCarrier,
+ iconsInteractor.mobileIsDefault,
+ iconsInteractor.defaultMobileIconMapping,
+ iconsInteractor.defaultMobileIconGroup,
+ iconsInteractor.isDefaultConnectionFailed,
+ iconsInteractor.isForceHidden,
+ repository,
+ context,
+ MobileIconCarrierIdOverridesFake(),
+ )
+
+ commonImpl =
+ MobileIconViewModelKairos(
+ SUB_1_ID,
+ interactor,
+ airplaneModeInteractor,
+ constants,
+ testScope.backgroundScope,
+ )
+
+ homeIcon = HomeMobileIconViewModelKairos(commonImpl, mock())
+ qsIcon = QsMobileIconViewModelKairos(commonImpl)
+ keyguardIcon = KeyguardMobileIconViewModelKairos(commonImpl)
+ }
+
+ @Test
+ fun locationBasedViewModelsReceiveSameIconIdWhenCommonImplUpdates() =
+ testScope.runTest {
+ var latestHome: SignalIconModel? = null
+ val homeJob = homeIcon.icon.onEach { latestHome = it }.launchIn(this)
+
+ var latestQs: SignalIconModel? = null
+ val qsJob = qsIcon.icon.onEach { latestQs = it }.launchIn(this)
+
+ var latestKeyguard: SignalIconModel? = null
+ val keyguardJob = keyguardIcon.icon.onEach { latestKeyguard = it }.launchIn(this)
+
+ var expected = defaultSignal(level = 1)
+
+ assertThat(latestHome).isEqualTo(expected)
+ assertThat(latestQs).isEqualTo(expected)
+ assertThat(latestKeyguard).isEqualTo(expected)
+
+ repository.setAllLevels(2)
+ expected = defaultSignal(level = 2)
+
+ assertThat(latestHome).isEqualTo(expected)
+ assertThat(latestQs).isEqualTo(expected)
+ assertThat(latestKeyguard).isEqualTo(expected)
+
+ homeJob.cancel()
+ qsJob.cancel()
+ keyguardJob.cancel()
+ }
+
+ companion object {
+ private const val SUB_1_ID = 1
+ private const val NUM_LEVELS = 4
+
+ /** Convenience constructor for these tests */
+ fun defaultSignal(level: Int = 1): SignalIconModel {
+ return SignalIconModel.Cellular(
+ level,
+ NUM_LEVELS,
+ showExclamationMark = false,
+ carrierNetworkChange = false,
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairosTest.kt
new file mode 100644
index 000000000000..6b114a8256f2
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairosTest.kt
@@ -0,0 +1,1077 @@
+/*
+ * Copyright (C) 2022 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.pipeline.mobile.ui.viewmodel
+
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.settingslib.mobile.MobileMappings
+import com.android.settingslib.mobile.TelephonyIcons.G
+import com.android.settingslib.mobile.TelephonyIcons.THREE_G
+import com.android.settingslib.mobile.TelephonyIcons.UNKNOWN
+import com.android.systemui.Flags.FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.FakeFeatureFlagsClassic
+import com.android.systemui.flags.Flags
+import com.android.systemui.log.table.logcatTableLogBuffer
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.connectivity.MobileIconCarrierIdOverridesFake
+import com.android.systemui.statusbar.core.NewStatusBarIcons
+import com.android.systemui.statusbar.core.StatusBarRootModernization
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository.Companion.DEFAULT_NETWORK_NAME
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractorImpl
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorImpl
+import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
+import com.android.systemui.statusbar.pipeline.mobile.ui.model.MobileContentDescription
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
+import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
+import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository
+import com.android.systemui.testKosmos
+import com.android.systemui.util.CarrierConfigTracker
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.yield
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class MobileIconViewModelKairosTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+
+ private var connectivityRepository = FakeConnectivityRepository()
+
+ private lateinit var underTest: MobileIconViewModelKairos
+ private lateinit var interactor: MobileIconInteractorImpl
+ private lateinit var iconsInteractor: MobileIconsInteractorImpl
+ private lateinit var repository: FakeMobileConnectionRepository
+ private lateinit var connectionsRepository: FakeMobileConnectionsRepository
+ private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
+ private lateinit var airplaneModeInteractor: AirplaneModeInteractor
+ @Mock private lateinit var constants: ConnectivityConstants
+ private val tableLogBuffer = logcatTableLogBuffer(kosmos, "MobileIconViewModelTest")
+ @Mock private lateinit var carrierConfigTracker: CarrierConfigTracker
+
+ private val flags =
+ FakeFeatureFlagsClassic().also {
+ it.set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, true)
+ }
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ whenever(constants.hasDataCapabilities).thenReturn(true)
+
+ connectionsRepository =
+ FakeMobileConnectionsRepository(FakeMobileMappingsProxy(), tableLogBuffer)
+
+ repository =
+ FakeMobileConnectionRepository(SUB_1_ID, tableLogBuffer).apply {
+ setNetworkTypeKey(connectionsRepository.GSM_KEY)
+ isInService.value = true
+ dataConnectionState.value = DataConnectionState.Connected
+ dataEnabled.value = true
+ }
+ connectionsRepository.activeMobileDataRepository.value = repository
+ connectionsRepository.mobileIsDefault.value = true
+
+ airplaneModeRepository = FakeAirplaneModeRepository()
+ airplaneModeInteractor =
+ AirplaneModeInteractor(
+ airplaneModeRepository,
+ connectivityRepository,
+ kosmos.fakeMobileConnectionsRepository,
+ )
+
+ iconsInteractor =
+ MobileIconsInteractorImpl(
+ connectionsRepository,
+ carrierConfigTracker,
+ tableLogBuffer,
+ connectivityRepository,
+ FakeUserSetupRepository(),
+ testScope.backgroundScope,
+ context,
+ flags,
+ )
+
+ interactor =
+ MobileIconInteractorImpl(
+ testScope.backgroundScope,
+ iconsInteractor.activeDataConnectionHasDataEnabled,
+ iconsInteractor.alwaysShowDataRatIcon,
+ iconsInteractor.alwaysUseCdmaLevel,
+ iconsInteractor.isSingleCarrier,
+ iconsInteractor.mobileIsDefault,
+ iconsInteractor.defaultMobileIconMapping,
+ iconsInteractor.defaultMobileIconGroup,
+ iconsInteractor.isDefaultConnectionFailed,
+ iconsInteractor.isForceHidden,
+ repository,
+ context,
+ MobileIconCarrierIdOverridesFake(),
+ )
+ createAndSetViewModel()
+ }
+
+ @Test
+ fun isVisible_notDataCapable_alwaysFalse() =
+ testScope.runTest {
+ // Create a new view model here so the constants are properly read
+ whenever(constants.hasDataCapabilities).thenReturn(false)
+ createAndSetViewModel()
+
+ var latest: Boolean? = null
+ val job = underTest.isVisible.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun isVisible_notAirplane_notForceHidden_true() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job = underTest.isVisible.onEach { latest = it }.launchIn(this)
+
+ airplaneModeRepository.setIsAirplaneMode(false)
+
+ assertThat(latest).isTrue()
+
+ job.cancel()
+ }
+
+ @Test
+ fun isVisible_airplaneAndNotAllowed_false() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job = underTest.isVisible.onEach { latest = it }.launchIn(this)
+
+ airplaneModeRepository.setIsAirplaneMode(true)
+ repository.isAllowedDuringAirplaneMode.value = false
+ connectivityRepository.setForceHiddenIcons(setOf())
+
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ /** Regression test for b/291993542. */
+ @Test
+ fun isVisible_airplaneButAllowed_true() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job = underTest.isVisible.onEach { latest = it }.launchIn(this)
+
+ airplaneModeRepository.setIsAirplaneMode(true)
+ repository.isAllowedDuringAirplaneMode.value = true
+ connectivityRepository.setForceHiddenIcons(setOf())
+
+ assertThat(latest).isTrue()
+
+ job.cancel()
+ }
+
+ @Test
+ fun isVisible_forceHidden_false() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job = underTest.isVisible.onEach { latest = it }.launchIn(this)
+
+ airplaneModeRepository.setIsAirplaneMode(false)
+ connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.MOBILE))
+
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun isVisible_respondsToUpdates() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job = underTest.isVisible.onEach { latest = it }.launchIn(this)
+
+ airplaneModeRepository.setIsAirplaneMode(false)
+ connectivityRepository.setForceHiddenIcons(setOf())
+
+ assertThat(latest).isTrue()
+
+ airplaneModeRepository.setIsAirplaneMode(true)
+ assertThat(latest).isFalse()
+
+ repository.isAllowedDuringAirplaneMode.value = true
+ assertThat(latest).isTrue()
+
+ connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.MOBILE))
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun isVisible_satellite_respectsAirplaneMode() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.isVisible)
+
+ repository.isNonTerrestrial.value = true
+ airplaneModeInteractor.setIsAirplaneMode(false)
+
+ assertThat(latest).isTrue()
+
+ airplaneModeInteractor.setIsAirplaneMode(true)
+
+ assertThat(latest).isFalse()
+ }
+
+ @Test
+ fun contentDescription_notInService_usesNoPhone() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.contentDescription)
+
+ repository.isInService.value = false
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL))
+ }
+
+ @Test
+ fun contentDescription_includesNetworkName() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.contentDescription)
+
+ repository.isInService.value = true
+ repository.networkName.value = NetworkNameModel.SubscriptionDerived("Test Network Name")
+ repository.numberOfLevels.value = 5
+ repository.setAllLevels(3)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular("Test Network Name", THREE_BARS))
+ }
+
+ @Test
+ fun contentDescription_inService_usesLevel() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.contentDescription)
+
+ repository.setAllLevels(2)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, TWO_BARS))
+
+ repository.setAllLevels(0)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL))
+ }
+
+ @Test
+ fun contentDescription_nonInflated_invalidLevelUsesNoSignalText() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.contentDescription)
+
+ repository.inflateSignalStrength.value = false
+ repository.setAllLevels(-1)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL))
+
+ repository.setAllLevels(100)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL))
+ }
+
+ @Test
+ fun contentDescription_nonInflated_levelStrings() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.contentDescription)
+
+ repository.inflateSignalStrength.value = false
+ repository.setAllLevels(0)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL))
+
+ repository.setAllLevels(1)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, ONE_BAR))
+
+ repository.setAllLevels(2)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, TWO_BARS))
+
+ repository.setAllLevels(3)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, THREE_BARS))
+
+ repository.setAllLevels(4)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, FULL_BARS))
+ }
+
+ @Test
+ fun contentDescription_inflated_invalidLevelUsesNoSignalText() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.contentDescription)
+
+ repository.inflateSignalStrength.value = true
+ repository.numberOfLevels.value = 6
+
+ repository.setAllLevels(-2)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL))
+
+ repository.setAllLevels(100)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL))
+ }
+
+ @Test
+ fun contentDescription_inflated_levelStrings() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.contentDescription)
+
+ repository.inflateSignalStrength.value = true
+ repository.numberOfLevels.value = 6
+
+ // Note that the _repo_ level is 1 lower than the reported level through the interactor
+
+ repository.setAllLevels(0)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, ONE_BAR))
+
+ repository.setAllLevels(1)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, TWO_BARS))
+
+ repository.setAllLevels(2)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, THREE_BARS))
+
+ repository.setAllLevels(3)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, FOUR_BARS))
+
+ repository.setAllLevels(4)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, FULL_BARS))
+ }
+
+ @Test
+ fun contentDescription_nonInflated_testABunchOfLevelsForNull() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.contentDescription)
+
+ repository.inflateSignalStrength.value = false
+ repository.numberOfLevels.value = 5
+
+ // -1 and 5 are out of the bounds for non-inflated content descriptions
+ for (i in -1..5) {
+ repository.setAllLevels(i)
+ when (i) {
+ -1,
+ 5 ->
+ assertWithMessage("Level $i is expected to be 'no signal'")
+ .that((latest as MobileContentDescription.Cellular).levelDescriptionRes)
+ .isEqualTo(NO_SIGNAL)
+ else ->
+ assertWithMessage("Level $i is expected not to be null")
+ .that(latest)
+ .isNotNull()
+ }
+ }
+ }
+
+ @Test
+ fun contentDescription_inflated_testABunchOfLevelsForNull() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.contentDescription)
+ repository.inflateSignalStrength.value = true
+ repository.numberOfLevels.value = 6
+ // -1 and 6 are out of the bounds for inflated content descriptions
+ // Note that the interactor adds 1 to the reported level, hence the -2 to 5 range
+ for (i in -2..5) {
+ repository.setAllLevels(i)
+ when (i) {
+ -2,
+ 5 ->
+ assertWithMessage("Level $i is expected to be 'no signal'")
+ .that((latest as MobileContentDescription.Cellular).levelDescriptionRes)
+ .isEqualTo(NO_SIGNAL)
+ else ->
+ assertWithMessage("Level $i is not expected to be null")
+ .that(latest)
+ .isNotNull()
+ }
+ }
+ }
+
+ @Test
+ fun networkType_dataEnabled_groupIsRepresented() =
+ testScope.runTest {
+ val expected =
+ Icon.Resource(
+ THREE_G.dataType,
+ ContentDescription.Resource(THREE_G.dataContentDescription),
+ )
+ connectionsRepository.mobileIsDefault.value = true
+ repository.setNetworkTypeKey(connectionsRepository.GSM_KEY)
+
+ var latest: Icon? = null
+ val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isEqualTo(expected)
+
+ job.cancel()
+ }
+
+ @Test
+ fun networkType_null_whenDisabled() =
+ testScope.runTest {
+ repository.setNetworkTypeKey(connectionsRepository.GSM_KEY)
+ repository.setDataEnabled(false)
+ connectionsRepository.mobileIsDefault.value = true
+ var latest: Icon? = null
+ val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isNull()
+
+ job.cancel()
+ }
+
+ @Test
+ fun networkType_null_whenCarrierNetworkChangeActive() =
+ testScope.runTest {
+ repository.setNetworkTypeKey(connectionsRepository.GSM_KEY)
+ repository.carrierNetworkChangeActive.value = true
+ connectionsRepository.mobileIsDefault.value = true
+ var latest: Icon? = null
+ val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isNull()
+
+ job.cancel()
+ }
+
+ @Test
+ fun networkTypeIcon_notNull_whenEnabled() =
+ testScope.runTest {
+ val expected =
+ Icon.Resource(
+ THREE_G.dataType,
+ ContentDescription.Resource(THREE_G.dataContentDescription),
+ )
+ repository.setNetworkTypeKey(connectionsRepository.GSM_KEY)
+ repository.setDataEnabled(true)
+ repository.dataConnectionState.value = DataConnectionState.Connected
+ connectionsRepository.mobileIsDefault.value = true
+ var latest: Icon? = null
+ val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isEqualTo(expected)
+
+ job.cancel()
+ }
+
+ @Test
+ fun networkType_nullWhenDataDisconnects() =
+ testScope.runTest {
+ val initial =
+ Icon.Resource(
+ THREE_G.dataType,
+ ContentDescription.Resource(THREE_G.dataContentDescription),
+ )
+
+ repository.setNetworkTypeKey(connectionsRepository.GSM_KEY)
+ var latest: Icon? = null
+ val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isEqualTo(initial)
+
+ repository.dataConnectionState.value = DataConnectionState.Disconnected
+
+ assertThat(latest).isNull()
+
+ job.cancel()
+ }
+
+ @Test
+ fun networkType_null_changeToDisabled() =
+ testScope.runTest {
+ val expected =
+ Icon.Resource(
+ THREE_G.dataType,
+ ContentDescription.Resource(THREE_G.dataContentDescription),
+ )
+ repository.dataEnabled.value = true
+ var latest: Icon? = null
+ val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isEqualTo(expected)
+
+ repository.dataEnabled.value = false
+
+ assertThat(latest).isNull()
+
+ job.cancel()
+ }
+
+ @Test
+ fun networkType_alwaysShow_shownEvenWhenDisabled() =
+ testScope.runTest {
+ repository.dataEnabled.value = false
+
+ connectionsRepository.defaultDataSubRatConfig.value =
+ MobileMappings.Config().also { it.alwaysShowDataRatIcon = true }
+
+ var latest: Icon? = null
+ val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ val expected =
+ Icon.Resource(
+ THREE_G.dataType,
+ ContentDescription.Resource(THREE_G.dataContentDescription),
+ )
+ assertThat(latest).isEqualTo(expected)
+
+ job.cancel()
+ }
+
+ @Test
+ fun networkType_alwaysShow_shownEvenWhenDisconnected() =
+ testScope.runTest {
+ repository.setNetworkTypeKey(connectionsRepository.GSM_KEY)
+ repository.dataConnectionState.value = DataConnectionState.Disconnected
+
+ connectionsRepository.defaultDataSubRatConfig.value =
+ MobileMappings.Config().also { it.alwaysShowDataRatIcon = true }
+
+ var latest: Icon? = null
+ val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ val expected =
+ Icon.Resource(
+ THREE_G.dataType,
+ ContentDescription.Resource(THREE_G.dataContentDescription),
+ )
+ assertThat(latest).isEqualTo(expected)
+
+ job.cancel()
+ }
+
+ @Test
+ fun networkType_alwaysShow_shownEvenWhenFailedConnection() =
+ testScope.runTest {
+ repository.setNetworkTypeKey(connectionsRepository.GSM_KEY)
+ connectionsRepository.mobileIsDefault.value = true
+ connectionsRepository.defaultDataSubRatConfig.value =
+ MobileMappings.Config().also { it.alwaysShowDataRatIcon = true }
+
+ var latest: Icon? = null
+ val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ val expected =
+ Icon.Resource(
+ THREE_G.dataType,
+ ContentDescription.Resource(THREE_G.dataContentDescription),
+ )
+ assertThat(latest).isEqualTo(expected)
+
+ job.cancel()
+ }
+
+ @Test
+ fun networkType_alwaysShow_usesDefaultIconWhenInvalid() =
+ testScope.runTest {
+ // The UNKNOWN icon group doesn't have a valid data type icon ID, and the logic from the
+ // old pipeline was to use the default icon group if the map doesn't exist
+ repository.setNetworkTypeKey(UNKNOWN.name)
+ connectionsRepository.defaultDataSubRatConfig.value =
+ MobileMappings.Config().also { it.alwaysShowDataRatIcon = true }
+
+ var latest: Icon? = null
+ val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ val expected =
+ Icon.Resource(
+ connectionsRepository.defaultMobileIconGroup.value.dataType,
+ ContentDescription.Resource(G.dataContentDescription),
+ )
+
+ assertThat(latest).isEqualTo(expected)
+
+ job.cancel()
+ }
+
+ @Test
+ fun networkType_alwaysShow_shownWhenNotDefault() =
+ testScope.runTest {
+ repository.setNetworkTypeKey(connectionsRepository.GSM_KEY)
+ connectionsRepository.mobileIsDefault.value = false
+ connectionsRepository.defaultDataSubRatConfig.value =
+ MobileMappings.Config().also { it.alwaysShowDataRatIcon = true }
+
+ var latest: Icon? = null
+ val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ val expected =
+ Icon.Resource(
+ THREE_G.dataType,
+ ContentDescription.Resource(THREE_G.dataContentDescription),
+ )
+ assertThat(latest).isEqualTo(expected)
+
+ job.cancel()
+ }
+
+ @Test
+ fun networkType_notShownWhenNotDefault() =
+ testScope.runTest {
+ repository.setNetworkTypeKey(connectionsRepository.GSM_KEY)
+ repository.dataConnectionState.value = DataConnectionState.Connected
+ connectionsRepository.mobileIsDefault.value = false
+
+ var latest: Icon? = null
+ val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isNull()
+
+ job.cancel()
+ }
+
+ @Test
+ fun roaming() =
+ testScope.runTest {
+ repository.setAllRoaming(true)
+
+ var latest: Boolean? = null
+ val job = underTest.roaming.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isTrue()
+
+ repository.setAllRoaming(false)
+
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun dataActivity_nullWhenConfigIsOff() =
+ testScope.runTest {
+ // Create a new view model here so the constants are properly read
+ whenever(constants.shouldShowActivityConfig).thenReturn(false)
+ createAndSetViewModel()
+
+ var inVisible: Boolean? = null
+ val inJob = underTest.activityInVisible.onEach { inVisible = it }.launchIn(this)
+
+ var outVisible: Boolean? = null
+ val outJob = underTest.activityInVisible.onEach { outVisible = it }.launchIn(this)
+
+ var containerVisible: Boolean? = null
+ val containerJob =
+ underTest.activityInVisible.onEach { containerVisible = it }.launchIn(this)
+
+ repository.dataActivityDirection.value =
+ DataActivityModel(hasActivityIn = true, hasActivityOut = true)
+
+ assertThat(inVisible).isFalse()
+ assertThat(outVisible).isFalse()
+ assertThat(containerVisible).isFalse()
+
+ inJob.cancel()
+ outJob.cancel()
+ containerJob.cancel()
+ }
+
+ @Test
+ @DisableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS)
+ fun dataActivity_configOn_testIndicators_staticFlagOff() =
+ testScope.runTest {
+ // Create a new view model here so the constants are properly read
+ whenever(constants.shouldShowActivityConfig).thenReturn(true)
+ createAndSetViewModel()
+
+ var inVisible: Boolean? = null
+ val inJob = underTest.activityInVisible.onEach { inVisible = it }.launchIn(this)
+
+ var outVisible: Boolean? = null
+ val outJob = underTest.activityOutVisible.onEach { outVisible = it }.launchIn(this)
+
+ var containerVisible: Boolean? = null
+ val containerJob =
+ underTest.activityContainerVisible.onEach { containerVisible = it }.launchIn(this)
+
+ repository.dataActivityDirection.value =
+ DataActivityModel(hasActivityIn = true, hasActivityOut = false)
+
+ yield()
+
+ assertThat(inVisible).isTrue()
+ assertThat(outVisible).isFalse()
+ assertThat(containerVisible).isTrue()
+
+ repository.dataActivityDirection.value =
+ DataActivityModel(hasActivityIn = false, hasActivityOut = true)
+
+ assertThat(inVisible).isFalse()
+ assertThat(outVisible).isTrue()
+ assertThat(containerVisible).isTrue()
+
+ repository.dataActivityDirection.value =
+ DataActivityModel(hasActivityIn = false, hasActivityOut = false)
+
+ assertThat(inVisible).isFalse()
+ assertThat(outVisible).isFalse()
+ assertThat(containerVisible).isFalse()
+
+ inJob.cancel()
+ outJob.cancel()
+ containerJob.cancel()
+ }
+
+ @Test
+ @EnableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS)
+ fun dataActivity_configOn_testIndicators_staticFlagOn() =
+ testScope.runTest {
+ // Create a new view model here so the constants are properly read
+ whenever(constants.shouldShowActivityConfig).thenReturn(true)
+ createAndSetViewModel()
+
+ var inVisible: Boolean? = null
+ val inJob = underTest.activityInVisible.onEach { inVisible = it }.launchIn(this)
+
+ var outVisible: Boolean? = null
+ val outJob = underTest.activityOutVisible.onEach { outVisible = it }.launchIn(this)
+
+ var containerVisible: Boolean? = null
+ val containerJob =
+ underTest.activityContainerVisible.onEach { containerVisible = it }.launchIn(this)
+
+ repository.dataActivityDirection.value =
+ DataActivityModel(hasActivityIn = true, hasActivityOut = false)
+
+ yield()
+
+ assertThat(inVisible).isTrue()
+ assertThat(outVisible).isFalse()
+ assertThat(containerVisible).isTrue()
+
+ repository.dataActivityDirection.value =
+ DataActivityModel(hasActivityIn = false, hasActivityOut = true)
+
+ assertThat(inVisible).isFalse()
+ assertThat(outVisible).isTrue()
+ assertThat(containerVisible).isTrue()
+
+ repository.dataActivityDirection.value =
+ DataActivityModel(hasActivityIn = false, hasActivityOut = false)
+
+ assertThat(inVisible).isFalse()
+ assertThat(outVisible).isFalse()
+ assertThat(containerVisible).isTrue()
+
+ inJob.cancel()
+ outJob.cancel()
+ containerJob.cancel()
+ }
+
+ @Test
+ fun netTypeBackground_nullWhenNoPrioritizedCapabilities() =
+ testScope.runTest {
+ createAndSetViewModel()
+
+ val latest by collectLastValue(underTest.networkTypeBackground)
+
+ repository.hasPrioritizedNetworkCapabilities.value = false
+
+ assertThat(latest).isNull()
+ }
+
+ @Test
+ @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
+ fun netTypeBackground_sliceUiEnabled_notNullWhenPrioritizedCapabilities_newIcons() =
+ testScope.runTest {
+ createAndSetViewModel()
+
+ val latest by collectLastValue(underTest.networkTypeBackground)
+
+ repository.hasPrioritizedNetworkCapabilities.value = true
+
+ assertThat(latest)
+ .isEqualTo(Icon.Resource(R.drawable.mobile_network_type_background_updated, null))
+ }
+
+ @Test
+ @DisableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
+ fun netTypeBackground_sliceUiDisabled_notNullWhenPrioritizedCapabilities_oldIcons() =
+ testScope.runTest {
+ createAndSetViewModel()
+
+ val latest by collectLastValue(underTest.networkTypeBackground)
+
+ repository.hasPrioritizedNetworkCapabilities.value = true
+
+ assertThat(latest)
+ .isEqualTo(Icon.Resource(R.drawable.mobile_network_type_background, null))
+ }
+
+ @Test
+ fun nonTerrestrial_defaultProperties() =
+ testScope.runTest {
+ repository.isNonTerrestrial.value = true
+
+ val roaming by collectLastValue(underTest.roaming)
+ val networkTypeIcon by collectLastValue(underTest.networkTypeIcon)
+ val networkTypeBackground by collectLastValue(underTest.networkTypeBackground)
+ val activityInVisible by collectLastValue(underTest.activityInVisible)
+ val activityOutVisible by collectLastValue(underTest.activityOutVisible)
+ val activityContainerVisible by collectLastValue(underTest.activityContainerVisible)
+
+ assertThat(roaming).isFalse()
+ assertThat(networkTypeIcon).isNull()
+ assertThat(networkTypeBackground).isNull()
+ assertThat(activityInVisible).isFalse()
+ assertThat(activityOutVisible).isFalse()
+ assertThat(activityContainerVisible).isFalse()
+ }
+
+ @Test
+ fun nonTerrestrial_ignoresDefaultProperties() =
+ testScope.runTest {
+ repository.isNonTerrestrial.value = true
+
+ val roaming by collectLastValue(underTest.roaming)
+ val networkTypeIcon by collectLastValue(underTest.networkTypeIcon)
+ val networkTypeBackground by collectLastValue(underTest.networkTypeBackground)
+ val activityInVisible by collectLastValue(underTest.activityInVisible)
+ val activityOutVisible by collectLastValue(underTest.activityOutVisible)
+ val activityContainerVisible by collectLastValue(underTest.activityContainerVisible)
+
+ repository.setAllRoaming(true)
+ repository.setNetworkTypeKey(connectionsRepository.LTE_KEY)
+ // sets the background on cellular
+ repository.hasPrioritizedNetworkCapabilities.value = true
+ repository.dataActivityDirection.value =
+ DataActivityModel(hasActivityIn = true, hasActivityOut = true)
+
+ assertThat(roaming).isFalse()
+ assertThat(networkTypeIcon).isNull()
+ assertThat(networkTypeBackground).isNull()
+ assertThat(activityInVisible).isFalse()
+ assertThat(activityOutVisible).isFalse()
+ assertThat(activityContainerVisible).isFalse()
+ }
+
+ @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+ @Test
+ fun nonTerrestrial_usesSatelliteIcon_flagOff() =
+ testScope.runTest {
+ repository.isNonTerrestrial.value = true
+ repository.setAllLevels(0)
+ repository.satelliteLevel.value = 0
+
+ val latest by
+ collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class))
+
+ // Level 0 -> no connection
+ assertThat(latest).isNotNull()
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0)
+
+ // 1-2 -> 1 bar
+ repository.setAllLevels(1)
+ repository.satelliteLevel.value = 1
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
+
+ repository.setAllLevels(2)
+ repository.satelliteLevel.value = 2
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
+
+ // 3-4 -> 2 bars
+ repository.setAllLevels(3)
+ repository.satelliteLevel.value = 3
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
+
+ repository.setAllLevels(4)
+ repository.satelliteLevel.value = 4
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
+ }
+
+ @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+ @Test
+ fun nonTerrestrial_usesSatelliteIcon_flagOn() =
+ testScope.runTest {
+ repository.isNonTerrestrial.value = true
+ repository.satelliteLevel.value = 0
+
+ val latest by
+ collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class))
+
+ // Level 0 -> no connection
+ assertThat(latest).isNotNull()
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0)
+
+ // 1-2 -> 1 bar
+ repository.satelliteLevel.value = 1
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
+
+ repository.satelliteLevel.value = 2
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
+
+ // 3-4 -> 2 bars
+ repository.satelliteLevel.value = 3
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
+
+ repository.satelliteLevel.value = 4
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
+ }
+
+ @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+ @Test
+ fun satelliteIcon_ignoresInflateSignalStrength_flagOff() =
+ testScope.runTest {
+ // Note that this is the exact same test as above, but with inflateSignalStrength set to
+ // true we note that the level is unaffected by inflation
+ repository.inflateSignalStrength.value = true
+ repository.isNonTerrestrial.value = true
+ repository.setAllLevels(0)
+ repository.satelliteLevel.value = 0
+
+ val latest by
+ collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class))
+
+ // Level 0 -> no connection
+ assertThat(latest).isNotNull()
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0)
+
+ // 1-2 -> 1 bar
+ repository.setAllLevels(1)
+ repository.satelliteLevel.value = 1
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
+
+ repository.setAllLevels(2)
+ repository.satelliteLevel.value = 2
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
+
+ // 3-4 -> 2 bars
+ repository.setAllLevels(3)
+ repository.satelliteLevel.value = 3
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
+
+ repository.setAllLevels(4)
+ repository.satelliteLevel.value = 4
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
+ }
+
+ @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+ @Test
+ fun satelliteIcon_ignoresInflateSignalStrength_flagOn() =
+ testScope.runTest {
+ // Note that this is the exact same test as above, but with inflateSignalStrength set to
+ // true we note that the level is unaffected by inflation
+ repository.inflateSignalStrength.value = true
+ repository.isNonTerrestrial.value = true
+ repository.satelliteLevel.value = 0
+
+ val latest by
+ collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class))
+
+ // Level 0 -> no connection
+ assertThat(latest).isNotNull()
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0)
+
+ // 1-2 -> 1 bar
+ repository.satelliteLevel.value = 1
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
+
+ repository.satelliteLevel.value = 2
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
+
+ // 3-4 -> 2 bars
+ repository.satelliteLevel.value = 3
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
+
+ repository.satelliteLevel.value = 4
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
+ }
+
+ private fun createAndSetViewModel() {
+ underTest =
+ MobileIconViewModelKairos(
+ SUB_1_ID,
+ interactor,
+ airplaneModeInteractor,
+ constants,
+ testScope.backgroundScope,
+ )
+ }
+
+ companion object {
+ private const val SUB_1_ID = 1
+
+ // For convenience, just define these as constants
+ private val NO_SIGNAL = R.string.accessibility_no_signal
+ private val ONE_BAR = R.string.accessibility_one_bar
+ private val TWO_BARS = R.string.accessibility_two_bars
+ private val THREE_BARS = R.string.accessibility_three_bars
+ private val FOUR_BARS = R.string.accessibility_four_bars
+ private val FULL_BARS = R.string.accessibility_signal_full
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairosTest.kt
new file mode 100644
index 000000000000..e921430394c2
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairosTest.kt
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2022 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.pipeline.mobile.ui.viewmodel
+
+import android.telephony.SubscriptionManager.PROFILE_CLASS_UNSET
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlagsClassic
+import com.android.systemui.statusbar.phone.StatusBarLocation
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel
+import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger
+import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
+import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class MobileIconsViewModelKairosTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+
+ private lateinit var underTest: MobileIconsViewModelKairos
+ private val interactor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
+ private val flags = FakeFeatureFlagsClassic()
+
+ private lateinit var airplaneModeInteractor: AirplaneModeInteractor
+ @Mock private lateinit var constants: ConnectivityConstants
+ @Mock private lateinit var logger: MobileViewLogger
+ @Mock private lateinit var verboseLogger: VerboseMobileViewLogger
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ airplaneModeInteractor =
+ AirplaneModeInteractor(
+ FakeAirplaneModeRepository(),
+ FakeConnectivityRepository(),
+ kosmos.fakeMobileConnectionsRepository,
+ )
+
+ underTest =
+ MobileIconsViewModelKairos(
+ logger,
+ verboseLogger,
+ interactor,
+ airplaneModeInteractor,
+ constants,
+ testScope.backgroundScope,
+ )
+
+ interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
+ }
+
+ @Test
+ fun subscriptionIdsFlow_matchesInteractor() =
+ testScope.runTest {
+ var latest: List<Int>? = null
+ val job = underTest.subscriptionIdsFlow.onEach { latest = it }.launchIn(this)
+
+ interactor.filteredSubscriptions.value =
+ listOf(
+ SubscriptionModel(
+ subscriptionId = 1,
+ isOpportunistic = false,
+ carrierName = "Carrier 1",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+ )
+ assertThat(latest).isEqualTo(listOf(1))
+
+ interactor.filteredSubscriptions.value =
+ listOf(
+ SubscriptionModel(
+ subscriptionId = 2,
+ isOpportunistic = false,
+ carrierName = "Carrier 2",
+ profileClass = PROFILE_CLASS_UNSET,
+ ),
+ SubscriptionModel(
+ subscriptionId = 5,
+ isOpportunistic = true,
+ carrierName = "Carrier 5",
+ profileClass = PROFILE_CLASS_UNSET,
+ ),
+ SubscriptionModel(
+ subscriptionId = 7,
+ isOpportunistic = true,
+ carrierName = "Carrier 7",
+ profileClass = PROFILE_CLASS_UNSET,
+ ),
+ )
+ assertThat(latest).isEqualTo(listOf(2, 5, 7))
+
+ interactor.filteredSubscriptions.value = emptyList()
+ assertThat(latest).isEmpty()
+
+ job.cancel()
+ }
+
+ @Test
+ fun caching_mobileIconViewModelIsReusedForSameSubId() =
+ testScope.runTest {
+ val model1 = underTest.viewModelForSub(1, StatusBarLocation.HOME)
+ val model2 = underTest.viewModelForSub(1, StatusBarLocation.QS)
+
+ assertThat(model1.commonImpl).isSameInstanceAs(model2.commonImpl)
+ }
+
+ @Test
+ fun caching_invalidViewModelsAreRemovedFromCacheWhenSubDisappears() =
+ testScope.runTest {
+ // Retrieve models to trigger caching
+ val model1 = underTest.viewModelForSub(1, StatusBarLocation.HOME)
+ val model2 = underTest.viewModelForSub(2, StatusBarLocation.QS)
+
+ // Both impls are cached
+ assertThat(underTest.reuseCache.keys).containsExactly(1, 2)
+
+ // SUB_1 is removed from the list...
+ interactor.filteredSubscriptions.value = listOf(SUB_2)
+
+ // ... and dropped from the cache
+ assertThat(underTest.reuseCache.keys).containsExactly(2)
+ }
+
+ @Test
+ fun caching_invalidatedViewModelsAreCanceled() =
+ testScope.runTest {
+ // Retrieve models to trigger caching
+ val model1 = underTest.viewModelForSub(1, StatusBarLocation.HOME)
+ val model2 = underTest.viewModelForSub(2, StatusBarLocation.QS)
+
+ var scope1 = underTest.reuseCache[1]?.second
+ var scope2 = underTest.reuseCache[2]?.second
+
+ // Scopes are not canceled
+ assertTrue(scope1!!.isActive)
+ assertTrue(scope2!!.isActive)
+
+ // SUB_1 is removed from the list...
+ interactor.filteredSubscriptions.value = listOf(SUB_2)
+
+ // scope1 is canceled
+ assertFalse(scope1!!.isActive)
+ assertTrue(scope2!!.isActive)
+ }
+
+ @Test
+ fun firstMobileSubShowingNetworkTypeIcon_noSubs_false() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job =
+ underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ interactor.filteredSubscriptions.value = emptyList()
+
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun firstMobileSubShowingNetworkTypeIcon_oneSub_notShowingRat_false() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job =
+ underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ interactor.filteredSubscriptions.value = listOf(SUB_1)
+ // The unknown icon group doesn't show a RAT
+ interactor.getInteractorForSubId(1)!!.networkTypeIconGroup.value =
+ NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN)
+
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun firstMobileSubShowingNetworkTypeIcon_oneSub_showingRat_true() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job =
+ underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ interactor.filteredSubscriptions.value = listOf(SUB_1)
+ // The 3G icon group will show a RAT
+ interactor.getInteractorForSubId(1)!!.networkTypeIconGroup.value =
+ NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G)
+
+ assertThat(latest).isTrue()
+
+ job.cancel()
+ }
+
+ @Test
+ fun firstMobileSubShowingNetworkTypeIcon_updatesAsSubUpdates() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job =
+ underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ interactor.filteredSubscriptions.value = listOf(SUB_1)
+ val sub1Interactor = interactor.getInteractorForSubId(1)!!
+
+ sub1Interactor.networkTypeIconGroup.value =
+ NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G)
+ assertThat(latest).isTrue()
+
+ sub1Interactor.networkTypeIconGroup.value =
+ NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN)
+ assertThat(latest).isFalse()
+
+ sub1Interactor.networkTypeIconGroup.value =
+ NetworkTypeIconModel.DefaultIcon(TelephonyIcons.LTE)
+ assertThat(latest).isTrue()
+
+ job.cancel()
+ }
+
+ @Test
+ fun firstMobileSubShowingNetworkTypeIcon_multipleSubs_lastSubNotShowingRat_false() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job =
+ underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
+ interactor.getInteractorForSubId(1)?.networkTypeIconGroup?.value =
+ NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G)
+ interactor.getInteractorForSubId(2)!!.networkTypeIconGroup.value =
+ NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN)
+
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun firstMobileSubShowingNetworkTypeIcon_multipleSubs_lastSubShowingRat_true() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job =
+ underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
+ interactor.getInteractorForSubId(1)?.networkTypeIconGroup?.value =
+ NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN)
+ interactor.getInteractorForSubId(2)!!.networkTypeIconGroup.value =
+ NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G)
+
+ assertThat(latest).isTrue()
+ job.cancel()
+ }
+
+ @Test
+ fun firstMobileSubShowingNetworkTypeIcon_subListUpdates_valAlsoUpdates() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job =
+ underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
+ interactor.getInteractorForSubId(1)?.networkTypeIconGroup?.value =
+ NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN)
+ interactor.getInteractorForSubId(2)!!.networkTypeIconGroup.value =
+ NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G)
+
+ assertThat(latest).isTrue()
+
+ // WHEN the sub list gets new subscriptions where the last subscription is not showing
+ // the network type icon
+ interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2, SUB_3)
+ interactor.getInteractorForSubId(3)!!.networkTypeIconGroup.value =
+ NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN)
+
+ // THEN the flow updates
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun firstMobileSubShowingNetworkTypeIcon_subListReorders_valAlsoUpdates() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job =
+ underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
+ // Immediately switch the order so that we've created both interactors
+ interactor.filteredSubscriptions.value = listOf(SUB_2, SUB_1)
+ val sub1Interactor = interactor.getInteractorForSubId(1)!!
+ val sub2Interactor = interactor.getInteractorForSubId(2)!!
+
+ interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
+ sub1Interactor.networkTypeIconGroup.value =
+ NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN)
+ sub2Interactor.networkTypeIconGroup.value =
+ NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G)
+ assertThat(latest).isTrue()
+
+ // WHEN sub1 becomes last and sub1 has no network type icon
+ interactor.filteredSubscriptions.value = listOf(SUB_2, SUB_1)
+
+ // THEN the flow updates
+ assertThat(latest).isFalse()
+
+ // WHEN sub2 becomes last and sub2 has a network type icon
+ interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
+
+ // THEN the flow updates
+ assertThat(latest).isTrue()
+
+ job.cancel()
+ }
+
+ companion object {
+ private val SUB_1 =
+ SubscriptionModel(
+ subscriptionId = 1,
+ isOpportunistic = false,
+ carrierName = "Carrier 1",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+ private val SUB_2 =
+ SubscriptionModel(
+ subscriptionId = 2,
+ isOpportunistic = false,
+ carrierName = "Carrier 2",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+ private val SUB_3 =
+ SubscriptionModel(
+ subscriptionId = 3,
+ isOpportunistic = false,
+ carrierName = "Carrier 3",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosTest.kt
new file mode 100644
index 000000000000..ce35d9d8610f
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosTest.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
+
+import android.platform.test.annotations.EnableFlags
+import android.telephony.SubscriptionManager.PROFILE_CLASS_UNSET
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.statusbar.core.NewStatusBarIcons
+import com.android.systemui.statusbar.core.StatusBarRootModernization
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.fakeMobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class StackedMobileIconViewModelKairosTest : SysuiTestCase() {
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+ private val testScope = kosmos.testScope
+
+ private val Kosmos.underTest: StackedMobileIconViewModelKairos by Fixture {
+ stackedMobileIconViewModelKairos
+ }
+
+ @Before
+ fun setUp() {
+ kosmos.underTest.activateIn(testScope)
+ }
+
+ @Test
+ @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
+ fun dualSim_filtersOutNonDualConnections() =
+ kosmos.runTest {
+ fakeMobileIconsInteractor.filteredSubscriptions.value = listOf()
+ assertThat(underTest.dualSim).isNull()
+
+ fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1)
+ assertThat(underTest.dualSim).isNull()
+
+ fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2, SUB_3)
+ assertThat(underTest.dualSim).isNull()
+
+ fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
+ assertThat(underTest.dualSim).isNotNull()
+ }
+
+ @Test
+ @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
+ fun dualSim_filtersOutNonCellularIcons() =
+ kosmos.runTest {
+ fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1)
+ assertThat(underTest.dualSim).isNull()
+
+ fakeMobileIconsInteractor
+ .getInteractorForSubId(SUB_1.subscriptionId)!!
+ .signalLevelIcon
+ .value =
+ SignalIconModel.Satellite(
+ level = 0,
+ icon = Icon.Resource(res = 0, contentDescription = null),
+ )
+ fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
+ assertThat(underTest.dualSim).isNull()
+ }
+
+ @Test
+ @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
+ fun dualSim_tracksActiveSubId() =
+ kosmos.runTest {
+ // Active sub id is null, order is unchanged
+ fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
+ setIconLevel(SUB_1.subscriptionId, 1)
+ setIconLevel(SUB_2.subscriptionId, 2)
+
+ assertThat(underTest.dualSim!!.primary.level).isEqualTo(1)
+ assertThat(underTest.dualSim!!.secondary.level).isEqualTo(2)
+
+ // Active sub is 2, order is swapped
+ fakeMobileIconsInteractor.activeMobileDataSubscriptionId.value = SUB_2.subscriptionId
+
+ assertThat(underTest.dualSim!!.primary.level).isEqualTo(2)
+ assertThat(underTest.dualSim!!.secondary.level).isEqualTo(1)
+ }
+
+ private fun setIconLevel(subId: Int, level: Int) {
+ with(kosmos.fakeMobileIconsInteractor.getInteractorForSubId(subId)!!) {
+ signalLevelIcon.value =
+ (signalLevelIcon.value as SignalIconModel.Cellular).copy(level = level)
+ }
+ }
+
+ companion object {
+ private val SUB_1 =
+ SubscriptionModel(
+ subscriptionId = 1,
+ isOpportunistic = false,
+ carrierName = "Carrier 1",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+ private val SUB_2 =
+ SubscriptionModel(
+ subscriptionId = 2,
+ isOpportunistic = false,
+ carrierName = "Carrier 2",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+ private val SUB_3 =
+ SubscriptionModel(
+ subscriptionId = 3,
+ isOpportunistic = false,
+ carrierName = "Carrier 3",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/TopLevelWindowEffectsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/TopLevelWindowEffectsTest.kt
new file mode 100644
index 000000000000..6d3813c90bfd
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/TopLevelWindowEffectsTest.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects
+
+import android.view.View
+import android.view.WindowManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.app.viewcapture.ViewCapture
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyevent.data.repository.fakeKeyEventRepository
+import com.android.systemui.keyevent.data.repository.keyEventRepository
+import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor
+import com.android.systemui.keyevent.domain.interactor.keyEventInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.testKosmos
+import com.android.systemui.topwindoweffects.data.repository.fakeSqueezeEffectRepository
+import com.android.systemui.topwindoweffects.domain.interactor.SqueezeEffectInteractor
+import com.android.systemui.topwindoweffects.ui.compose.EffectsWindowRoot
+import com.android.systemui.topwindoweffects.ui.viewmodel.SqueezeEffectViewModel
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.never
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class TopLevelWindowEffectsTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+
+ @Mock
+ private lateinit var windowManager: WindowManager
+
+ @Mock
+ private lateinit var viewCapture: Lazy<ViewCapture>
+
+ @Mock
+ private lateinit var viewModelFactory: SqueezeEffectViewModel.Factory
+
+ private val Kosmos.underTest by Kosmos.Fixture {
+ TopLevelWindowEffects(
+ context = mContext,
+ applicationScope = testScope.backgroundScope,
+ windowManager = ViewCaptureAwareWindowManager(
+ windowManager = windowManager,
+ lazyViewCapture = viewCapture,
+ isViewCaptureEnabled = false
+ ),
+ keyEventInteractor = keyEventInteractor,
+ viewModelFactory = viewModelFactory,
+ squeezeEffectInteractor = SqueezeEffectInteractor(
+ squeezeEffectRepository = fakeSqueezeEffectRepository
+ )
+ )
+ }
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ doNothing().whenever(windowManager).addView(any<View>(), any<WindowManager.LayoutParams>())
+ doNothing().whenever(windowManager).removeView(any<View>())
+ doNothing().whenever(windowManager).removeView(any<EffectsWindowRoot>())
+ }
+
+ @Test
+ fun noWindowWhenSqueezeEffectDisabled() =
+ kosmos.runTest {
+ fakeSqueezeEffectRepository.isSqueezeEffectEnabled.value = false
+
+ underTest.start()
+
+ verify(windowManager, never()).addView(any<View>(), any<WindowManager.LayoutParams>())
+ }
+
+ @Test
+ fun addViewToWindowWhenSqueezeEffectEnabled() =
+ kosmos.runTest {
+ fakeSqueezeEffectRepository.isSqueezeEffectEnabled.value = true
+ fakeKeyEventRepository.setPowerButtonDown(true)
+
+ underTest.start()
+
+ verify(windowManager, times(1)).addView(any<View>(),
+ any<WindowManager.LayoutParams>())
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryTest.kt
new file mode 100644
index 000000000000..9b01fd3242e5
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryTest.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects.data.repository
+
+import android.os.Handler
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.provider.Settings.Global.POWER_BUTTON_LONG_PRESS
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.shared.Flags
+import com.android.systemui.testKosmos
+import com.android.systemui.util.settings.FakeGlobalSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.StandardTestDispatcher
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SqueezeEffectRepositoryTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+ private val globalSettings = FakeGlobalSettings(StandardTestDispatcher())
+
+ @Mock
+ private lateinit var bgHandler: Handler
+
+ private val Kosmos.underTest by Kosmos.Fixture {
+ SqueezeEffectRepositoryImpl(
+ bgHandler = bgHandler,
+ bgCoroutineContext = testScope.testScheduler,
+ globalSettings = globalSettings
+ )
+ }
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ }
+
+ @DisableFlags(Flags.FLAG_ENABLE_LPP_SQUEEZE_EFFECT)
+ @Test
+ fun testSqueezeEffectDisabled_WhenFlagDisabled() =
+ kosmos.runTest {
+ val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled)
+
+ assertThat(isSqueezeEffectEnabled).isFalse()
+ }
+
+ @EnableFlags(Flags.FLAG_ENABLE_LPP_SQUEEZE_EFFECT)
+ @Test
+ fun testSqueezeEffectDisabled_WhenFlagEnabled_GlobalSettingsDisabled() =
+ kosmos.runTest {
+ globalSettings.putInt(POWER_BUTTON_LONG_PRESS, 0)
+
+ val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled)
+
+ assertThat(isSqueezeEffectEnabled).isFalse()
+ }
+
+ @EnableFlags(Flags.FLAG_ENABLE_LPP_SQUEEZE_EFFECT)
+ @Test
+ fun testSqueezeEffectEnabled_WhenFlagEnabled_GlobalSettingEnabled() =
+ kosmos.runTest {
+ globalSettings.putInt(POWER_BUTTON_LONG_PRESS, 5)
+
+ val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled)
+
+ assertThat(isSqueezeEffectEnabled).isTrue()
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/domain/interactor/SqueezeEffectInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/domain/interactor/SqueezeEffectInteractorTest.kt
new file mode 100644
index 000000000000..a94d49c5d40e
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/domain/interactor/SqueezeEffectInteractorTest.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.testKosmos
+import com.android.systemui.topwindoweffects.data.repository.fakeSqueezeEffectRepository
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SqueezeEffectInteractorTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+
+ private val Kosmos.underTest by Kosmos.Fixture {
+ SqueezeEffectInteractor(
+ squeezeEffectRepository = fakeSqueezeEffectRepository
+ )
+ }
+
+ @Test
+ fun testIsSqueezeEffectDisabled_whenDisabledInRepository() =
+ kosmos.runTest {
+ fakeSqueezeEffectRepository.isSqueezeEffectEnabled.value = false
+
+ val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled)
+
+ assertThat(isSqueezeEffectEnabled).isFalse()
+ }
+
+ @Test
+ fun testIsSqueezeEffectEnabled_whenEnabledInRepository() =
+ kosmos.runTest {
+ fakeSqueezeEffectRepository.isSqueezeEffectEnabled.value = true
+
+ val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled)
+
+ assertThat(isSqueezeEffectEnabled).isTrue()
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/ClientTrackingWakeLockTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/ClientTrackingWakeLockTest.kt
index fdfcdc486c02..9a58365116fd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/ClientTrackingWakeLockTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/ClientTrackingWakeLockTest.kt
@@ -16,14 +16,13 @@
package com.android.systemui.util.wakelock
-import android.os.Build
import android.os.PowerManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.assertLogsWtf
import org.junit.After
import org.junit.Assert
-import org.junit.Assume
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -82,12 +81,11 @@ class ClientTrackingWakeLockTest : SysuiTestCase() {
@Test
fun wakeLock_releasedTooManyTimes_stillReleased_noThrow() {
- Assume.assumeFalse(Build.IS_ENG)
mWakeLock.acquire(WHY)
mWakeLock.acquire(WHY_2)
mWakeLock.release(WHY)
mWakeLock.release(WHY_2)
- mWakeLock.release(WHY)
+ assertLogsWtf { mWakeLock.release(WHY) }
Assert.assertFalse(mInner.isHeld)
}
@@ -104,9 +102,8 @@ class ClientTrackingWakeLockTest : SysuiTestCase() {
@Test
fun prodBuild_wakeLock_releaseWithoutAcquire_noThrow() {
- Assume.assumeFalse(Build.IS_ENG)
// shouldn't throw an exception on production builds
- mWakeLock.release(WHY)
+ assertLogsWtf { mWakeLock.release(WHY) }
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/WakeLockTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/WakeLockTest.java
index 90aecfb62e91..3951670c4125 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/WakeLockTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/WakeLockTest.java
@@ -19,16 +19,15 @@ package com.android.systemui.util.wakelock;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
-import android.os.Build;
import android.os.PowerManager;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.log.LogAssertKt;
import org.junit.After;
-import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -93,8 +92,7 @@ public class WakeLockTest extends SysuiTestCase {
@Test
public void prodBuild_wakeLock_releaseWithoutAcquire_noThrow() {
- Assume.assumeFalse(Build.IS_ENG);
// shouldn't throw an exception on production builds
- mWakeLock.release(WHY);
+ LogAssertKt.assertRunnableLogsWtf(() -> mWakeLock.release(WHY));
}
}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt
index b52db83d513c..7657a2179d4f 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt
@@ -13,7 +13,6 @@
*/
package com.android.systemui.plugins.clocks
-import android.graphics.RectF
import com.android.systemui.plugins.annotations.ProtectedInterface
import com.android.systemui.plugins.annotations.SimpleProperty
import java.io.PrintWriter
@@ -50,5 +49,5 @@ interface ClockController {
}
interface ClockEventListener {
- fun onBoundsChanged(bounds: RectF)
+ fun onBoundsChanged(bounds: VRectF)
}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt
index 029e54658f60..a658c15a1a99 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt
@@ -13,6 +13,7 @@
*/
package com.android.systemui.plugins.clocks
+import android.content.Context
import android.graphics.Rect
import com.android.systemui.plugins.annotations.ProtectedInterface
@@ -44,6 +45,7 @@ interface ClockFaceEvents {
* render within the centered targetRect to avoid obstructing other elements. The specified
* targetRegion is relative to the parent view.
*/
+ @Deprecated("No longer necessary, pending removal")
fun onTargetRegionChanged(targetRegion: Rect?)
/** Called to notify the clock about its display. */
@@ -60,4 +62,12 @@ data class ThemeConfig(
* value denotes that we should use the seed color for the current system theme.
*/
val seedColor: Int?,
-)
+) {
+ fun getDefaultColor(context: Context): Int {
+ return when {
+ seedColor != null -> seedColor!!
+ isDarkTheme -> context.resources.getColor(android.R.color.system_accent1_100)
+ else -> context.resources.getColor(android.R.color.system_accent2_600)
+ }
+ }
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt
index 02a3902a042c..f9ff75d5fdc8 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt
@@ -16,7 +16,6 @@
package com.android.systemui.plugins.clocks
-import android.graphics.Rect
import android.view.View
import android.view.View.MeasureSpec
import com.android.systemui.log.core.LogLevel
@@ -56,12 +55,9 @@ class ClockLogger(private val view: View?, buffer: MessageBuffer, tag: String) :
}
fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
- d({ "onLayout($bool1, ${Rect(int1, int2, long1.toInt(), long2.toInt())})" }) {
+ d({ "onLayout($bool1, ${VRect(long1.toULong())})" }) {
bool1 = changed
- int1 = left
- int2 = top
- long1 = right.toLong()
- long2 = bottom.toLong()
+ long1 = VRect(left, top, right, bottom).data.toLong()
}
}
@@ -108,8 +104,11 @@ class ClockLogger(private val view: View?, buffer: MessageBuffer, tag: String) :
}
}
- fun animateDoze() {
- d("animateDoze()")
+ fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
+ d({ "animateDoze(isDozing=$bool1, isAnimated=$bool2)" }) {
+ bool1 = isDozing
+ bool2 = isAnimated
+ }
}
fun animateCharge() {
@@ -117,10 +116,7 @@ class ClockLogger(private val view: View?, buffer: MessageBuffer, tag: String) :
}
fun animateFidget(x: Float, y: Float) {
- d({ "animateFidget($str1, $str2)" }) {
- str1 = x.toString()
- str2 = y.toString()
- }
+ d({ "animateFidget(${VPointF(long1.toULong())})" }) { long1 = VPointF(x, y).data.toLong() }
}
companion object {
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/VPoint.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VPoint.kt
index 3dae5305542b..1fb37ec28835 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/VPoint.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VPoint.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.shared.clocks
+package com.android.systemui.plugins.clocks
import android.graphics.Point
import android.graphics.PointF
@@ -30,22 +30,20 @@ private val Y_MASK: ULong = 0x00000000FFFFFFFFU
private fun unpackX(data: ULong): Int = ((data and X_MASK) shr 32).toInt()
-private fun unpackY(data: ULong): Int = (data and Y_MASK).toInt()
+private fun unpackY(data: ULong): Int = ((data and Y_MASK) shr 0).toInt()
private fun pack(x: Int, y: Int): ULong {
- return ((x.toULong() shl 32) and X_MASK) or (y.toULong() and Y_MASK)
+ return ((x.toULong() shl 32) and X_MASK) or ((y.toULong() shl 0) and Y_MASK)
}
@JvmInline
-value class VPointF(private val data: ULong) {
+value class VPointF(val data: ULong) {
val x: Float
get() = Float.fromBits(unpackX(data))
val y: Float
get() = Float.fromBits(unpackY(data))
- constructor() : this(0f, 0f)
-
constructor(pt: PointF) : this(pt.x, pt.y)
constructor(x: Int, y: Int) : this(x.toFloat(), y.toFloat())
@@ -139,15 +137,13 @@ value class VPointF(private val data: ULong) {
}
@JvmInline
-value class VPoint(private val data: ULong) {
+value class VPoint(val data: ULong) {
val x: Int
get() = unpackX(data)
val y: Int
get() = unpackY(data)
- constructor() : this(0, 0)
-
constructor(x: Int, y: Int) : this(pack(x, y))
fun toPoint() = Point(x, y)
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VRect.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VRect.kt
new file mode 100644
index 000000000000..3c1adf22a405
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VRect.kt
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.plugins.clocks
+
+import android.graphics.Rect
+import android.graphics.RectF
+import android.util.Half
+
+private val LEFT_MASK: ULong = 0xFFFF000000000000U
+private val TOP_MASK: ULong = 0x0000FFFF00000000U
+private val RIGHT_MASK: ULong = 0x00000000FFFF0000U
+private val BOTTOM_MASK: ULong = 0x000000000000FFFFU
+
+private fun unpackLeft(data: ULong): Short = ((data and LEFT_MASK) shr 48).toShort()
+
+private fun unpackTop(data: ULong): Short = ((data and TOP_MASK) shr 32).toShort()
+
+private fun unpackRight(data: ULong): Short = ((data and RIGHT_MASK) shr 16).toShort()
+
+private fun unpackBottom(data: ULong): Short = ((data and BOTTOM_MASK) shr 0).toShort()
+
+private fun pack(left: Short, top: Short, right: Short, bottom: Short): ULong {
+ return ((left.toULong() shl 48) and LEFT_MASK) or
+ ((top.toULong() shl 32) and TOP_MASK) or
+ ((right.toULong() shl 16) and RIGHT_MASK) or
+ ((bottom.toULong() shl 0) and BOTTOM_MASK)
+}
+
+@JvmInline
+value class VRectF(val data: ULong) {
+ val left: Float
+ get() = fromBits(unpackLeft(data))
+
+ val top: Float
+ get() = fromBits(unpackTop(data))
+
+ val right: Float
+ get() = fromBits(unpackRight(data))
+
+ val bottom: Float
+ get() = fromBits(unpackBottom(data))
+
+ val width: Float
+ get() = right - left
+
+ val height: Float
+ get() = bottom - top
+
+ constructor(rect: RectF) : this(rect.left, rect.top, rect.right, rect.bottom)
+
+ constructor(
+ rect: Rect
+ ) : this(
+ left = rect.left.toFloat(),
+ top = rect.top.toFloat(),
+ right = rect.right.toFloat(),
+ bottom = rect.bottom.toFloat(),
+ )
+
+ constructor(
+ left: Float,
+ top: Float,
+ right: Float,
+ bottom: Float,
+ ) : this(pack(toBits(left), toBits(top), toBits(right), toBits(bottom)))
+
+ val center: VPointF
+ get() = VPointF(left, top) + size / 2f
+
+ val size: VPointF
+ get() = VPointF(width, height)
+
+ override fun toString() = "($left, $top) -> ($right, $bottom)"
+
+ companion object {
+ private fun toBits(value: Float): Short = Half.halfToShortBits(Half.toHalf(value))
+
+ private fun fromBits(value: Short): Float = Half.toFloat(Half.intBitsToHalf(value.toInt()))
+
+ fun fromCenter(center: VPointF, size: VPointF): VRectF {
+ return VRectF(
+ center.x - size.x / 2,
+ center.y - size.y / 2,
+ center.x + size.x / 2,
+ center.y + size.y / 2,
+ )
+ }
+
+ fun fromTopLeft(pos: VPointF, size: VPointF): VRectF {
+ return VRectF(pos.x, pos.y, pos.x + size.x, pos.y + size.y)
+ }
+
+ val ZERO = VRectF(0f, 0f, 0f, 0f)
+ }
+}
+
+@JvmInline
+value class VRect(val data: ULong) {
+ val left: Int
+ get() = unpackLeft(data).toInt()
+
+ val top: Int
+ get() = unpackTop(data).toInt()
+
+ val right: Int
+ get() = unpackRight(data).toInt()
+
+ val bottom: Int
+ get() = unpackBottom(data).toInt()
+
+ val width: Int
+ get() = right - left
+
+ val height: Int
+ get() = bottom - top
+
+ constructor(
+ rect: Rect
+ ) : this(
+ left = rect.left.toShort(),
+ top = rect.top.toShort(),
+ right = rect.right.toShort(),
+ bottom = rect.bottom.toShort(),
+ )
+
+ constructor(
+ left: Int,
+ top: Int,
+ right: Int,
+ bottom: Int,
+ ) : this(
+ left = left.toShort(),
+ top = top.toShort(),
+ right = right.toShort(),
+ bottom = bottom.toShort(),
+ )
+
+ constructor(
+ left: Short,
+ top: Short,
+ right: Short,
+ bottom: Short,
+ ) : this(pack(left, top, right, bottom))
+
+ val center: VPoint
+ get() = VPoint(left, top) + size / 2
+
+ val size: VPoint
+ get() = VPoint(width, height)
+
+ override fun toString() = "($left, $top) -> ($right, $bottom)"
+
+ companion object {
+ val ZERO = VRect(0, 0, 0, 0)
+
+ fun fromCenter(center: VPoint, size: VPoint): VRect {
+ return VRect(
+ (center.x - size.x / 2).toShort(),
+ (center.y - size.y / 2).toShort(),
+ (center.x + size.x / 2).toShort(),
+ (center.y + size.y / 2).toShort(),
+ )
+ }
+
+ fun fromTopLeft(pos: VPoint, size: VPoint): VRect {
+ return VRect(
+ pos.x.toShort(),
+ pos.y.toShort(),
+ (pos.x + size.x).toShort(),
+ (pos.y + size.y).toShort(),
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/WeatherData.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/WeatherData.kt
index f920b187e7e5..f59dda049aa1 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/WeatherData.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/WeatherData.kt
@@ -24,6 +24,8 @@ data class WeatherData(
@VisibleForTesting const val TEMPERATURE_KEY = "temperature"
private const val INVALID_WEATHER_ICON_STATE = -1
+ @JvmStatic
+ @JvmOverloads
fun fromBundle(extras: Bundle, touchAction: WeatherTouchAction? = null): WeatherData? {
val description = extras.getString(DESCRIPTION_KEY)
val state =
@@ -46,7 +48,7 @@ data class WeatherData(
state = state,
useCelsius = extras.getBoolean(USE_CELSIUS_KEY),
temperature = temperature,
- touchAction = touchAction
+ touchAction = touchAction,
)
if (DEBUG) {
Log.i(TAG, "Weather data parsed $result from $extras")
@@ -87,53 +89,53 @@ data class WeatherData(
}
// Values for WeatherStateIcon must stay in sync with go/g3-WeatherStateIcon
- enum class WeatherStateIcon(val id: Int) {
- UNKNOWN_ICON(0),
+ enum class WeatherStateIcon(val id: Int, val icon: String) {
+ UNKNOWN_ICON(0, ""),
// Clear, day & night.
- SUNNY(1),
- CLEAR_NIGHT(2),
+ SUNNY(1, "a"),
+ CLEAR_NIGHT(2, "f"),
// Mostly clear, day & night.
- MOSTLY_SUNNY(3),
- MOSTLY_CLEAR_NIGHT(4),
+ MOSTLY_SUNNY(3, "b"),
+ MOSTLY_CLEAR_NIGHT(4, "n"),
// Partly cloudy, day & night.
- PARTLY_CLOUDY(5),
- PARTLY_CLOUDY_NIGHT(6),
+ PARTLY_CLOUDY(5, "b"),
+ PARTLY_CLOUDY_NIGHT(6, "n"),
// Mostly cloudy, day & night.
- MOSTLY_CLOUDY_DAY(7),
- MOSTLY_CLOUDY_NIGHT(8),
- CLOUDY(9),
- HAZE_FOG_DUST_SMOKE(10),
- DRIZZLE(11),
- HEAVY_RAIN(12),
- SHOWERS_RAIN(13),
+ MOSTLY_CLOUDY_DAY(7, "e"),
+ MOSTLY_CLOUDY_NIGHT(8, "e"),
+ CLOUDY(9, "e"),
+ HAZE_FOG_DUST_SMOKE(10, "d"),
+ DRIZZLE(11, "c"),
+ HEAVY_RAIN(12, "c"),
+ SHOWERS_RAIN(13, "c"),
// Scattered showers, day & night.
- SCATTERED_SHOWERS_DAY(14),
- SCATTERED_SHOWERS_NIGHT(15),
+ SCATTERED_SHOWERS_DAY(14, "c"),
+ SCATTERED_SHOWERS_NIGHT(15, "c"),
// Isolated scattered thunderstorms, day & night.
- ISOLATED_SCATTERED_TSTORMS_DAY(16),
- ISOLATED_SCATTERED_TSTORMS_NIGHT(17),
- STRONG_TSTORMS(18),
- BLIZZARD(19),
- BLOWING_SNOW(20),
- FLURRIES(21),
- HEAVY_SNOW(22),
+ ISOLATED_SCATTERED_TSTORMS_DAY(16, "i"),
+ ISOLATED_SCATTERED_TSTORMS_NIGHT(17, "i"),
+ STRONG_TSTORMS(18, "i"),
+ BLIZZARD(19, "j"),
+ BLOWING_SNOW(20, "j"),
+ FLURRIES(21, "h"),
+ HEAVY_SNOW(22, "j"),
// Scattered snow showers, day & night.
- SCATTERED_SNOW_SHOWERS_DAY(23),
- SCATTERED_SNOW_SHOWERS_NIGHT(24),
- SNOW_SHOWERS_SNOW(25),
- MIXED_RAIN_HAIL_RAIN_SLEET(26),
- SLEET_HAIL(27),
- TORNADO(28),
- TROPICAL_STORM_HURRICANE(29),
- WINDY_BREEZY(30),
- WINTRY_MIX_RAIN_SNOW(31);
+ SCATTERED_SNOW_SHOWERS_DAY(23, "h"),
+ SCATTERED_SNOW_SHOWERS_NIGHT(24, "h"),
+ SNOW_SHOWERS_SNOW(25, "g"),
+ MIXED_RAIN_HAIL_RAIN_SLEET(26, "h"),
+ SLEET_HAIL(27, "h"),
+ TORNADO(28, "l"),
+ TROPICAL_STORM_HURRICANE(29, "m"),
+ WINDY_BREEZY(30, "k"),
+ WINTRY_MIX_RAIN_SNOW(31, "h");
companion object {
fun fromInt(value: Int) = values().firstOrNull { it.id == value }
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt
index be0362fd7481..ac7a85742385 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt
@@ -16,15 +16,13 @@
package com.android.systemui.plugins.qs
-/**
- * The base view model class for rendering the Tile's TileDetailsView.
- */
-abstract class TileDetailsViewModel {
+/** The view model interface for rendering the Tile's TileDetailsView. */
+interface TileDetailsViewModel {
// The callback when the settings button is clicked. Currently this is the same as the on tile
// long press callback
- abstract fun clickOnSettingsButton()
+ fun clickOnSettingsButton()
- abstract fun getTitle(): String
+ val title: String
- abstract fun getSubTitle(): String
+ val subTitle: String
}
diff --git a/packages/SystemUI/res/drawable/notification_2025_smart_reply_button_background.xml b/packages/SystemUI/res/drawable/notification_2025_smart_reply_button_background.xml
new file mode 100644
index 000000000000..d398f60ddc3c
--- /dev/null
+++ b/packages/SystemUI/res/drawable/notification_2025_smart_reply_button_background.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/notification_ripple_untinted_color">
+ <item>
+ <inset
+ android:insetLeft="0dp"
+ android:insetTop="8dp"
+ android:insetRight="0dp"
+ android:insetBottom="8dp">
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/notification_2025_smart_reply_button_corner_radius" />
+ <stroke android:width="@dimen/smart_reply_button_stroke_width"
+ android:color="@color/smart_reply_button_stroke" />
+ <solid android:color="@color/smart_reply_button_background"/>
+ </shape>
+ </inset>
+ </item>
+</ripple>
diff --git a/packages/SystemUI/res/layout/clipboard_overlay.xml b/packages/SystemUI/res/layout/clipboard_overlay.xml
index 915563b1ae20..c7add163dffa 100644
--- a/packages/SystemUI/res/layout/clipboard_overlay.xml
+++ b/packages/SystemUI/res/layout/clipboard_overlay.xml
@@ -85,6 +85,7 @@
android:paddingVertical="@dimen/overlay_action_container_padding_vertical"
android:elevation="4dp"
android:scrollbars="none"
+ android:importantForAccessibility="no"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintWidth_percent="1.0"
app:layout_constraintWidth_max="wrap"
@@ -176,6 +177,8 @@
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="4dp"
android:layout_marginBottom="2dp"
+ android:importantForAccessibility="yes"
+ android:contentDescription="@string/clipboard_overlay_window_name"
android:background="@drawable/clipboard_minimized_background_inset">
<ImageView
android:src="@drawable/ic_content_paste"
diff --git a/packages/SystemUI/res/layout/media_output_dialog.xml b/packages/SystemUI/res/layout/media_output_dialog.xml
index 6f8b4cd3e4a9..9b629ace76af 100644
--- a/packages/SystemUI/res/layout/media_output_dialog.xml
+++ b/packages/SystemUI/res/layout/media_output_dialog.xml
@@ -15,8 +15,8 @@
~ limitations under the License.
-->
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/media_output_dialog"
android:layout_width="@dimen/large_dialog_width"
android:layout_height="wrap_content"
@@ -35,24 +35,25 @@
android:orientation="horizontal">
<ImageView
android:id="@+id/header_icon"
- android:layout_width="72dp"
- android:layout_height="72dp"
+ android:layout_width="@dimen/media_output_dialog_header_album_icon_size"
+ android:layout_height="@dimen/media_output_dialog_header_album_icon_size"
+ android:layout_marginEnd="@dimen/media_output_dialog_header_icon_padding"
android:importantForAccessibility="no"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:paddingStart="12dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:gravity="center_vertical"
+ android:gravity="top"
android:orientation="horizontal">
<ImageView
android:id="@+id/app_source_icon"
android:layout_width="20dp"
android:layout_height="20dp"
+ android:layout_marginBottom="7dp"
android:gravity="center_vertical"
android:importantForAccessibility="no"/>
@@ -103,12 +104,11 @@
android:layout_height="wrap_content" >
</ViewStub>
- <LinearLayout
+ <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/device_list"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:orientation="vertical">
+ android:layout_height="0dp"
+ android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list_result"
@@ -116,8 +116,11 @@
android:paddingTop="8dp"
android:clipToPadding="false"
android:layout_width="match_parent"
- android:layout_height="wrap_content"/>
- </LinearLayout>
+ android:layout_height="wrap_content"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintHeight_max="@dimen/media_output_dialog_list_max_height"/>
+ </androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:layout_width="match_parent"
diff --git a/packages/SystemUI/res/layout/notif_half_shelf.xml b/packages/SystemUI/res/layout/notif_half_shelf.xml
index 9a66ca9f3baa..603cc00a312c 100644
--- a/packages/SystemUI/res/layout/notif_half_shelf.xml
+++ b/packages/SystemUI/res/layout/notif_half_shelf.xml
@@ -71,15 +71,18 @@
android:textSize="16sp"
/>
- <Switch
- android:id="@+id/toggle"
+ <com.google.android.material.materialswitch.MaterialSwitch
+ android:theme="@style/Theme.Material3.DynamicColors.DayNight"
+ android:id="@+id/material_toggle"
+ android:filterTouchesWhenObscured="false"
+ android:clickable="true"
+ android:focusable="true"
+ android:padding="8dp"
android:layout_height="48dp"
android:layout_width="wrap_content"
android:layout_gravity="center_vertical"
- android:padding="8dp"
- android:track="@drawable/settingslib_track_selector"
- android:thumb="@drawable/settingslib_thumb_selector"
- android:theme="@style/MainSwitch.Settingslib"/>
+ style="@style/SettingslibSwitchStyle.Expressive"/>
+
</com.android.systemui.statusbar.notification.row.AppControlView>
<ScrollView
diff --git a/packages/SystemUI/res/layout/notif_half_shelf_row.xml b/packages/SystemUI/res/layout/notif_half_shelf_row.xml
index b2eaa6ce92b5..4bc37f82a9d7 100644
--- a/packages/SystemUI/res/layout/notif_half_shelf_row.xml
+++ b/packages/SystemUI/res/layout/notif_half_shelf_row.xml
@@ -80,15 +80,16 @@
/>
</RelativeLayout>
- <Switch
- android:id="@+id/toggle"
+ <com.google.android.material.materialswitch.MaterialSwitch
+ android:theme="@style/Theme.Material3.DynamicColors.DayNight"
+ android:id="@+id/material_toggle"
+ android:filterTouchesWhenObscured="false"
+ android:clickable="true"
+ android:focusable="true"
+ android:padding="8dp"
android:layout_height="48dp"
android:layout_width="wrap_content"
android:layout_gravity="center_vertical"
- android:padding="8dp"
- android:track="@drawable/settingslib_track_selector"
- android:thumb="@drawable/settingslib_thumb_selector"
- android:theme="@style/MainSwitch.Settingslib"
- />
+ style="@style/SettingslibSwitchStyle.Expressive"/>
</LinearLayout>
</com.android.systemui.statusbar.notification.row.ChannelRow>
diff --git a/packages/SystemUI/res/layout/notification_2025_smart_action_button.xml b/packages/SystemUI/res/layout/notification_2025_smart_action_button.xml
new file mode 100644
index 000000000000..ed905885a76f
--- /dev/null
+++ b/packages/SystemUI/res/layout/notification_2025_smart_action_button.xml
@@ -0,0 +1,35 @@
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<!-- android:paddingHorizontal is set dynamically in SmartReplyView. -->
+<Button xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@android:style/Widget.Material.Button"
+ android:stateListAnimator="@null"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:minWidth="0dp"
+ android:minHeight="@dimen/notification_2025_smart_reply_button_min_height"
+ android:paddingVertical="@dimen/smart_reply_button_padding_vertical"
+ android:background="@drawable/notification_2025_smart_reply_button_background"
+ android:gravity="center"
+ android:fontFamily="google-sans-flex"
+ android:textSize="@dimen/smart_reply_button_font_size"
+ android:textColor="@color/smart_reply_button_text"
+ android:paddingStart="@dimen/smart_reply_button_action_padding_left"
+ android:paddingEnd="@dimen/smart_reply_button_padding_horizontal"
+ android:drawablePadding="@dimen/smart_action_button_icon_padding"
+ android:textStyle="normal"
+ android:ellipsize="none"/>
diff --git a/packages/SystemUI/res/layout/notification_2025_smart_reply_button.xml b/packages/SystemUI/res/layout/notification_2025_smart_reply_button.xml
new file mode 100644
index 000000000000..4f543e5099bf
--- /dev/null
+++ b/packages/SystemUI/res/layout/notification_2025_smart_reply_button.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<!-- android:paddingHorizontal is set dynamically in SmartReplyView. -->
+<Button xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@android:style/Widget.Material.Button"
+ android:stateListAnimator="@null"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:minWidth="0dp"
+ android:minHeight="@dimen/notification_2025_smart_reply_button_min_height"
+ android:paddingVertical="@dimen/smart_reply_button_padding_vertical"
+ android:background="@drawable/notification_2025_smart_reply_button_background"
+ android:gravity="center"
+ android:fontFamily="google-sans-flex"
+ android:textSize="@dimen/smart_reply_button_font_size"
+ android:textColor="@color/smart_reply_button_text"
+ android:paddingStart="@dimen/smart_reply_button_padding_horizontal"
+ android:paddingEnd="@dimen/smart_reply_button_padding_horizontal"
+ android:textStyle="normal"
+ android:ellipsize="none"/>
diff --git a/packages/SystemUI/res/raw/fingerprint_dialogue_unlocked_to_checkmark_success_lottie.json b/packages/SystemUI/res/raw/fingerprint_dialogue_unlocked_to_checkmark_success_lottie.json
index b1d6a270bc67..3f03fcff7603 100644
--- a/packages/SystemUI/res/raw/fingerprint_dialogue_unlocked_to_checkmark_success_lottie.json
+++ b/packages/SystemUI/res/raw/fingerprint_dialogue_unlocked_to_checkmark_success_lottie.json
@@ -1 +1 @@
-{"v":"5.7.13","fr":60,"ip":0,"op":55,"w":80,"h":80,"nm":"unlocked_to_checkmark_success","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".colorAccentPrimary","cl":"colorAccentPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[47.143,32,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[2.761,0],[0,-2.7],[0,0]],"o":[[0,0],[0,-2.7],[-2.761,0],[0,0],[0,0]],"v":[[5,5],[5,-0.111],[0,-5],[-5,-0.111],[-5.01,4]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":4},"w":{"a":0,"k":2.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".colorAccentPrimary","cl":"colorAccentPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[38,45,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[18,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":2,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":4},"w":{"a":0,"k":2.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".colorAccentPrimary","cl":"colorAccentPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[37.999,44.999,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.42,0],[0,1.42],[1.42,0],[0,-1.42]],"o":[[1.42,0],[0,-1.42],[-1.42,0],[0,1.42]],"v":[[0,2.571],[2.571,0],[0,-2.571],[-2.571,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".green200","cl":"green200","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40.5,40.75,0],"ix":2,"l":2},"a":{"a":0,"k":[12.5,-6.25,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[60,60,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":19,"s":[112,112,100]},{"t":30,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-10.556,-9.889],[7.444,6.555],[34.597,-20.486]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.658823529412,0.854901960784,0.709803921569,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":10,"s":[0]},{"t":20,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":10,"op":910,"st":10,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".green200","cl":"green200","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":15,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[40.25,40.25,0],"ix":1,"l":2},"s":{"a":0,"k":[93.5,93.5,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.12,0],[0,-22.08],[-22.08,0],[0,22.08]],"o":[[-22.08,0],[0,22.08],[22.12,0],[0,-22.08]],"v":[[-0.04,-40],[-40,0],[-0.04,40],[40,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[40.25,40.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.658823529412,0.854901960784,0.709803921569,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":15,"s":[100]}],"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[0]},{"t":20,"s":[4]}],"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":10,"op":77,"st":10,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".grey700","cl":"grey700","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[40.25,40.25,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.12,0],[0,-22.08],[-22.08,0],[0,22.08]],"o":[[-22.08,0],[0,22.08],[22.12,0],[0,-22.08]],"v":[[-0.04,-40],[-40,0],[-0.04,40],[40,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.278431385756,0.278431385756,0.278431385756,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"t":20,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[40.25,40.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}],"markers":[]} \ No newline at end of file
+{"v":"5.7.13","fr":60,"ip":0,"op":55,"w":80,"h":80,"nm":"unlocked_to_checkmark_success","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".onPrimary","cl":"onPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[47.143,32,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[2.761,0],[0,-2.7],[0,0]],"o":[[0,0],[0,-2.7],[-2.761,0],[0,0],[0,0]],"v":[[5,5],[5,-0.111],[0,-5],[-5,-0.111],[-5.01,4]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":4},"w":{"a":0,"k":2.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".onPrimary","cl":"onPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[38,45,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[18,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":2,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":4},"w":{"a":0,"k":2.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".onPrimary","cl":"onPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[37.999,44.999,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.42,0],[0,1.42],[1.42,0],[0,-1.42]],"o":[[1.42,0],[0,-1.42],[-1.42,0],[0,1.42]],"v":[[0,2.571],[2.571,0],[0,-2.571],[-2.571,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".green200","cl":"green200","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40.5,40.75,0],"ix":2,"l":2},"a":{"a":0,"k":[12.5,-6.25,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[60,60,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":19,"s":[112,112,100]},{"t":30,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-10.556,-9.889],[7.444,6.555],[34.597,-20.486]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.658823529412,0.854901960784,0.709803921569,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":10,"s":[0]},{"t":20,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":10,"op":910,"st":10,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".green200","cl":"green200","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":15,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[40.25,40.25,0],"ix":1,"l":2},"s":{"a":0,"k":[93.5,93.5,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.12,0],[0,-22.08],[-22.08,0],[0,22.08]],"o":[[-22.08,0],[0,22.08],[22.12,0],[0,-22.08]],"v":[[-0.04,-40],[-40,0],[-0.04,40],[40,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[40.25,40.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.658823529412,0.854901960784,0.709803921569,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":15,"s":[100]}],"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[0]},{"t":20,"s":[4]}],"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":10,"op":77,"st":10,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".primary","cl":"primary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[40.25,40.25,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.12,0],[0,-22.08],[-22.08,0],[0,22.08]],"o":[[-22.08,0],[0,22.08],[22.12,0],[0,-22.08]],"v":[[-0.04,-40],[-40,0],[-0.04,40],[40,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.278431385756,0.278431385756,0.278431385756,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"t":20,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[40.25,40.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}],"markers":[]} \ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 8342a9cc244b..d0ae307b6919 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -283,7 +283,7 @@
<dimen name="notification_max_height">358dp</dimen>
<!-- Height of a large promoted ongoing notification in the status bar -->
- <dimen name="notification_max_height_for_promoted_ongoing">272dp</dimen>
+ <dimen name="notification_max_height_for_promoted_ongoing">218dp</dimen>
<!-- Height of a heads up notification in the status bar for legacy custom views -->
<dimen name="notification_max_heads_up_height_legacy">128dp</dimen>
@@ -1157,6 +1157,8 @@
<dimen name="smart_action_button_icon_size">18dp</dimen>
<dimen name="smart_action_button_icon_padding">8dp</dimen>
<dimen name="smart_action_button_outline_stroke_width">2dp</dimen>
+ <dimen name="notification_2025_smart_reply_button_corner_radius">18dp</dimen>
+ <dimen name="notification_2025_smart_reply_button_min_height">48dp</dimen>
<!-- Magic Action params. -->
<!-- Corner radius = half of min_height to create rounded sides. -->
@@ -2166,7 +2168,7 @@
<dimen name="volume_dialog_background_top_margin">-28dp</dimen>
<dimen name="volume_dialog_window_margin">14dp</dimen>
- <dimen name="volume_dialog_components_spacing">8dp</dimen>
+ <dimen name="volume_dialog_components_spacing">10dp</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_vertical_padding_negative">
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index c06c17a0844f..8c1fd65d96d4 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2357,8 +2357,8 @@
<string name="system_multitasking_lhs">Use split screen with app on the left</string>
<!-- User visible title for the keyboard shortcut that switches from split screen to full screen [CHAR LIMIT=70] -->
<string name="system_multitasking_full_screen">Use full screen</string>
- <!-- User visible title for the keyboard shortcut that switches to desktop view [CHAR LIMIT=70] -->
- <string name="system_multitasking_desktop_view">Use desktop view</string>
+ <!-- User visible title for the keyboard shortcut that switches to desktop windowing [CHAR LIMIT=70] -->
+ <string name="system_multitasking_desktop_view">Use desktop windowing</string>
<!-- User visible title for the keyboard shortcut that switches to app on right or below while using split screen [CHAR LIMIT=70] -->
<string name="system_multitasking_splitscreen_focus_rhs">Switch to app on right or below while using split screen</string>
<!-- User visible title for the keyboard shortcut that switches to app on left or above while using split screen [CHAR LIMIT=70] -->
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 1549b699eee6..763b1072f968 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -21,7 +21,6 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.res.Resources
-import android.graphics.RectF
import android.os.Trace
import android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
import android.provider.Settings.Global.ZEN_MODE_OFF
@@ -60,6 +59,7 @@ import com.android.systemui.plugins.clocks.ClockEventListener
import com.android.systemui.plugins.clocks.ClockFaceController
import com.android.systemui.plugins.clocks.ClockMessageBuffers
import com.android.systemui.plugins.clocks.ClockTickRate
+import com.android.systemui.plugins.clocks.VRectF
import com.android.systemui.plugins.clocks.WeatherData
import com.android.systemui.plugins.clocks.ZenData
import com.android.systemui.plugins.clocks.ZenData.ZenMode
@@ -250,7 +250,7 @@ constructor(
private var largeClockOnSecondaryDisplay = false
val dozeAmount = MutableStateFlow(0f)
- val onClockBoundsChanged = MutableStateFlow<RectF?>(null)
+ val onClockBoundsChanged = MutableStateFlow<VRectF>(VRectF.ZERO)
private fun isDarkTheme(): Boolean {
val isLightTheme = TypedValue()
@@ -315,7 +315,7 @@ constructor(
private val clockListener =
object : ClockEventListener {
- override fun onBoundsChanged(bounds: RectF) {
+ override fun onBoundsChanged(bounds: VRectF) {
onClockBoundsChanged.value = bounds
}
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
index ec97b8a96c1f..b8726101602c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
@@ -21,6 +21,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.res.ColorStateList;
import android.content.res.Resources;
+import android.hardware.input.InputManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
@@ -219,6 +220,7 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView>
private final KeyguardKeyboardInteractor mKeyguardKeyboardInteractor;
private final BouncerHapticPlayer mBouncerHapticPlayer;
private final UserActivityNotifier mUserActivityNotifier;
+ private final InputManager mInputManager;
@Inject
public Factory(KeyguardUpdateMonitor keyguardUpdateMonitor,
@@ -235,7 +237,8 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView>
UiEventLogger uiEventLogger,
KeyguardKeyboardInteractor keyguardKeyboardInteractor,
BouncerHapticPlayer bouncerHapticPlayer,
- UserActivityNotifier userActivityNotifier) {
+ UserActivityNotifier userActivityNotifier,
+ InputManager inputManager) {
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mLockPatternUtils = lockPatternUtils;
mLatencyTracker = latencyTracker;
@@ -254,6 +257,7 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView>
mKeyguardKeyboardInteractor = keyguardKeyboardInteractor;
mBouncerHapticPlayer = bouncerHapticPlayer;
mUserActivityNotifier = userActivityNotifier;
+ mInputManager = inputManager;
}
/** Create a new {@link KeyguardInputViewController}. */
@@ -285,22 +289,23 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView>
emergencyButtonController, mFalsingCollector,
mDevicePostureController, mFeatureFlags, mSelectedUserInteractor,
mUiEventLogger, mKeyguardKeyboardInteractor, mBouncerHapticPlayer,
- mUserActivityNotifier);
+ mUserActivityNotifier, mInputManager);
} else if (keyguardInputView instanceof KeyguardSimPinView) {
return new KeyguardSimPinViewController((KeyguardSimPinView) keyguardInputView,
mKeyguardUpdateMonitor, securityMode, mLockPatternUtils,
keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker,
mTelephonyManager, mFalsingCollector,
emergencyButtonController, mFeatureFlags, mSelectedUserInteractor,
- mKeyguardKeyboardInteractor, mBouncerHapticPlayer, mUserActivityNotifier);
+ mKeyguardKeyboardInteractor, mBouncerHapticPlayer, mUserActivityNotifier,
+ mInputManager);
} else if (keyguardInputView instanceof KeyguardSimPukView) {
return new KeyguardSimPukViewController((KeyguardSimPukView) keyguardInputView,
mKeyguardUpdateMonitor, securityMode, mLockPatternUtils,
keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker,
mTelephonyManager, mFalsingCollector,
emergencyButtonController, mFeatureFlags, mSelectedUserInteractor,
- mKeyguardKeyboardInteractor, mBouncerHapticPlayer, mUserActivityNotifier
- );
+ mKeyguardKeyboardInteractor, mBouncerHapticPlayer, mUserActivityNotifier,
+ mInputManager);
}
throw new RuntimeException("Unable to find controller for " + keyguardInputView);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
index 0e9d8fec9717..ec9aedfc7551 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
@@ -18,12 +18,14 @@ package com.android.keyguard;
import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED;
+import static com.android.internal.widget.flags.Flags.hideLastCharWithPhysicalInput;
import static com.android.systemui.Flags.pinInputFieldStyledFocusState;
import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.StateListDrawable;
+import android.hardware.input.InputManager;
import android.util.TypedValue;
import android.view.KeyEvent;
import android.view.MotionEvent;
@@ -43,11 +45,13 @@ import com.android.systemui.res.R;
import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinBasedInputView>
- extends KeyguardAbsKeyInputViewController<T> {
+ extends KeyguardAbsKeyInputViewController<T> implements InputManager.InputDeviceListener {
private final FalsingCollector mFalsingCollector;
private final KeyguardKeyboardInteractor mKeyguardKeyboardInteractor;
protected PasswordTextView mPasswordEntry;
+ private Boolean mShowAnimations;
+ private InputManager mInputManager;
private final OnKeyListener mOnKeyListener = (v, keyCode, event) -> {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
@@ -79,7 +83,8 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB
SelectedUserInteractor selectedUserInteractor,
KeyguardKeyboardInteractor keyguardKeyboardInteractor,
BouncerHapticPlayer bouncerHapticPlayer,
- UserActivityNotifier userActivityNotifier) {
+ UserActivityNotifier userActivityNotifier,
+ InputManager inputManager) {
super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback,
messageAreaControllerFactory, latencyTracker, falsingCollector,
emergencyButtonController, featureFlags, selectedUserInteractor,
@@ -87,6 +92,51 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB
mFalsingCollector = falsingCollector;
mKeyguardKeyboardInteractor = keyguardKeyboardInteractor;
mPasswordEntry = mView.findViewById(mView.getPasswordTextViewId());
+ mInputManager = inputManager;
+ mShowAnimations = null;
+ }
+
+ private void updateAnimations(Boolean showAnimations) {
+ if (!hideLastCharWithPhysicalInput()) return;
+
+ if (showAnimations == null) {
+ showAnimations = !mLockPatternUtils
+ .isPinEnhancedPrivacyEnabled(mSelectedUserInteractor.getSelectedUserId());
+ }
+ if (mShowAnimations != null && showAnimations.equals(mShowAnimations)) return;
+ mShowAnimations = showAnimations;
+
+ for (NumPadKey button : mView.getButtons()) {
+ button.setAnimationEnabled(mShowAnimations);
+ }
+ mPasswordEntry.setShowPassword(mShowAnimations);
+ }
+
+ @Override
+ public void onInputDeviceAdded(int deviceId) {
+ if (!hideLastCharWithPhysicalInput()) return;
+
+ // If we were showing animations before maybe the new device is a keyboard.
+ if (mShowAnimations) {
+ updateAnimations(null);
+ }
+ }
+
+ @Override
+ public void onInputDeviceRemoved(int deviceId) {
+ if (!hideLastCharWithPhysicalInput()) return;
+
+ // If we were hiding animations because of a keyboard the keyboard may have been unplugged.
+ if (!mShowAnimations) {
+ updateAnimations(null);
+ }
+ }
+
+ @Override
+ public void onInputDeviceChanged(int deviceId) {
+ if (!hideLastCharWithPhysicalInput()) return;
+
+ updateAnimations(null);
}
@Override
@@ -95,7 +145,13 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB
boolean showAnimations = !mLockPatternUtils
.isPinEnhancedPrivacyEnabled(mSelectedUserInteractor.getSelectedUserId());
- mPasswordEntry.setShowPassword(showAnimations);
+ if (hideLastCharWithPhysicalInput()) {
+ mInputManager.registerInputDeviceListener(this, null);
+ updateAnimations(showAnimations);
+ } else {
+ mPasswordEntry.setShowPassword(showAnimations);
+ }
+
for (NumPadKey button : mView.getButtons()) {
button.setOnTouchListener((v, event) -> {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
@@ -103,7 +159,9 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB
}
return false;
});
- button.setAnimationEnabled(showAnimations);
+ if (!hideLastCharWithPhysicalInput()) {
+ button.setAnimationEnabled(showAnimations);
+ }
button.setBouncerHapticHelper(mBouncerHapticPlayer);
}
mPasswordEntry.setOnKeyListener(mOnKeyListener);
@@ -191,6 +249,10 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB
protected void onViewDetached() {
super.onViewDetached();
+ if (hideLastCharWithPhysicalInput()) {
+ mInputManager.unregisterInputDeviceListener(this);
+ }
+
for (NumPadKey button : mView.getButtons()) {
button.setOnTouchListener(null);
button.setBouncerHapticHelper(null);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
index 9ae4cc6a4b4f..eefcab38ecd3 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
@@ -18,6 +18,7 @@ package com.android.keyguard;
import static com.android.systemui.flags.Flags.LOCKSCREEN_ENABLE_LANDSCAPE;
+import android.hardware.input.InputManager;
import android.view.View;
import com.android.internal.logging.UiEvent;
@@ -63,11 +64,13 @@ public class KeyguardPinViewController
SelectedUserInteractor selectedUserInteractor, UiEventLogger uiEventLogger,
KeyguardKeyboardInteractor keyguardKeyboardInteractor,
BouncerHapticPlayer bouncerHapticPlayer,
- UserActivityNotifier userActivityNotifier) {
+ UserActivityNotifier userActivityNotifier,
+ InputManager inputManager) {
super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback,
messageAreaControllerFactory, latencyTracker,
emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor,
- keyguardKeyboardInteractor, bouncerHapticPlayer, userActivityNotifier);
+ keyguardKeyboardInteractor, bouncerHapticPlayer, userActivityNotifier, inputManager
+ );
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mPostureController = postureController;
mLockPatternUtils = lockPatternUtils;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java
index 24f77d77dbe1..a5bb62c04d00 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java
@@ -29,6 +29,7 @@ import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Color;
+import android.hardware.input.InputManager;
import android.telephony.PinResult;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
@@ -97,11 +98,13 @@ public class KeyguardSimPinViewController
SelectedUserInteractor selectedUserInteractor,
KeyguardKeyboardInteractor keyguardKeyboardInteractor,
BouncerHapticPlayer bouncerHapticPlayer,
- UserActivityNotifier userActivityNotifier) {
+ UserActivityNotifier userActivityNotifier,
+ InputManager inputManager) {
super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback,
messageAreaControllerFactory, latencyTracker,
emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor,
- keyguardKeyboardInteractor, bouncerHapticPlayer, userActivityNotifier);
+ keyguardKeyboardInteractor, bouncerHapticPlayer, userActivityNotifier, inputManager
+ );
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mTelephonyManager = telephonyManager;
mSimImageView = mView.findViewById(R.id.keyguard_sim);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java
index e17e8cc05f7e..adede3dc058d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java
@@ -25,6 +25,7 @@ import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Color;
+import android.hardware.input.InputManager;
import android.telephony.PinResult;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
@@ -95,11 +96,13 @@ public class KeyguardSimPukViewController
SelectedUserInteractor selectedUserInteractor,
KeyguardKeyboardInteractor keyguardKeyboardInteractor,
BouncerHapticPlayer bouncerHapticPlayer,
- UserActivityNotifier userActivityNotifier) {
+ UserActivityNotifier userActivityNotifier,
+ InputManager inputManager) {
super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback,
messageAreaControllerFactory, latencyTracker,
emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor,
- keyguardKeyboardInteractor, bouncerHapticPlayer, userActivityNotifier);
+ keyguardKeyboardInteractor, bouncerHapticPlayer, userActivityNotifier, inputManager
+ );
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mTelephonyManager = telephonyManager;
mSimImageView = mView.findViewById(R.id.keyguard_sim);
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index 4a8e4ed3f6f1..f72087efc03e 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -32,6 +32,7 @@ import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.fragments.FragmentService;
+import com.android.systemui.media.NotificationMediaManager;
import com.android.systemui.model.SysUiState;
import com.android.systemui.navigationbar.NavigationBarController;
import com.android.systemui.navigationbar.NavigationModeController;
@@ -42,7 +43,6 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.recents.LauncherProxyService;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
import com.android.systemui.statusbar.notification.stack.AmbientState;
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
index 08559f2eca8d..fb3bc620ee68 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
@@ -63,7 +63,7 @@ import com.android.systemui.accessibility.hearingaid.HearingDevicesListAdapter.H
import com.android.systemui.animation.DialogTransitionAnimator;
import com.android.systemui.bluetooth.qsdialog.ActiveHearingDeviceItemFactory;
import com.android.systemui.bluetooth.qsdialog.AvailableHearingDeviceItemFactory;
-import com.android.systemui.bluetooth.qsdialog.ConnectedDeviceItemFactory;
+import com.android.systemui.bluetooth.qsdialog.ConnectedHearingDeviceItemFactory;
import com.android.systemui.bluetooth.qsdialog.DeviceItem;
import com.android.systemui.bluetooth.qsdialog.DeviceItemFactory;
import com.android.systemui.bluetooth.qsdialog.DeviceItemType;
@@ -71,6 +71,7 @@ import com.android.systemui.bluetooth.qsdialog.SavedHearingDeviceItemFactory;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.qs.shared.QSSettingsPackageRepository;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.phone.SystemUIDialog;
@@ -111,6 +112,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
private final HearingDevicesUiEventLogger mUiEventLogger;
private final boolean mShowPairNewDevice;
private final int mLaunchSourceId;
+ private final QSSettingsPackageRepository mQSSettingsPackageRepository;
private SystemUIDialog mDialog;
private HearingDevicesListAdapter mDeviceListAdapter;
@@ -142,12 +144,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
private final List<DeviceItemFactory> mHearingDeviceItemFactoryList = List.of(
new ActiveHearingDeviceItemFactory(),
new AvailableHearingDeviceItemFactory(),
- // TODO(b/331305850): setHearingAidInfo() for connected but not connect to profile
- // hearing device only called from
- // settings/bluetooth/DeviceListPreferenceFragment#handleLeScanResult, so we don't know
- // it is connected but not yet connect to profile hearing device in systemui.
- // Show all connected but not connect to profile bluetooth device for now.
- new ConnectedDeviceItemFactory(),
+ new ConnectedHearingDeviceItemFactory(),
new SavedHearingDeviceItemFactory()
);
@@ -171,7 +168,8 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
@Main Executor mainExecutor,
@Background Executor bgExecutor,
AudioManager audioManager,
- HearingDevicesUiEventLogger uiEventLogger) {
+ HearingDevicesUiEventLogger uiEventLogger,
+ QSSettingsPackageRepository qsSettingsPackageRepository) {
mShowPairNewDevice = showPairNewDevice;
mSystemUIDialogFactory = systemUIDialogFactory;
mActivityStarter = activityStarter;
@@ -183,6 +181,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
mProfileManager = localBluetoothManager.getProfileManager();
mUiEventLogger = uiEventLogger;
mLaunchSourceId = launchSourceId;
+ mQSSettingsPackageRepository = qsSettingsPackageRepository;
}
@Override
@@ -198,11 +197,11 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
public void onDeviceItemGearClicked(@NonNull DeviceItem deviceItem, @NonNull View view) {
mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_GEAR_CLICK, mLaunchSourceId);
dismissDialogIfExists();
- Intent intent = new Intent(ACTION_BLUETOOTH_DEVICE_DETAILS);
Bundle bundle = new Bundle();
bundle.putString(KEY_BLUETOOTH_ADDRESS, deviceItem.getCachedBluetoothDevice().getAddress());
- intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, bundle);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ Intent intent = new Intent(ACTION_BLUETOOTH_DEVICE_DETAILS)
+ .setPackage(mQSSettingsPackageRepository.getSettingsPackageName())
+ .putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, bundle);
mActivityStarter.postStartActivityDismissingKeyguard(intent, /* delay= */ 0,
mDialogTransitionAnimator.createActivityTransitionController(view));
}
@@ -401,8 +400,8 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
pairButton.setOnClickListener(v -> {
mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_PAIR, mLaunchSourceId);
dismissDialogIfExists();
- final Intent intent = new Intent(Settings.ACTION_HEARING_DEVICE_PAIRING_SETTINGS);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ final Intent intent = new Intent(Settings.ACTION_HEARING_DEVICE_PAIRING_SETTINGS)
+ .setPackage(mQSSettingsPackageRepository.getSettingsPackageName());
mActivityStarter.postStartActivityDismissingKeyguard(intent, /* delay= */ 0,
mDialogTransitionAnimator.createActivityTransitionController(dialog));
});
@@ -523,8 +522,9 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
com.android.internal.R.color.materialColorOnPrimaryContainer));
}
text.setText(item.getToolName());
- Intent intent = item.getToolIntent();
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ Intent intent = item.getToolIntent()
+ .setPackage(mQSSettingsPackageRepository.getSettingsPackageName());
+
view.setOnClickListener(v -> {
final String name = intent.getComponent() != null
? intent.getComponent().flattenToString()
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt
index 5863a9385234..7d8752ef7222 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt
@@ -21,18 +21,14 @@ import com.android.systemui.plugins.qs.TileDetailsViewModel
class BluetoothDetailsViewModel(
private val onSettingsClick: () -> Unit,
val detailsContentViewModel: BluetoothDetailsContentViewModel,
-) : TileDetailsViewModel() {
+) : TileDetailsViewModel {
override fun clickOnSettingsButton() {
onSettingsClick()
}
- override fun getTitle(): String {
- // TODO: b/378513956 Update the placeholder text
- return "Bluetooth"
- }
+ // TODO: b/378513956 Update the placeholder text
+ override val title = "Bluetooth"
- override fun getSubTitle(): String {
- // TODO: b/378513956 Update the placeholder text
- return "Tap to connect or disconnect a device"
- }
+ // TODO: b/378513956 Update the placeholder text
+ override val subTitle = "Tap to connect or disconnect a device"
}
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 858cc00b86b9..208e498126c1 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
@@ -214,7 +214,7 @@ internal class AvailableHearingDeviceItemFactory : AvailableMediaDeviceItemFacto
}
}
-internal class ConnectedDeviceItemFactory : DeviceItemFactory() {
+internal open class ConnectedDeviceItemFactory : DeviceItemFactory() {
override fun isFilterMatched(
context: Context,
cachedDevice: CachedBluetoothDevice,
@@ -238,6 +238,19 @@ internal class ConnectedDeviceItemFactory : DeviceItemFactory() {
}
}
+internal class ConnectedHearingDeviceItemFactory : ConnectedDeviceItemFactory() {
+ override fun isFilterMatched(
+ context: Context,
+ cachedDevice: CachedBluetoothDevice,
+ isOngoingCall: Boolean,
+ audioSharingAvailable: Boolean,
+ ): Boolean {
+ return cachedDevice.isHearingDevice &&
+ cachedDevice.bondState == BluetoothDevice.BOND_BONDED &&
+ cachedDevice.device.isConnected
+ }
+}
+
internal open class SavedDeviceItemFactory : DeviceItemFactory() {
override fun isFilterMatched(
context: Context,
@@ -274,7 +287,7 @@ internal class SavedHearingDeviceItemFactory : SavedDeviceItemFactory() {
context,
cachedDevice.getDevice(),
) &&
- cachedDevice.isHearingAidDevice &&
+ cachedDevice.isHearingDevice &&
cachedDevice.bondState == BluetoothDevice.BOND_BONDED &&
!cachedDevice.isConnected
}
diff --git a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt
index 52204b84346d..6aeb35b3b158 100644
--- a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt
+++ b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt
@@ -19,6 +19,7 @@ package com.android.systemui.brightness.ui.compose
import android.content.Context
import android.view.MotionEvent
import androidx.annotation.VisibleForTesting
+import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.VectorConverter
@@ -40,6 +41,7 @@ import androidx.compose.material3.SliderDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
@@ -323,7 +325,7 @@ private fun Modifier.sliderBackground(color: Color) = drawWithCache {
fun BrightnessSliderContainer(
viewModel: BrightnessSliderViewModel,
modifier: Modifier = Modifier,
- containerColor: Color = colorResource(R.color.shade_scrim_background_dark),
+ containerColors: ContainerColors,
) {
val gamma = viewModel.currentBrightness.value
if (gamma == BrightnessSliderViewModel.initialValue.value) { // Ignore initial negative value.
@@ -344,6 +346,16 @@ fun BrightnessSliderContainer(
DisposableEffect(Unit) { onDispose { viewModel.setIsDragging(false) } }
+ var dragging by remember { mutableStateOf(false) }
+
+ // Use dragging instead of viewModel.showMirror so the color starts changing as soon as the
+ // dragging state changes. If not, we may be waiting for the background to finish fading in
+ // when stopping dragging
+ val containerColor by
+ animateColorAsState(
+ if (dragging) containerColors.mirrorColor else containerColors.idleColor
+ )
+
Box(
modifier =
modifier
@@ -360,10 +372,12 @@ fun BrightnessSliderContainer(
onRestrictedClick = viewModel::showPolicyRestrictionDialog,
onDrag = {
viewModel.setIsDragging(true)
+ dragging = true
coroutineScope.launch { viewModel.onDrag(Drag.Dragging(GammaBrightness(it))) }
},
onStop = {
viewModel.setIsDragging(false)
+ dragging = false
coroutineScope.launch { viewModel.onDrag(Drag.Stopped(GammaBrightness(it))) }
},
modifier =
@@ -392,6 +406,15 @@ fun BrightnessSliderContainer(
}
}
+data class ContainerColors(val idleColor: Color, val mirrorColor: Color) {
+ companion object {
+ fun singleColor(color: Color) = ContainerColors(color, color)
+
+ val defaultContainerColor: Color
+ @Composable @ReadOnlyComposable get() = colorResource(R.color.shade_panel_fallback)
+ }
+}
+
private object Dimensions {
val SliderBackgroundFrameSize = DpSize(10.dp, 6.dp)
val SliderBackgroundRoundedCorner = 24.dp
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingView.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingView.kt
index 42f1b738ec20..6c3535a42a6e 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingView.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingView.kt
@@ -27,17 +27,17 @@ import android.view.ViewConfiguration
import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
+import com.android.systemui.Flags.doubleTapToSleep
import com.android.systemui.log.TouchHandlingViewLogger
import com.android.systemui.shade.TouchLogger
-import kotlin.math.pow
-import kotlin.math.sqrt
import kotlinx.coroutines.DisposableHandle
/**
- * View designed to handle long-presses.
+ * View designed to handle long-presses and double taps.
*
- * The view will not handle any long pressed by default. To set it up, set up a listener and, when
- * ready to start consuming long-presses, set [setLongPressHandlingEnabled] to `true`.
+ * The view will not handle any gestures by default. To set it up, set up a listener and, when ready
+ * to start consuming gestures, set the gesture's enable function ([setLongPressHandlingEnabled],
+ * [setDoublePressHandlingEnabled]) to `true`.
*/
class TouchHandlingView(
context: Context,
@@ -62,6 +62,9 @@ class TouchHandlingView(
/** Notifies that the gesture was too short for a long press, it is actually a click. */
fun onSingleTapDetected(view: View, x: Int, y: Int) = Unit
+
+ /** Notifies that a double tap has been detected by the given view. */
+ fun onDoubleTapDetected(view: View) = Unit
}
var listener: Listener? = null
@@ -70,6 +73,7 @@ class TouchHandlingView(
private val interactionHandler: TouchHandlingViewInteractionHandler by lazy {
TouchHandlingViewInteractionHandler(
+ context = context,
postDelayed = { block, timeoutMs ->
val dispatchToken = Any()
@@ -84,6 +88,9 @@ class TouchHandlingView(
onSingleTapDetected = { x, y ->
listener?.onSingleTapDetected(this@TouchHandlingView, x = x, y = y)
},
+ onDoubleTapDetected = {
+ if (doubleTapToSleep()) listener?.onDoubleTapDetected(this@TouchHandlingView)
+ },
longPressDuration = longPressDuration,
allowedTouchSlop = allowedTouchSlop,
logger = logger,
@@ -100,13 +107,17 @@ class TouchHandlingView(
interactionHandler.isLongPressHandlingEnabled = isEnabled
}
+ fun setDoublePressHandlingEnabled(isEnabled: Boolean) {
+ interactionHandler.isDoubleTapHandlingEnabled = isEnabled
+ }
+
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
return TouchLogger.logDispatchTouch("long_press", event, super.dispatchTouchEvent(event))
}
@SuppressLint("ClickableViewAccessibility")
- override fun onTouchEvent(event: MotionEvent?): Boolean {
- return interactionHandler.onTouchEvent(event?.toModel())
+ override fun onTouchEvent(event: MotionEvent): Boolean {
+ return interactionHandler.onTouchEvent(event)
}
private fun setupAccessibilityDelegate() {
@@ -154,33 +165,3 @@ class TouchHandlingView(
}
}
}
-
-private fun MotionEvent.toModel(): TouchHandlingViewInteractionHandler.MotionEventModel {
- return when (actionMasked) {
- MotionEvent.ACTION_DOWN ->
- TouchHandlingViewInteractionHandler.MotionEventModel.Down(x = x.toInt(), y = y.toInt())
- MotionEvent.ACTION_MOVE ->
- TouchHandlingViewInteractionHandler.MotionEventModel.Move(
- distanceMoved = distanceMoved()
- )
- MotionEvent.ACTION_UP ->
- TouchHandlingViewInteractionHandler.MotionEventModel.Up(
- distanceMoved = distanceMoved(),
- gestureDuration = gestureDuration(),
- )
- MotionEvent.ACTION_CANCEL -> TouchHandlingViewInteractionHandler.MotionEventModel.Cancel
- else -> TouchHandlingViewInteractionHandler.MotionEventModel.Other
- }
-}
-
-private fun MotionEvent.distanceMoved(): Float {
- return if (historySize > 0) {
- sqrt((x - getHistoricalX(0)).pow(2) + (y - getHistoricalY(0)).pow(2))
- } else {
- 0f
- }
-}
-
-private fun MotionEvent.gestureDuration(): Long {
- return eventTime - downTime
-}
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandler.kt
index 5863fc644c8e..fe509d74edc0 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandler.kt
@@ -17,12 +17,20 @@
package com.android.systemui.common.ui.view
+import android.content.Context
import android.graphics.Point
+import android.view.GestureDetector
+import android.view.MotionEvent
+import android.view.ViewConfiguration
import com.android.systemui.log.TouchHandlingViewLogger
+import kotlin.math.pow
+import kotlin.math.sqrt
+import kotlin.properties.Delegates
import kotlinx.coroutines.DisposableHandle
/** Encapsulates logic to handle complex touch interactions with a [TouchHandlingView]. */
class TouchHandlingViewInteractionHandler(
+ context: Context,
/**
* Callback to run the given [Runnable] with the given delay, returning a [DisposableHandle]
* allowing the delayed runnable to be canceled before it is run.
@@ -34,6 +42,8 @@ class TouchHandlingViewInteractionHandler(
private val onLongPressDetected: (x: Int, y: Int) -> Unit,
/** Callback reporting the a single tap gesture was detected at the given coordinates. */
private val onSingleTapDetected: (x: Int, y: Int) -> Unit,
+ /** Callback reporting that a double tap gesture was detected. */
+ private val onDoubleTapDetected: () -> Unit,
/** Time for the touch to be considered a long-press in ms */
var longPressDuration: () -> Long,
/**
@@ -58,48 +68,98 @@ class TouchHandlingViewInteractionHandler(
}
var isLongPressHandlingEnabled: Boolean = false
+ var isDoubleTapHandlingEnabled: Boolean = false
var scheduledLongPressHandle: DisposableHandle? = null
+ private var doubleTapAwaitingUp: Boolean = false
+ private var lastDoubleTapDownEventTime: Long? = null
+
/** Record coordinate for last DOWN event for single tap */
val lastEventDownCoordinate = Point(-1, -1)
- fun onTouchEvent(event: MotionEventModel?): Boolean {
- if (!isLongPressHandlingEnabled) {
- return false
- }
- return when (event) {
- is MotionEventModel.Down -> {
- scheduleLongPress(event.x, event.y)
- lastEventDownCoordinate.x = event.x
- lastEventDownCoordinate.y = event.y
- true
+ private val gestureDetector =
+ GestureDetector(
+ context,
+ object : GestureDetector.SimpleOnGestureListener() {
+ override fun onDoubleTap(event: MotionEvent): Boolean {
+ if (isDoubleTapHandlingEnabled) {
+ doubleTapAwaitingUp = true
+ lastDoubleTapDownEventTime = event.eventTime
+ return true
+ }
+ return false
+ }
+ },
+ )
+
+ fun onTouchEvent(event: MotionEvent): Boolean {
+ if (isDoubleTapHandlingEnabled) {
+ gestureDetector.onTouchEvent(event)
+ if (event.actionMasked == MotionEvent.ACTION_UP && doubleTapAwaitingUp) {
+ lastDoubleTapDownEventTime?.let { time ->
+ if (
+ event.eventTime - time < ViewConfiguration.getDoubleTapTimeout()
+ ) {
+ cancelScheduledLongPress()
+ onDoubleTapDetected()
+ }
+ }
+ doubleTapAwaitingUp = false
+ } else if (event.actionMasked == MotionEvent.ACTION_CANCEL && doubleTapAwaitingUp) {
+ doubleTapAwaitingUp = false
}
- is MotionEventModel.Move -> {
- if (event.distanceMoved > allowedTouchSlop) {
- logger?.cancelingLongPressDueToTouchSlop(event.distanceMoved, allowedTouchSlop)
+ }
+
+ if (isLongPressHandlingEnabled) {
+ val motionEventModel = event.toModel()
+
+ return when (motionEventModel) {
+ is MotionEventModel.Down -> {
+ scheduleLongPress(motionEventModel.x, motionEventModel.y)
+ lastEventDownCoordinate.x = motionEventModel.x
+ lastEventDownCoordinate.y = motionEventModel.y
+ true
+ }
+
+ is MotionEventModel.Move -> {
+ if (motionEventModel.distanceMoved > allowedTouchSlop) {
+ logger?.cancelingLongPressDueToTouchSlop(
+ motionEventModel.distanceMoved,
+ allowedTouchSlop,
+ )
+ cancelScheduledLongPress()
+ }
+ false
+ }
+
+ is MotionEventModel.Up -> {
+ logger?.onUpEvent(
+ motionEventModel.distanceMoved,
+ allowedTouchSlop,
+ motionEventModel.gestureDuration,
+ )
cancelScheduledLongPress()
+ if (
+ motionEventModel.distanceMoved <= allowedTouchSlop &&
+ motionEventModel.gestureDuration < longPressDuration()
+ ) {
+ logger?.dispatchingSingleTap()
+ dispatchSingleTap(lastEventDownCoordinate.x, lastEventDownCoordinate.y)
+ }
+ false
}
- false
- }
- is MotionEventModel.Up -> {
- logger?.onUpEvent(event.distanceMoved, allowedTouchSlop, event.gestureDuration)
- cancelScheduledLongPress()
- if (
- event.distanceMoved <= allowedTouchSlop &&
- event.gestureDuration < longPressDuration()
- ) {
- logger?.dispatchingSingleTap()
- dispatchSingleTap(lastEventDownCoordinate.x, lastEventDownCoordinate.y)
+
+ is MotionEventModel.Cancel -> {
+ logger?.motionEventCancelled()
+ cancelScheduledLongPress()
+ false
}
- false
- }
- is MotionEventModel.Cancel -> {
- logger?.motionEventCancelled()
- cancelScheduledLongPress()
- false
+
+ else -> false
}
- else -> false
}
+
+ return false
}
private fun scheduleLongPress(x: Int, y: Int) {
@@ -134,4 +194,30 @@ class TouchHandlingViewInteractionHandler(
onSingleTapDetected(x, y)
}
+
+ private fun MotionEvent.toModel(): MotionEventModel {
+ return when (actionMasked) {
+ MotionEvent.ACTION_DOWN -> MotionEventModel.Down(x = x.toInt(), y = y.toInt())
+ MotionEvent.ACTION_MOVE -> MotionEventModel.Move(distanceMoved = distanceMoved())
+ MotionEvent.ACTION_UP ->
+ MotionEventModel.Up(
+ distanceMoved = distanceMoved(),
+ gestureDuration = gestureDuration(),
+ )
+ MotionEvent.ACTION_CANCEL -> MotionEventModel.Cancel
+ else -> MotionEventModel.Other
+ }
+ }
+
+ private fun MotionEvent.distanceMoved(): Float {
+ return if (historySize > 0) {
+ sqrt((x - getHistoricalX(0)).pow(2) + (y - getHistoricalY(0)).pow(2))
+ } else {
+ 0f
+ }
+ }
+
+ private fun MotionEvent.gestureDuration(): Long {
+ return eventTime - downTime
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
index 140db7b7a0b7..62a98d7a48ea 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
@@ -377,12 +377,15 @@ constructor(
MutableStateFlow(false)
}
- val inAllowedKeyguardState =
- keyguardTransitionInteractor.startedKeyguardTransitionStep.map {
- it.to == KeyguardState.LOCKSCREEN || it.to == KeyguardState.GLANCEABLE_HUB
- }
-
- allOf(inAllowedDeviceState, inAllowedKeyguardState)
+ if (v2FlagEnabled()) {
+ val inAllowedKeyguardState =
+ keyguardTransitionInteractor.startedKeyguardTransitionStep.map {
+ it.to == KeyguardState.LOCKSCREEN || it.to == KeyguardState.GLANCEABLE_HUB
+ }
+ allOf(inAllowedDeviceState, inAllowedKeyguardState)
+ } else {
+ inAllowedDeviceState
+ }
}
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
index a25faa3a7aec..11b42a8eafd6 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
@@ -36,6 +36,8 @@ import com.android.systemui.dock.DockManager;
import com.android.systemui.dock.DockManagerImpl;
import com.android.systemui.doze.DozeHost;
import com.android.systemui.education.dagger.ContextualEducationModule;
+import com.android.systemui.topwindoweffects.dagger.SqueezeEffectRepositoryModule;
+import com.android.systemui.topwindoweffects.dagger.TopLevelWindowEffectsModule;
import com.android.systemui.emergency.EmergencyGestureModule;
import com.android.systemui.inputdevice.tutorial.KeyboardTouchpadTutorialModule;
import com.android.systemui.keyboard.shortcut.ShortcutHelperModule;
@@ -160,12 +162,14 @@ import javax.inject.Named;
StatusBarPhoneModule.class,
SystemActionsModule.class,
ShadeModule.class,
+ SqueezeEffectRepositoryModule.class,
StartCentralSurfacesModule.class,
SceneContainerFrameworkModule.class,
SysUICoroutinesModule.class,
SysUIUnfoldStartableModule.class,
UnfoldTransitionModule.Startables.class,
ToastModule.class,
+ TopLevelWindowEffectsModule.class,
TouchpadTutorialModule.class,
VolumeModule.class,
WallpaperModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricAuthInteractor.kt
index 69da67e055fe..1e7bec257432 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricAuthInteractor.kt
@@ -68,10 +68,4 @@ constructor(
emptyFlow()
}
}
-
- /** Triggered if a face failure occurs regardless of the mode. */
- val faceFailure: Flow<FailedFaceAuthenticationStatus> =
- deviceEntryFaceAuthInteractor.authenticationStatus.filterIsInstance<
- FailedFaceAuthenticationStatus
- >()
}
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt
index 38e0503440f9..09936839c590 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt
@@ -22,7 +22,6 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dump.DumpManager
import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor
import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardBypassInteractor
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.power.shared.model.WakeSleepReason
import com.android.systemui.util.kotlin.FlowDumperImpl
@@ -49,8 +48,6 @@ class DeviceEntryHapticsInteractor
constructor(
biometricSettingsRepository: BiometricSettingsRepository,
deviceEntryBiometricAuthInteractor: DeviceEntryBiometricAuthInteractor,
- deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor,
- keyguardBypassInteractor: KeyguardBypassInteractor,
deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor,
deviceEntrySourceInteractor: DeviceEntrySourceInteractor,
fingerprintPropertyRepository: FingerprintPropertyRepository,
@@ -83,7 +80,12 @@ constructor(
emit(recentPowerButtonPressThresholdMs * -1L - 1L)
}
- private val playHapticsOnDeviceEntry: Flow<Boolean> =
+ /**
+ * Indicates when success haptics should play when the device is entered. This always occurs on
+ * successful fingerprint authentications. It also occurs on successful face authentication but
+ * only if the lockscreen is bypassed.
+ */
+ val playSuccessHapticOnDeviceEntry: Flow<Unit> =
deviceEntrySourceInteractor.deviceEntryFromBiometricSource
.sample(
combine(
@@ -93,29 +95,17 @@ constructor(
::Triple,
)
)
- .map { (sideFpsEnrolled, powerButtonDown, lastPowerButtonWakeup) ->
+ .filter { (sideFpsEnrolled, powerButtonDown, lastPowerButtonWakeup) ->
val sideFpsAllowsHaptic =
!powerButtonDown &&
systemClock.uptimeMillis() - lastPowerButtonWakeup >
recentPowerButtonPressThresholdMs
val allowHaptic = !sideFpsEnrolled || sideFpsAllowsHaptic
if (!allowHaptic) {
- logger.d(
- "Skip success entry haptic from power button. Recent power button press or button is down."
- )
+ logger.d("Skip success haptic. Recent power button press or button is down.")
}
allowHaptic
}
-
- private val playHapticsOnFaceAuthSuccessAndBypassDisabled: Flow<Boolean> =
- deviceEntryFaceAuthInteractor.isAuthenticated
- .filter { it }
- .sample(keyguardBypassInteractor.isBypassAvailable)
- .map { !it }
-
- val playSuccessHaptic: Flow<Unit> =
- merge(playHapticsOnDeviceEntry, playHapticsOnFaceAuthSuccessAndBypassDisabled)
- .filter { it }
// map to Unit
.map {}
.dumpWhileCollecting("playSuccessHaptic")
@@ -123,7 +113,7 @@ constructor(
private val playErrorHapticForBiometricFailure: Flow<Unit> =
merge(
deviceEntryFingerprintAuthInteractor.fingerprintFailure,
- deviceEntryBiometricAuthInteractor.faceFailure,
+ deviceEntryBiometricAuthInteractor.faceOnlyFaceFailure,
)
// map to Unit
.map {}
diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/FakePerDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/FakePerDisplayRepository.kt
index 083191c8ecde..a56710ee3772 100644
--- a/packages/SystemUI/src/com/android/systemui/display/data/repository/FakePerDisplayRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/FakePerDisplayRepository.kt
@@ -32,9 +32,6 @@ class FakePerDisplayRepository<T> : PerDisplayRepository<T> {
return instances[displayId]
}
- override val displayIds: Set<Int>
- get() = instances.keys
-
override val debugName: String
get() = "FakePerDisplayRepository"
}
diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt
index d27e33e53dbb..d1d013542fbf 100644
--- a/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt
@@ -86,9 +86,6 @@ interface PerDisplayRepository<T> {
/** Gets the cached instance or create a new one for a given display. */
operator fun get(displayId: Int): T?
- /** List of display ids for which this repository has an instance. */
- val displayIds: Set<Int>
-
/** Debug name for this repository, mainly for tracing and logging. */
val debugName: String
}
@@ -122,9 +119,6 @@ constructor(
backgroundApplicationScope.launch("$debugName#start") { start() }
}
- override val displayIds: Set<Int>
- get() = perDisplayInstances.keys
-
private suspend fun start() {
dumpManager.registerNormalDumpable("PerDisplayRepository-${debugName}", this)
displayRepository.displayIds.collectLatest { displayIds ->
@@ -199,8 +193,6 @@ class DefaultDisplayOnlyInstanceRepositoryImpl<T>(
private val lazyDefaultDisplayInstance by lazy {
instanceProvider.createInstance(Display.DEFAULT_DISPLAY)
}
- override val displayIds: Set<Int> = setOf(Display.DEFAULT_DISPLAY)
-
override fun get(displayId: Int): T? = lazyDefaultDisplayInstance
}
@@ -214,7 +206,5 @@ class DefaultDisplayOnlyInstanceRepositoryImpl<T>(
*/
class SingleInstanceRepositoryImpl<T>(override val debugName: String, private val instance: T) :
PerDisplayRepository<T> {
- override val displayIds: Set<Int> = setOf(Display.DEFAULT_DISPLAY)
-
override fun get(displayId: Int): T? = instance
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyevent/data/repository/KeyEventRepository.kt b/packages/SystemUI/src/com/android/systemui/keyevent/data/repository/KeyEventRepository.kt
index 9da9a7328659..32257df40d02 100644
--- a/packages/SystemUI/src/com/android/systemui/keyevent/data/repository/KeyEventRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyevent/data/repository/KeyEventRepository.kt
@@ -18,7 +18,7 @@ package com.android.systemui.keyevent.data.repository
import android.view.KeyEvent
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.CommandQueue
import javax.inject.Inject
@@ -29,6 +29,9 @@ import kotlinx.coroutines.flow.Flow
interface KeyEventRepository {
/** Observable for whether the power button key is pressed/down or not. */
val isPowerButtonDown: Flow<Boolean>
+
+ /** Observable for when the power button is being pressed but till the duration of long press */
+ val isPowerButtonLongPressed: Flow<Boolean>
}
@SysUISingleton
@@ -51,6 +54,21 @@ constructor(
awaitClose { commandQueue.removeCallback(callback) }
}
+ override val isPowerButtonLongPressed: Flow<Boolean> = conflatedCallbackFlow {
+ val callback =
+ object : CommandQueue.Callbacks {
+ override fun handleSystemKey(event: KeyEvent) {
+ if (event.keyCode == KeyEvent.KEYCODE_POWER) {
+ trySendWithFailureLogging(event.action == KeyEvent.ACTION_DOWN
+ && event.isLongPress, TAG, "updated isPowerButtonLongPressed")
+ }
+ }
+ }
+ trySendWithFailureLogging(false, TAG, "init isPowerButtonLongPressed")
+ commandQueue.addCallback(callback)
+ awaitClose { commandQueue.removeCallback(callback) }
+ }
+
companion object {
private const val TAG = "KeyEventRepositoryImpl"
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractor.kt
index 9949fa589cd5..ec9bbfb1bc77 100644
--- a/packages/SystemUI/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractor.kt
@@ -32,4 +32,5 @@ constructor(
repository: KeyEventRepository,
) {
val isPowerButtonDown = repository.isPowerButtonDown
+ val isPowerButtonLongPressed = repository.isPowerButtonLongPressed
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
index 3b85b571023f..9ade5036b9a8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
@@ -51,10 +51,10 @@ import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.keyguard.KeyguardUpdateMonitorCallback;
import com.android.systemui.SystemUIAppComponentFactoryBase;
import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.media.NotificationMediaManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.res.R;
import com.android.systemui.settings.UserTracker;
-import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt
index 705eaa22aa9a..55534c4f1444 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt
@@ -20,11 +20,14 @@ package com.android.systemui.keyguard.domain.interactor
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
+import android.os.PowerManager
+import android.provider.Settings
import android.view.accessibility.AccessibilityManager
import androidx.annotation.VisibleForTesting
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.internal.logging.UiEvent
import com.android.internal.logging.UiEventLogger
+import com.android.systemui.Flags.doubleTapToSleep
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -36,10 +39,13 @@ import com.android.systemui.res.R
import com.android.systemui.shade.PulsingGestureListener
import com.android.systemui.shade.ShadeDisplayAware
import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
+import com.android.systemui.util.settings.repository.UserAwareSecureSettingsRepository
+import com.android.systemui.util.time.SystemClock
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -66,10 +72,13 @@ constructor(
private val accessibilityManager: AccessibilityManagerWrapper,
private val pulsingGestureListener: PulsingGestureListener,
private val faceAuthInteractor: DeviceEntryFaceAuthInteractor,
+ private val secureSettingsRepository: UserAwareSecureSettingsRepository,
+ private val powerManager: PowerManager,
+ private val systemClock: SystemClock,
) {
/** Whether the long-press handling feature should be enabled. */
val isLongPressHandlingEnabled: StateFlow<Boolean> =
- if (isFeatureEnabled()) {
+ if (isLongPressFeatureEnabled()) {
combine(
transitionInteractor.isFinishedIn(KeyguardState.LOCKSCREEN),
repository.isQuickSettingsVisible,
@@ -85,6 +94,30 @@ constructor(
initialValue = false,
)
+ /** Whether the double tap handling handling feature should be enabled. */
+ val isDoubleTapHandlingEnabled: StateFlow<Boolean> =
+ if (isDoubleTapFeatureEnabled()) {
+ combine(
+ transitionInteractor.transitionValue(KeyguardState.LOCKSCREEN),
+ repository.isQuickSettingsVisible,
+ isDoubleTapSettingEnabled(),
+ ) {
+ isFullyTransitionedToLockScreen,
+ isQuickSettingsVisible,
+ isDoubleTapSettingEnabled ->
+ isFullyTransitionedToLockScreen == 1f &&
+ !isQuickSettingsVisible &&
+ isDoubleTapSettingEnabled
+ }
+ } else {
+ flowOf(false)
+ }
+ .stateIn(
+ scope = scope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = false,
+ )
+
private val _isMenuVisible = MutableStateFlow(false)
/** Model for whether the menu should be shown. */
val isMenuVisible: StateFlow<Boolean> =
@@ -116,7 +149,7 @@ constructor(
private var delayedHideMenuJob: Job? = null
init {
- if (isFeatureEnabled()) {
+ if (isLongPressFeatureEnabled()) {
broadcastDispatcher
.broadcastFlow(IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
.onEach { hideMenu() }
@@ -175,17 +208,30 @@ constructor(
/** Notifies that the lockscreen has been double clicked. */
fun onDoubleClick() {
- pulsingGestureListener.onDoubleTapEvent()
+ if (isDoubleTapHandlingEnabled.value) {
+ powerManager.goToSleep(systemClock.uptimeMillis())
+ } else {
+ pulsingGestureListener.onDoubleTapEvent()
+ }
+ }
+
+ private fun isDoubleTapSettingEnabled(): Flow<Boolean> {
+ return secureSettingsRepository.boolSetting(Settings.Secure.DOUBLE_TAP_TO_SLEEP)
}
private fun showSettings() {
_shouldOpenSettings.value = true
}
- private fun isFeatureEnabled(): Boolean {
+ private fun isLongPressFeatureEnabled(): Boolean {
return context.resources.getBoolean(R.bool.long_press_keyguard_customize_lockscreen_enabled)
}
+ private fun isDoubleTapFeatureEnabled(): Boolean {
+ return doubleTapToSleep() &&
+ context.resources.getBoolean(com.android.internal.R.bool.config_supportDoubleTapSleep)
+ }
+
/** Updates application state to ask to show the menu. */
private fun showMenu() {
_isMenuVisible.value = true
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
index 17e14c3e83da..70a827d5e45b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
@@ -48,7 +48,7 @@ object DeviceEntryIconViewBinder {
/**
* Updates UI for:
* - device entry containing view (parent view for the below views)
- * - long-press handling view (transparent, no UI)
+ * - touch handling view (transparent, no UI)
* - foreground icon view (lock/unlock/fingerprint)
* - background view (optional)
*/
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewSmartspaceViewBinder.kt
index 741b149f29da..92b9da6790d1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewSmartspaceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewSmartspaceViewBinder.kt
@@ -31,6 +31,37 @@ import com.android.systemui.plugins.clocks.ClockPreviewConfig
object KeyguardPreviewSmartspaceViewBinder {
@JvmStatic
+ fun bind(parentView: View, viewModel: KeyguardPreviewSmartspaceViewModel) {
+ if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {
+ val largeDateView =
+ parentView.findViewById<View>(
+ com.android.systemui.shared.R.id.date_smartspace_view_large
+ )
+ val smallDateView =
+ parentView.findViewById<View>(com.android.systemui.shared.R.id.date_smartspace_view)
+ parentView.repeatWhenAttached {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ launch("$TAG#viewModel.selectedClockSize") {
+ viewModel.previewingClockSize.collect {
+ when (it) {
+ ClockSizeSetting.DYNAMIC -> {
+ smallDateView?.visibility = View.GONE
+ largeDateView?.visibility = View.VISIBLE
+ }
+
+ ClockSizeSetting.SMALL -> {
+ smallDateView?.visibility = View.VISIBLE
+ largeDateView?.visibility = View.GONE
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @JvmStatic
fun bind(
smartspace: View,
viewModel: KeyguardPreviewSmartspaceViewModel,
@@ -44,6 +75,7 @@ object KeyguardPreviewSmartspaceViewBinder {
when (it) {
ClockSizeSetting.DYNAMIC ->
viewModel.getLargeClockSmartspaceTopPadding(clockPreviewConfig)
+
ClockSizeSetting.SMALL ->
viewModel.getSmallClockSmartspaceTopPadding(clockPreviewConfig)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index 45801ba3517a..aeb327035c79 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -333,7 +333,7 @@ object KeyguardRootViewBinder {
if (deviceEntryHapticsInteractor != null && vibratorHelper != null) {
launch {
- deviceEntryHapticsInteractor.playSuccessHaptic.collect {
+ deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry.collect {
if (msdlFeedback()) {
msdlPlayer?.playToken(
MSDLToken.UNLOCK,
@@ -474,7 +474,7 @@ object KeyguardRootViewBinder {
val transition = blueprintViewModel.currentTransition.value
val shouldAnimate = transition != null && transition.config.type.animateNotifChanges
if (prevTransition == transition && shouldAnimate) {
- logger.w("Skipping; layout during transition")
+ logger.w("Skipping onNotificationContainerBoundsChanged during transition")
return
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
index e81d5354ec6e..5ef2d6fd3256 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
@@ -29,6 +29,7 @@ import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.plugins.clocks.VRectF
import com.android.systemui.res.R
import com.android.systemui.shared.R as sharedR
import kotlinx.coroutines.DisposableHandle
@@ -135,7 +136,7 @@ object KeyguardSmartspaceViewBinder {
}
}
- if (clockBounds == null) return@collect
+ if (clockBounds == VRectF.ZERO) return@collect
if (isLargeClock) {
val largeDateHeight =
keyguardRootView
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardTouchViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardTouchViewBinder.kt
index 195413a80f4b..485e1ce5b2f7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardTouchViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardTouchViewBinder.kt
@@ -75,6 +75,13 @@ object KeyguardTouchViewBinder {
onSingleTap(x, y)
}
+
+ override fun onDoubleTapDetected(view: View) {
+ if (falsingManager.isFalseDoubleTap()) {
+ return
+ }
+ viewModel.onDoubleClick()
+ }
}
view.repeatWhenAttached {
@@ -90,9 +97,20 @@ object KeyguardTouchViewBinder {
}
}
}
+ launch("$TAG#viewModel.isDoubleTapHandlingEnabled") {
+ viewModel.isDoubleTapHandlingEnabled.collect { isEnabled ->
+ view.setDoublePressHandlingEnabled(isEnabled)
+ view.contentDescription =
+ if (isEnabled) {
+ view.resources.getString(R.string.accessibility_desc_lock_screen)
+ } else {
+ null
+ }
+ }
+ }
}
}
}
- private const val TAG = "KeyguardLongPressViewBinder"
+ private const val TAG = "KeyguardTouchViewBinder"
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
index 242926b3e1d1..d749e3c11378 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
@@ -68,6 +68,7 @@ import com.android.systemui.monet.ColorScheme
import com.android.systemui.monet.Style
import com.android.systemui.plugins.clocks.ClockController
import com.android.systemui.plugins.clocks.ClockPreviewConfig
+import com.android.systemui.plugins.clocks.ContextExt.getId
import com.android.systemui.plugins.clocks.ThemeConfig
import com.android.systemui.plugins.clocks.WeatherData
import com.android.systemui.res.R
@@ -126,6 +127,7 @@ constructor(
private val displayId = bundle.getInt(KEY_DISPLAY_ID, DEFAULT_DISPLAY)
private val display: Display? = displayManager.getDisplay(displayId)
+
/**
* Returns a key that should make the KeyguardPreviewRenderer unique and if two of them have the
* same key they will be treated as the same KeyguardPreviewRenderer. Primary this is used to
@@ -144,6 +146,8 @@ constructor(
get() = checkNotNull(host.surfacePackage)
private var smartSpaceView: View? = null
+ private var largeDateView: View? = null
+ private var smallDateView: View? = null
private val disposables = DisposableHandles()
private var isDestroyed = false
@@ -181,7 +185,7 @@ constructor(
ContextThemeWrapper(context.createDisplayContext(it), context.getTheme())
} ?: context
- val rootView = FrameLayout(previewContext)
+ val rootView = ConstraintLayout(previewContext)
setupKeyguardRootView(previewContext, rootView)
@@ -252,6 +256,24 @@ constructor(
fun onClockSizeSelected(clockSize: ClockSizeSetting) {
smartspaceViewModel.setOverrideClockSize(clockSize)
+ if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {
+ when (clockSize) {
+ ClockSizeSetting.DYNAMIC -> {
+ largeDateView?.post {
+ smallDateView?.visibility = View.GONE
+ largeDateView?.visibility = View.VISIBLE
+ }
+ }
+
+ ClockSizeSetting.SMALL -> {
+ largeDateView?.post {
+ smallDateView?.visibility = View.VISIBLE
+ largeDateView?.visibility = View.GONE
+ }
+ }
+ }
+ smartSpaceView?.post { smartSpaceView?.visibility = View.GONE }
+ }
}
fun destroy() {
@@ -280,7 +302,7 @@ constructor(
*
* The end padding is as follows: Below clock padding end
*/
- private fun setUpSmartspace(previewContext: Context, parentView: ViewGroup) {
+ private fun setUpSmartspace(previewContext: Context, parentView: ConstraintLayout) {
if (
!lockscreenSmartspaceController.isEnabled ||
!lockscreenSmartspaceController.isDateWeatherDecoupled
@@ -292,40 +314,90 @@ constructor(
parentView.removeView(smartSpaceView)
}
- smartSpaceView =
- lockscreenSmartspaceController.buildAndConnectDateView(
- parent = parentView,
- isLargeClock = false,
- )
+ if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {
+ val cs = ConstraintSet()
+ cs.clone(parentView)
+ cs.apply {
+ val largeClockViewId = previewContext.getId("lockscreen_clock_view_large")
+ val smallClockViewId = previewContext.getId("lockscreen_clock_view")
+ largeDateView =
+ lockscreenSmartspaceController
+ .buildAndConnectDateView(parentView, true)
+ ?.also { view ->
+ constrainWidth(view.id, ConstraintSet.WRAP_CONTENT)
+ constrainHeight(view.id, ConstraintSet.WRAP_CONTENT)
+ connect(view.id, START, largeClockViewId, START)
+ connect(view.id, ConstraintSet.END, largeClockViewId, ConstraintSet.END)
+ connect(
+ view.id,
+ TOP,
+ largeClockViewId,
+ ConstraintSet.BOTTOM,
+ smartspaceViewModel.getDateWeatherEndPadding(previewContext),
+ )
+ }
+ smallDateView =
+ lockscreenSmartspaceController
+ .buildAndConnectDateView(parentView, false)
+ ?.also { view ->
+ constrainWidth(view.id, ConstraintSet.WRAP_CONTENT)
+ constrainHeight(view.id, ConstraintSet.WRAP_CONTENT)
+ connect(
+ view.id,
+ START,
+ smallClockViewId,
+ ConstraintSet.END,
+ context.resources.getDimensionPixelSize(
+ R.dimen.smartspace_padding_horizontal
+ ),
+ )
+ connect(view.id, TOP, smallClockViewId, TOP)
+ connect(
+ view.id,
+ ConstraintSet.BOTTOM,
+ smallClockViewId,
+ ConstraintSet.BOTTOM,
+ )
+ }
+ parentView.addView(largeDateView)
+ parentView.addView(smallDateView)
+ }
+ cs.applyTo(parentView)
+ } else {
+ smartSpaceView =
+ lockscreenSmartspaceController.buildAndConnectDateView(
+ parent = parentView,
+ isLargeClock = false,
+ )
- val topPadding: Int =
- smartspaceViewModel.getLargeClockSmartspaceTopPadding(
- ClockPreviewConfig(
- previewContext,
- getPreviewShadeLayoutWide(display!!),
- SceneContainerFlag.isEnabled,
+ val topPadding: Int =
+ smartspaceViewModel.getLargeClockSmartspaceTopPadding(
+ ClockPreviewConfig(
+ previewContext,
+ getPreviewShadeLayoutWide(display!!),
+ SceneContainerFlag.isEnabled,
+ )
)
- )
- val startPadding: Int = smartspaceViewModel.getDateWeatherStartPadding(previewContext)
- val endPadding: Int = smartspaceViewModel.getDateWeatherEndPadding(previewContext)
-
- smartSpaceView?.let {
- it.setPaddingRelative(startPadding, topPadding, endPadding, 0)
- it.isClickable = false
- it.isInvisible = true
- parentView.addView(
- it,
- FrameLayout.LayoutParams(
- FrameLayout.LayoutParams.MATCH_PARENT,
- FrameLayout.LayoutParams.WRAP_CONTENT,
- ),
- )
+ val startPadding: Int = smartspaceViewModel.getDateWeatherStartPadding(previewContext)
+ val endPadding: Int = smartspaceViewModel.getDateWeatherEndPadding(previewContext)
+
+ smartSpaceView?.let {
+ it.setPaddingRelative(startPadding, topPadding, endPadding, 0)
+ it.isClickable = false
+ it.isInvisible = true
+ parentView.addView(
+ it,
+ FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ FrameLayout.LayoutParams.WRAP_CONTENT,
+ ),
+ )
+ }
+ smartSpaceView?.alpha = if (shouldHighlightSelectedAffordance) DIM_ALPHA else 1.0f
}
-
- smartSpaceView?.alpha = if (shouldHighlightSelectedAffordance) DIM_ALPHA else 1.0f
}
- private fun setupKeyguardRootView(previewContext: Context, rootView: FrameLayout) {
+ private fun setupKeyguardRootView(previewContext: Context, rootView: ConstraintLayout) {
val keyguardRootView = KeyguardRootView(previewContext, null)
rootView.addView(
keyguardRootView,
@@ -341,6 +413,13 @@ constructor(
if (!shouldHideClock) {
setUpClock(previewContext, rootView)
+ if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {
+ setUpSmartspace(previewContext, keyguardRootView)
+ KeyguardPreviewSmartspaceViewBinder.bind(
+ keyguardRootView,
+ smartspaceViewModel,
+ )
+ }
KeyguardPreviewClockViewBinder.bind(
keyguardRootView,
clockViewModel,
@@ -354,19 +433,22 @@ constructor(
)
}
- setUpSmartspace(previewContext, rootView)
-
- smartSpaceView?.let {
- KeyguardPreviewSmartspaceViewBinder.bind(
- it,
- smartspaceViewModel,
- clockPreviewConfig =
- ClockPreviewConfig(
- previewContext,
- getPreviewShadeLayoutWide(display!!),
- SceneContainerFlag.isEnabled,
- ),
- )
+ if (!com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {
+ setUpSmartspace(previewContext, keyguardRootView)
+ smartSpaceView?.let {
+ KeyguardPreviewSmartspaceViewBinder.bind(
+ it,
+ smartspaceViewModel,
+ clockPreviewConfig =
+ ClockPreviewConfig(
+ previewContext,
+ getPreviewShadeLayoutWide(display!!),
+ SceneContainerFlag.isEnabled,
+ lockId = null,
+ udfpsTop = null,
+ ),
+ )
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt
index c4a7e1ed95e1..55fac3c7644b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt
@@ -36,6 +36,7 @@ import com.android.systemui.keyguard.ui.viewmodel.DreamingToGlanceableHubTransit
import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToAodTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToDreamingTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToLockscreenTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToOccludedTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.GoneToAodTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.GoneToDozingTransitionViewModel
@@ -272,6 +273,12 @@ abstract class DeviceEntryIconTransitionModule {
@Binds
@IntoSet
+ abstract fun glanceableHubToLockscreen(
+ impl: GlanceableHubToLockscreenTransitionViewModel
+ ): DeviceEntryIconTransition
+
+ @Binds
+ @IntoSet
abstract fun occludedToGlanceableHub(
impl: OccludedToGlanceableHubTransitionViewModel
): DeviceEntryIconTransition
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
index 0ccb24a9858a..20fc88446ce3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
@@ -61,6 +61,7 @@ constructor(
primaryBouncerToLockscreenTransitionViewModel: PrimaryBouncerToLockscreenTransitionViewModel,
lockscreenToDozingTransitionViewModel: LockscreenToDozingTransitionViewModel,
glanceableHubToAodTransitionViewModel: GlanceableHubToAodTransitionViewModel,
+ glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel,
) {
val color: Flow<Int> =
deviceEntryIconViewModel.useBackgroundProtection.flatMapLatest { useBackground ->
@@ -108,6 +109,7 @@ constructor(
.deviceEntryBackgroundViewAlpha,
lockscreenToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha,
glanceableHubToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
+ glanceableHubToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
)
.merge()
.onStart {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt
index b4b4c82c59b9..bcbe66642d11 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt
@@ -27,6 +27,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB
import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
import com.android.systemui.keyguard.ui.StateToValue
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
import com.android.systemui.keyguard.ui.transitions.GlanceableHubTransition
import com.android.systemui.res.R
import com.android.systemui.scene.shared.model.Scenes
@@ -49,7 +50,7 @@ constructor(
@ShadeDisplayAware configurationInteractor: ConfigurationInteractor,
animationFlow: KeyguardTransitionAnimationFlow,
private val blurFactory: GlanceableHubBlurComponent.Factory,
-) : GlanceableHubTransition {
+) : GlanceableHubTransition, DeviceEntryIconTransition {
private val transitionAnimation =
animationFlow
.setup(
@@ -102,4 +103,8 @@ constructor(
val notificationTranslationX: Flow<Float> =
keyguardTranslationX.map { it.value }.filterNotNull()
+
+ val deviceEntryBackgroundViewAlpha: Flow<Float> = keyguardAlpha
+
+ override val deviceEntryParentViewAlpha: Flow<Float> = keyguardAlpha
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index d4676bc9c146..6d8a943d3e28 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -132,6 +132,8 @@ constructor(
private val occludedToAodTransitionViewModel: OccludedToAodTransitionViewModel,
private val occludedToDozingTransitionViewModel: OccludedToDozingTransitionViewModel,
private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
+ private val occludedToPrimaryBouncerTransitionViewModel:
+ OccludedToPrimaryBouncerTransitionViewModel,
private val offToLockscreenTransitionViewModel: OffToLockscreenTransitionViewModel,
private val primaryBouncerToAodTransitionViewModel: PrimaryBouncerToAodTransitionViewModel,
private val primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel,
@@ -288,6 +290,7 @@ constructor(
occludedToAodTransitionViewModel.lockscreenAlpha,
occludedToDozingTransitionViewModel.lockscreenAlpha,
occludedToLockscreenTransitionViewModel.lockscreenAlpha,
+ occludedToPrimaryBouncerTransitionViewModel.lockscreenAlpha,
offToLockscreenTransitionViewModel.lockscreenAlpha,
primaryBouncerToAodTransitionViewModel.lockscreenAlpha,
primaryBouncerToGoneTransitionViewModel.lockscreenAlpha,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt
index 1d2edc6d406b..d4e7af48adfe 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt
@@ -33,6 +33,9 @@ constructor(
/** Whether the long-press handling feature should be enabled. */
val isLongPressHandlingEnabled: Flow<Boolean> = interactor.isLongPressHandlingEnabled
+ /** Whether the double tap handling feature should be enabled. */
+ val isDoubleTapHandlingEnabled: Flow<Boolean> = interactor.isDoubleTapHandlingEnabled
+
/** Notifies that the user has long-pressed on the lock screen.
*
* @param isA11yAction: Whether the action was performed as an a11y action
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt
index f14a5a282e88..67c3071db390 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt
@@ -45,6 +45,12 @@ constructor(
)
.setupWithoutSceneContainer(edge = Edge.create(OCCLUDED, PRIMARY_BOUNCER))
+ /**
+ * Reasserts that lockscreen content should not be visible. It is possible the keyguard alpha is
+ * set to 1f if coming from an expanded shade that collapsed to launch an occluding activity.
+ */
+ val lockscreenAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f)
+
override val windowBlurRadius: Flow<Float> =
shadeDependentFlows.transitionFlow(
flowWhenShadeIsExpanded =
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index faa6c52162ce..a85b9b04c1ce 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -327,7 +327,8 @@ public class LogModule {
@SysUISingleton
@KeyguardBlueprintLog
public static LogBuffer provideKeyguardBlueprintLog(LogBufferFactory factory) {
- return factory.create("KeyguardBlueprintLog", 100);
+ // TODO(b/389987229): Reduce back to 100
+ return factory.create("KeyguardBlueprintLog", 1000);
}
/**
@@ -337,7 +338,8 @@ public class LogModule {
@SysUISingleton
@KeyguardClockLog
public static LogBuffer provideKeyguardClockLog(LogBufferFactory factory) {
- return factory.create("KeyguardClockLog", 100);
+ // TODO(b/389987229): Reduce back to 100
+ return factory.create("KeyguardClockLog", 1000);
}
/**
@@ -347,7 +349,8 @@ public class LogModule {
@SysUISingleton
@KeyguardSmallClockLog
public static LogBuffer provideKeyguardSmallClockLog(LogBufferFactory factory) {
- return factory.create("KeyguardSmallClockLog", 100);
+ // TODO(b/389987229): Reduce back to 100
+ return factory.create("KeyguardSmallClockLog", 1000);
}
/**
@@ -357,7 +360,8 @@ public class LogModule {
@SysUISingleton
@KeyguardLargeClockLog
public static LogBuffer provideKeyguardLargeClockLog(LogBufferFactory factory) {
- return factory.create("KeyguardLargeClockLog", 100);
+ // TODO(b/389987229): Reduce back to 100
+ return factory.create("KeyguardLargeClockLog", 1000);
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/media/NotificationMediaManager.java
index 18f4b4ab9b88..db4c7a5b2ee5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
+++ b/packages/SystemUI/src/com/android/systemui/media/NotificationMediaManager.java
@@ -11,9 +11,9 @@
* 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
+ * limitations under the License.
*/
-package com.android.systemui.statusbar;
+package com.android.systemui.media;
import static com.android.systemui.Flags.mediaControlsUserInitiatedDeleteintent;
@@ -40,6 +40,8 @@ import com.android.systemui.dump.DumpManager;
import com.android.systemui.media.controls.domain.pipeline.MediaDataManager;
import com.android.systemui.media.controls.shared.model.MediaData;
import com.android.systemui.media.controls.shared.model.SmartspaceMediaData;
+import com.android.systemui.statusbar.NotificationPresenter;
+import com.android.systemui.statusbar.StatusBarIconView;
import com.android.systemui.statusbar.dagger.CentralSurfacesModule;
import com.android.systemui.statusbar.notification.collection.NotifCollection;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
diff --git a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
index e7c2a454e16c..b71d8c995e51 100644
--- a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
+++ b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
@@ -17,6 +17,7 @@
package com.android.systemui.media;
import android.annotation.Nullable;
+import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.PackageManager.NameNotFoundException;
@@ -126,6 +127,8 @@ public class RingtonePlayer implements CoreStartable {
Log.d(TAG, "play(token=" + token + ", uri=" + uri + ", uid="
+ Binder.getCallingUid() + ")");
}
+ enforceUriUserId(uri);
+
Client client;
synchronized (mClients) {
client = mClients.get(token);
@@ -207,6 +210,7 @@ public class RingtonePlayer implements CoreStartable {
@Override
public String getTitle(Uri uri) {
+ enforceUriUserId(uri);
final UserHandle user = Binder.getCallingUserHandle();
return Ringtone.getTitle(getContextForUser(user), uri,
false /*followSettingsUri*/, false /*allowRemote*/);
@@ -214,6 +218,7 @@ public class RingtonePlayer implements CoreStartable {
@Override
public ParcelFileDescriptor openRingtone(Uri uri) {
+ enforceUriUserId(uri);
final UserHandle user = Binder.getCallingUserHandle();
final ContentResolver resolver = getContextForUser(user).getContentResolver();
@@ -241,6 +246,28 @@ public class RingtonePlayer implements CoreStartable {
}
};
+ /**
+ * Must be called from the Binder calling thread.
+ * Ensures caller is from the same userId as the content they're trying to access.
+ * @param uri the URI to check
+ * @throws SecurityException when in a non-system call and userId in uri differs from the
+ * caller's userId
+ */
+ private void enforceUriUserId(Uri uri) throws SecurityException {
+ final int uriUserId = ContentProvider.getUserIdFromUri(uri, UserHandle.myUserId());
+ // for a non-system call, verify the URI to play belongs to the same user as the caller
+ if (UserHandle.isApp(Binder.getCallingUid()) && (UserHandle.myUserId() != uriUserId)) {
+ final String errorMessage = "Illegal access to uri=" + uri
+ + " content associated with user=" + uriUserId
+ + ", current userID: " + UserHandle.myUserId();
+ if (android.media.audio.Flags.ringtoneUserUriCheck()) {
+ throw new SecurityException(errorMessage);
+ } else {
+ Log.e(TAG, errorMessage, new Exception());
+ }
+ }
+ }
+
private Context getContextForUser(UserHandle user) {
try {
return mContext.createPackageContextAsUser(mContext.getPackageName(), 0, user);
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
index 46cf0a63e93d..309b6751176c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
@@ -66,6 +66,7 @@ import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
+import com.android.systemui.media.NotificationMediaManager.isPlayingState
import com.android.systemui.media.controls.domain.pipeline.MediaDataManager.Companion.isMediaNotification
import com.android.systemui.media.controls.domain.resume.MediaResumeListener
import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser
@@ -83,7 +84,6 @@ import com.android.systemui.media.controls.util.MediaDataUtils
import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.res.R
-import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
import com.android.systemui.statusbar.notification.row.HybridGroupManager
import com.android.systemui.util.Assert
import com.android.systemui.util.Utils
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt
index 5fef81f4596a..da462e6713c6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt
@@ -30,6 +30,8 @@ import android.service.notification.StatusBarNotification
import android.util.Log
import androidx.media.utils.MediaConstants
import com.android.systemui.Flags
+import com.android.systemui.media.NotificationMediaManager.isConnectingState
+import com.android.systemui.media.NotificationMediaManager.isPlayingState
import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManagerImpl.Companion.MAX_COMPACT_ACTIONS
import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManagerImpl.Companion.MAX_NOTIFICATION_ACTIONS
import com.android.systemui.media.controls.shared.MediaControlDrawables
@@ -38,8 +40,6 @@ import com.android.systemui.media.controls.shared.model.MediaButton
import com.android.systemui.media.controls.shared.model.MediaNotificationAction
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.res.R
-import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState
-import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
import com.android.systemui.util.kotlin.logI
private const val TAG = "MediaActions"
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt
index 8bb7303a8386..dbd2250a75b0 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt
@@ -51,6 +51,7 @@ import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.graphics.ImageLoader
+import com.android.systemui.media.NotificationMediaManager.isPlayingState
import com.android.systemui.media.controls.shared.model.MediaAction
import com.android.systemui.media.controls.shared.model.MediaButton
import com.android.systemui.media.controls.shared.model.MediaData
@@ -60,7 +61,6 @@ import com.android.systemui.media.controls.util.MediaControllerFactory
import com.android.systemui.media.controls.util.MediaDataUtils
import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.res.R
-import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
import com.android.systemui.statusbar.notification.row.HybridGroupManager
import com.android.systemui.util.kotlin.logD
import java.util.concurrent.ConcurrentHashMap
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
index fe852ce7979f..94df4b353c94 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
@@ -66,6 +66,7 @@ import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
+import com.android.systemui.media.NotificationMediaManager.isPlayingState
import com.android.systemui.media.controls.data.repository.MediaDataRepository
import com.android.systemui.media.controls.domain.pipeline.MediaDataManager.Companion.isMediaNotification
import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
@@ -85,7 +86,6 @@ import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.res.R
import com.android.systemui.scene.shared.flag.SceneContainerFlag
-import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
import com.android.systemui.statusbar.notification.row.HybridGroupManager
import com.android.systemui.util.Assert
import com.android.systemui.util.Utils
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt
index 49b53c2d78ae..dfb32e66dae5 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt
@@ -37,6 +37,7 @@ import com.android.settingslib.media.LocalMediaManager
import com.android.settingslib.media.MediaDevice
import com.android.settingslib.media.PhoneMediaDevice
import com.android.settingslib.media.flags.Flags
+import com.android.systemui.Flags.mediaControlsDeviceManagerBackgroundExecution
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.media.controls.shared.MediaControlDrawables
@@ -94,8 +95,39 @@ constructor(
data: MediaData,
immediately: Boolean,
receivedSmartspaceCardLatency: Int,
- isSsReactivated: Boolean
+ isSsReactivated: Boolean,
) {
+ if (mediaControlsDeviceManagerBackgroundExecution()) {
+ bgExecutor.execute { onMediaLoaded(key, oldKey, data) }
+ } else {
+ onMediaLoaded(key, oldKey, data)
+ }
+ }
+
+ override fun onMediaDataRemoved(key: String, userInitiated: Boolean) {
+ if (mediaControlsDeviceManagerBackgroundExecution()) {
+ bgExecutor.execute { onMediaRemoved(key, userInitiated) }
+ } else {
+ onMediaRemoved(key, userInitiated)
+ }
+ }
+
+ fun dump(pw: PrintWriter) {
+ with(pw) {
+ println("MediaDeviceManager state:")
+ entries.forEach { (key, entry) ->
+ println(" key=$key")
+ entry.dump(pw)
+ }
+ }
+ }
+
+ @MainThread
+ private fun processDevice(key: String, oldKey: String?, device: MediaDeviceData?) {
+ listeners.forEach { it.onMediaDeviceChanged(key, oldKey, device) }
+ }
+
+ private fun onMediaLoaded(key: String, oldKey: String?, data: MediaData) {
if (oldKey != null && oldKey != key) {
val oldEntry = entries.remove(oldKey)
oldEntry?.stop()
@@ -104,9 +136,13 @@ constructor(
if (entry == null || entry.token != data.token) {
entry?.stop()
if (data.device != null) {
- // If we were already provided device info (e.g. from RCN), keep that and don't
- // listen for updates, but process once to push updates to listeners
- processDevice(key, oldKey, data.device)
+ // If we were already provided device info (e.g. from RCN), keep that and
+ // don't listen for updates, but process once to push updates to listeners
+ if (mediaControlsDeviceManagerBackgroundExecution()) {
+ fgExecutor.execute { processDevice(key, oldKey, data.device) }
+ } else {
+ processDevice(key, oldKey, data.device)
+ }
return
}
val controller = data.token?.let { controllerFactory.create(it) }
@@ -120,27 +156,18 @@ constructor(
}
}
- override fun onMediaDataRemoved(key: String, userInitiated: Boolean) {
+ private fun onMediaRemoved(key: String, userInitiated: Boolean) {
val token = entries.remove(key)
token?.stop()
- token?.let { listeners.forEach { it.onKeyRemoved(key, userInitiated) } }
- }
-
- fun dump(pw: PrintWriter) {
- with(pw) {
- println("MediaDeviceManager state:")
- entries.forEach { (key, entry) ->
- println(" key=$key")
- entry.dump(pw)
+ if (mediaControlsDeviceManagerBackgroundExecution()) {
+ fgExecutor.execute {
+ token?.let { listeners.forEach { it.onKeyRemoved(key, userInitiated) } }
}
+ } else {
+ token?.let { listeners.forEach { it.onKeyRemoved(key, userInitiated) } }
}
}
- @MainThread
- private fun processDevice(key: String, oldKey: String?, device: MediaDeviceData?) {
- listeners.forEach { it.onMediaDeviceChanged(key, oldKey, device) }
- }
-
interface Listener {
/** Called when the route has changed for a given notification. */
fun onMediaDeviceChanged(key: String, oldKey: String?, data: MediaDeviceData?)
@@ -260,7 +287,7 @@ constructor(
override fun onAboutToConnectDeviceAdded(
deviceAddress: String,
deviceName: String,
- deviceIcon: Drawable?
+ deviceIcon: Drawable?,
) {
aboutToConnectDeviceOverride =
AboutToConnectDevice(
@@ -270,8 +297,8 @@ constructor(
/* enabled */ enabled = true,
/* icon */ deviceIcon,
/* name */ deviceName,
- /* showBroadcastButton */ showBroadcastButton = false
- )
+ /* showBroadcastButton */ showBroadcastButton = false,
+ ),
)
updateCurrent()
}
@@ -292,7 +319,7 @@ constructor(
override fun onBroadcastMetadataChanged(
broadcastId: Int,
- metadata: BluetoothLeBroadcastMetadata
+ metadata: BluetoothLeBroadcastMetadata,
) {
logger.logBroadcastMetadataChanged(broadcastId, metadata.toString())
updateCurrent()
@@ -352,14 +379,14 @@ constructor(
// route.
connectedDevice?.copy(
name = it.name ?: connectedDevice.name,
- icon = icon
+ icon = icon,
)
}
?: MediaDeviceData(
enabled = false,
icon = MediaControlDrawables.getHomeDevices(context),
name = context.getString(R.string.media_seamless_other_device),
- showBroadcastButton = false
+ showBroadcastButton = false,
)
logger.logRemoteDevice(routingSession?.name, connectedDevice)
} else {
@@ -398,7 +425,7 @@ constructor(
device?.iconWithoutBackground,
name,
id = device?.id,
- showBroadcastButton = false
+ showBroadcastButton = false,
)
}
}
@@ -415,7 +442,7 @@ constructor(
icon = iconWithoutBackground,
name = name,
id = id,
- showBroadcastButton = false
+ showBroadcastButton = false,
)
private fun getLeAudioBroadcastDeviceData(): MediaDeviceData {
@@ -425,7 +452,7 @@ constructor(
icon = MediaControlDrawables.getLeAudioSharing(context),
name = context.getString(R.string.audio_sharing_description),
intent = null,
- showBroadcastButton = false
+ showBroadcastButton = false,
)
} else {
MediaDeviceData(
@@ -433,7 +460,7 @@ constructor(
icon = MediaControlDrawables.getAntenna(context),
name = broadcastDescription,
intent = null,
- showBroadcastButton = true
+ showBroadcastButton = true,
)
}
}
@@ -449,7 +476,7 @@ constructor(
device,
controller,
routingSession?.name,
- selectedRoutes?.firstOrNull()?.name
+ selectedRoutes?.firstOrNull()?.name,
)
if (controller == null) {
@@ -514,7 +541,7 @@ constructor(
MediaDataUtils.getAppLabel(
context,
localMediaManager.packageName,
- context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name)
+ context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name),
)
val isCurrentBroadcastedApp = TextUtils.equals(mediaApp, currentBroadcastedApp)
if (isCurrentBroadcastedApp) {
@@ -538,5 +565,5 @@ constructor(
*/
private data class AboutToConnectDevice(
val fullMediaDevice: MediaDevice? = null,
- val backupMediaDeviceData: MediaDeviceData? = null
+ val backupMediaDeviceData: MediaDeviceData? = null,
)
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt
index 684a560b0502..be4e6cc59f76 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt
@@ -25,12 +25,12 @@ import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.media.NotificationMediaManager.isPlayingState
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
import com.android.systemui.media.controls.util.MediaControllerFactory
import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.time.SystemClock
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt
index a1f0cc33c065..8744c5c9a838 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt
@@ -39,8 +39,8 @@ import androidx.lifecycle.MutableLiveData
import com.android.systemui.Flags
import com.android.systemui.classifier.Classifier.MEDIA_SEEKBAR
import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.media.NotificationMediaManager
import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.statusbar.NotificationMediaManager
import com.android.systemui.util.concurrency.RepeatableExecutor
import java.util.Locale
import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java
index c58ba377fb68..ac1672db9375 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java
@@ -351,8 +351,9 @@ public abstract class MediaOutputAdapterBase extends RecyclerView.Adapter<Recycl
@VisibleForTesting
void showCustomEndSessionDialog(MediaDevice device) {
MediaSessionReleaseDialog mediaSessionReleaseDialog = new MediaSessionReleaseDialog(
- mContext, () -> transferOutput(device), mController.getColorButtonBackground(),
- mController.getColorItemContent());
+ mContext, () -> transferOutput(device),
+ mController.getColorSchemeLegacy().getColorButtonBackground(),
+ mController.getColorSchemeLegacy().getColorItemContent());
mediaSessionReleaseDialog.show();
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java
index 300a3578bb8f..6ab4a52dc919 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java
@@ -50,9 +50,11 @@ import androidx.recyclerview.widget.RecyclerView;
import com.android.media.flags.Flags;
import com.android.settingslib.media.InputMediaDevice;
import com.android.settingslib.media.MediaDevice;
-import com.android.settingslib.utils.ThreadUtils;
+import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.res.R;
+import java.util.concurrent.Executor;
/**
* A RecyclerView adapter for the legacy UI media output dialog device list.
*/
@@ -61,14 +63,20 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final int UNMUTE_DEFAULT_VOLUME = 2;
- private static final float DEVICE_DISABLED_ALPHA = 0.5f;
- private static final float DEVICE_ACTIVE_ALPHA = 1f;
+ @VisibleForTesting static final float DEVICE_DISABLED_ALPHA = 0.5f;
+ @VisibleForTesting static final float DEVICE_ACTIVE_ALPHA = 1f;
+ private final Executor mMainExecutor;
+ private final Executor mBackgroundExecutor;
View mHolderView;
- private boolean mIsInitVolumeFirstTime;
- public MediaOutputAdapterLegacy(MediaSwitchingController controller) {
+ public MediaOutputAdapterLegacy(
+ MediaSwitchingController controller,
+ @Main Executor mainExecutor,
+ @Background Executor backgroundExecutor
+ ) {
super(controller);
- mIsInitVolumeFirstTime = true;
+ mMainExecutor = mainExecutor;
+ mBackgroundExecutor = backgroundExecutor;
}
@Override
@@ -181,9 +189,9 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
mEndTouchArea.setVisibility(View.GONE);
mEndClickIcon.setVisibility(View.GONE);
mContainerLayout.setOnClickListener(null);
- mTitleText.setTextColor(mController.getColorItemContent());
- mSubTitleText.setTextColor(mController.getColorItemContent());
- mVolumeValueText.setTextColor(mController.getColorItemContent());
+ mTitleText.setTextColor(mController.getColorSchemeLegacy().getColorItemContent());
+ mSubTitleText.setTextColor(mController.getColorSchemeLegacy().getColorItemContent());
+ mVolumeValueText.setTextColor(mController.getColorSchemeLegacy().getColorItemContent());
mIconAreaLayout.setBackground(null);
updateIconAreaClickListener(null);
updateSeekBarProgressColor();
@@ -193,14 +201,14 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
/** Binds a ViewHolder for a "Connect a device" item. */
void onBindPairNewDevice() {
- mTitleText.setTextColor(mController.getColorItemContent());
+ mTitleText.setTextColor(mController.getColorSchemeLegacy().getColorItemContent());
mCheckBox.setVisibility(View.GONE);
updateTitle(mContext.getText(R.string.media_output_dialog_pairing_new));
updateItemBackground(ConnectionState.DISCONNECTED);
final Drawable addDrawable = mContext.getDrawable(R.drawable.ic_add);
mTitleIcon.setImageDrawable(addDrawable);
- mTitleIcon.setImageTintList(
- ColorStateList.valueOf(mController.getColorItemContent()));
+ mTitleIcon.setImageTintList(ColorStateList.valueOf(
+ mController.getColorSchemeLegacy().getColorItemContent()));
mContainerLayout.setOnClickListener(mController::launchBluetoothPairing);
}
@@ -251,10 +259,9 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
updateSeekbarProgressBackground();
}
}
- boolean isCurrentSeekbarInvisible = mSeekBar.getVisibility() == View.GONE;
mSeekBar.setVisibility(showSeekBar ? View.VISIBLE : View.GONE);
if (showSeekBar) {
- initSeekbar(device, isCurrentSeekbarInvisible);
+ initSeekbar(device);
updateContainerContentA11yImportance(false /* isImportant */);
mSeekBar.setContentDescription(contentDescription);
} else {
@@ -264,9 +271,8 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
void updateGroupSeekBar(String contentDescription) {
updateSeekbarProgressBackground();
- boolean isCurrentSeekbarInvisible = mSeekBar.getVisibility() == View.GONE;
mSeekBar.setVisibility(View.VISIBLE);
- initGroupSeekbar(isCurrentSeekbarInvisible);
+ initGroupSeekbar();
updateContainerContentA11yImportance(false /* isImportant */);
mSeekBar.setContentDescription(contentDescription);
}
@@ -297,8 +303,8 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
protected void updateLoadingIndicator(ConnectionState connectionState) {
if (connectionState == ConnectionState.CONNECTING) {
mProgressBar.setVisibility(View.VISIBLE);
- mProgressBar.getIndeterminateDrawable().setTintList(
- ColorStateList.valueOf(mController.getColorItemContent()));
+ mProgressBar.getIndeterminateDrawable().setTintList(ColorStateList.valueOf(
+ mController.getColorSchemeLegacy().getColorItemContent()));
} else {
mProgressBar.setVisibility(View.GONE);
}
@@ -318,8 +324,8 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
// Connected or connecting state has a darker background.
int backgroundColor = isConnected || isConnecting
- ? mController.getColorConnectedItemBackground()
- : mController.getColorItemBackground();
+ ? mController.getColorSchemeLegacy().getColorConnectedItemBackground()
+ : mController.getColorSchemeLegacy().getColorItemBackground();
mItemLayout.setBackgroundTintList(ColorStateList.valueOf(backgroundColor));
}
@@ -332,13 +338,13 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
}
private void updateSeekBarProgressColor() {
- mSeekBar.setProgressTintList(
- ColorStateList.valueOf(mController.getColorSeekbarProgress()));
+ mSeekBar.setProgressTintList(ColorStateList.valueOf(
+ mController.getColorSchemeLegacy().getColorSeekbarProgress()));
final Drawable contrastDotDrawable =
((LayerDrawable) mSeekBar.getProgressDrawable()).findDrawableByLayerId(
R.id.contrast_dot);
- contrastDotDrawable.setTintList(
- ColorStateList.valueOf(mController.getColorItemContent()));
+ contrastDotDrawable.setTintList(ColorStateList.valueOf(
+ mController.getColorSchemeLegacy().getColorItemContent()));
}
void updateSeekbarProgressBackground() {
@@ -354,31 +360,21 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
mActiveRadius, 0, 0});
}
- private void initializeSeekbarVolume(
- @Nullable MediaDevice device, int currentVolume,
- boolean isCurrentSeekbarInvisible) {
+ private void initializeSeekbarVolume(@Nullable MediaDevice device, int currentVolume) {
if (!isDragging()) {
if (mSeekBar.getVolume() != currentVolume && (mLatestUpdateVolume == -1
|| currentVolume == mLatestUpdateVolume)) {
// Update only if volume of device and value of volume bar doesn't match.
// Check if response volume match with the latest request, to ignore obsolete
// response
- if (isCurrentSeekbarInvisible && !mIsInitVolumeFirstTime) {
+ if (!mVolumeAnimator.isStarted()) {
if (currentVolume == 0) {
updateMutedVolumeIcon(device);
} else {
updateUnmutedVolumeIcon(device);
}
- } else {
- if (!mVolumeAnimator.isStarted()) {
- if (currentVolume == 0) {
- updateMutedVolumeIcon(device);
- } else {
- updateUnmutedVolumeIcon(device);
- }
- mSeekBar.setVolume(currentVolume);
- mLatestUpdateVolume = -1;
- }
+ mSeekBar.setVolume(currentVolume);
+ mLatestUpdateVolume = -1;
}
} else if (currentVolume == 0) {
mSeekBar.resetVolume();
@@ -388,12 +384,9 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
mLatestUpdateVolume = -1;
}
}
- if (mIsInitVolumeFirstTime) {
- mIsInitVolumeFirstTime = false;
- }
}
- void initSeekbar(@NonNull MediaDevice device, boolean isCurrentSeekbarInvisible) {
+ void initSeekbar(@NonNull MediaDevice device) {
SeekBarVolumeControl volumeControl = new SeekBarVolumeControl() {
@Override
public int getVolume() {
@@ -422,7 +415,7 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
}
mSeekBar.setMaxVolume(device.getMaxVolume());
final int currentVolume = device.getCurrentVolume();
- initializeSeekbarVolume(device, currentVolume, isCurrentSeekbarInvisible);
+ initializeSeekbarVolume(device, currentVolume);
mSeekBar.setOnSeekBarChangeListener(new MediaSeekBarChangedListener(
device, volumeControl) {
@@ -435,7 +428,7 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
}
// Initializes the seekbar for a group of devices.
- void initGroupSeekbar(boolean isCurrentSeekbarInvisible) {
+ void initGroupSeekbar() {
SeekBarVolumeControl volumeControl = new SeekBarVolumeControl() {
@Override
public int getVolume() {
@@ -462,7 +455,7 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
mSeekBar.setMaxVolume(mController.getSessionVolumeMax());
final int currentVolume = mController.getSessionVolume();
- initializeSeekbarVolume(null, currentVolume, isCurrentSeekbarInvisible);
+ initializeSeekbarVolume(null, currentVolume);
mSeekBar.setOnSeekBarChangeListener(new MediaSeekBarChangedListener(
null, volumeControl) {
@Override
@@ -503,9 +496,10 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
boolean isInputMediaDevice = device instanceof InputMediaDevice;
int id = getDrawableId(isInputMediaDevice, isMutedVolumeIcon);
mTitleIcon.setImageDrawable(mContext.getDrawable(id));
- mTitleIcon.setImageTintList(ColorStateList.valueOf(mController.getColorItemContent()));
- mIconAreaLayout.setBackgroundTintList(
- ColorStateList.valueOf(mController.getColorSeekbarProgress()));
+ mTitleIcon.setImageTintList(ColorStateList.valueOf(
+ mController.getColorSchemeLegacy().getColorItemContent()));
+ mIconAreaLayout.setBackgroundTintList(ColorStateList.valueOf(
+ mController.getColorSchemeLegacy().getColorSeekbarProgress()));
}
@VisibleForTesting
@@ -534,8 +528,8 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
mStatusIcon.setVisibility(View.GONE);
} else {
mStatusIcon.setImageDrawable(deviceStatusIcon);
- mStatusIcon.setImageTintList(
- ColorStateList.valueOf(mController.getColorItemContent()));
+ mStatusIcon.setImageTintList(ColorStateList.valueOf(
+ mController.getColorSchemeLegacy().getColorItemContent()));
if (deviceStatusIcon instanceof AnimatedVectorDrawable) {
((AnimatedVectorDrawable) deviceStatusIcon).start();
}
@@ -585,9 +579,10 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
private void updateEndAreaWithIcon(View.OnClickListener clickListener,
@DrawableRes int iconDrawableId,
@StringRes int accessibilityStringId) {
- updateEndAreaColor(mController.getColorSeekbarProgress());
+ updateEndAreaColor(mController.getColorSchemeLegacy().getColorSeekbarProgress());
mEndClickIcon.setImageTintList(
- ColorStateList.valueOf(mController.getColorItemContent()));
+ ColorStateList.valueOf(
+ mController.getColorSchemeLegacy().getColorItemContent()));
mEndClickIcon.setOnClickListener(clickListener);
Drawable drawable = mContext.getDrawable(iconDrawableId);
mEndClickIcon.setImageDrawable(drawable);
@@ -600,8 +595,9 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
private void updateEndAreaForGroupCheckBox(@NonNull MediaDevice device,
@NonNull GroupStatus groupStatus) {
boolean isEnabled = isGroupCheckboxEnabled(groupStatus);
- updateEndAreaColor(groupStatus.selected() ? mController.getColorSeekbarProgress()
- : mController.getColorItemBackground());
+ updateEndAreaColor(groupStatus.selected()
+ ? mController.getColorSchemeLegacy().getColorSeekbarProgress()
+ : mController.getColorSchemeLegacy().getColorItemBackground());
mCheckBox.setContentDescription(mContext.getString(
groupStatus.selected() ? R.string.accessibility_remove_device_from_group
: R.string.accessibility_add_device_to_group));
@@ -611,7 +607,7 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
isEnabled ? (buttonView, isChecked) -> onGroupActionTriggered(
!groupStatus.selected(), device) : null);
mCheckBox.setEnabled(isEnabled);
- setCheckBoxColor(mCheckBox, mController.getColorItemContent());
+ setCheckBoxColor(mCheckBox, mController.getColorSchemeLegacy().getColorItemContent());
}
private void setCheckBoxColor(CheckBox checkBox, int color) {
@@ -714,15 +710,15 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
}
protected void setUpDeviceIcon(@NonNull MediaDevice device) {
- ThreadUtils.postOnBackgroundThread(() -> {
+ mBackgroundExecutor.execute(() -> {
Icon icon = mController.getDeviceIconCompat(device).toIcon(mContext);
- ThreadUtils.postOnMainThread(() -> {
+ mMainExecutor.execute(() -> {
if (!TextUtils.equals(mDeviceId, device.getId())) {
return;
}
mTitleIcon.setImageIcon(icon);
- mTitleIcon.setImageTintList(
- ColorStateList.valueOf(mController.getColorItemContent()));
+ mTitleIcon.setImageTintList(ColorStateList.valueOf(
+ mController.getColorSchemeLegacy().getColorItemContent()));
});
});
}
@@ -807,7 +803,7 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
}
void onBind(String groupDividerTitle) {
- mTitleText.setTextColor(mController.getColorItemContent());
+ mTitleText.setTextColor(mController.getColorSchemeLegacy().getColorItemContent());
mTitleText.setText(groupDividerTitle);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
index d791361d555f..49d09cf64c8e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -40,7 +40,6 @@ import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowManager;
@@ -93,13 +92,10 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
private ImageView mAppResourceIcon;
private ImageView mBroadcastIcon;
private RecyclerView mDevicesRecyclerView;
- private LinearLayout mDeviceListLayout;
+ private ViewGroup mDeviceListLayout;
private LinearLayout mMediaMetadataSectionLayout;
private Button mDoneButton;
private Button mStopButton;
- private int mListMaxHeight;
- private int mItemHeight;
- private int mListPaddingTop;
private WallpaperColors mWallpaperColors;
private boolean mShouldLaunchLeBroadcastDialog;
private boolean mIsLeBroadcastCallbackRegistered;
@@ -109,17 +105,6 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
protected Executor mExecutor;
- private final ViewTreeObserver.OnGlobalLayoutListener mDeviceListLayoutListener = () -> {
- ViewGroup.LayoutParams params = mDeviceListLayout.getLayoutParams();
- int totalItemsHeight = mAdapter.getItemCount() * mItemHeight
- + mListPaddingTop;
- int correctHeight = Math.min(totalItemsHeight, mListMaxHeight);
- // Set max height for list
- if (correctHeight != params.height) {
- params.height = correctHeight;
- mDeviceListLayout.setLayoutParams(params);
- }
- };
private final BluetoothLeBroadcast.Callback mBroadcastCallback =
new BluetoothLeBroadcast.Callback() {
@@ -220,12 +205,6 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
mBroadcastSender = broadcastSender;
mMediaSwitchingController = mediaSwitchingController;
mLayoutManager = new LayoutManagerWrapper(mContext);
- mListMaxHeight = context.getResources().getDimensionPixelSize(
- R.dimen.media_output_dialog_list_max_height);
- mItemHeight = context.getResources().getDimensionPixelSize(
- R.dimen.media_output_dialog_list_item_height);
- mListPaddingTop = mContext.getResources().getDimensionPixelSize(
- R.dimen.media_output_dialog_list_padding_top);
mExecutor = Executors.newSingleThreadExecutor();
mIncludePlaybackAndAppMetadata = includePlaybackAndAppMetadata;
}
@@ -258,8 +237,6 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
mAppResourceIcon = mDialogView.requireViewById(R.id.app_source_icon);
mBroadcastIcon = mDialogView.requireViewById(R.id.broadcast_icon);
- mDeviceListLayout.getViewTreeObserver().addOnGlobalLayoutListener(
- mDeviceListLayoutListener);
// Init device list
mLayoutManager.setAutoMeasureEnabled(true);
mDevicesRecyclerView.setLayoutManager(mLayoutManager);
@@ -342,7 +319,8 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
WallpaperColors wallpaperColors = WallpaperColors.fromBitmap(icon.getBitmap());
colorSetUpdated = !wallpaperColors.equals(mWallpaperColors);
if (colorSetUpdated) {
- mMediaSwitchingController.setCurrentColorScheme(wallpaperColors, isDarkThemeOn);
+ mMediaSwitchingController.updateCurrentColorScheme(wallpaperColors,
+ isDarkThemeOn);
updateButtonBackgroundColorFilter();
updateDialogBackgroundColor();
}
@@ -359,7 +337,8 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
mAppResourceIcon.setVisibility(View.GONE);
} else if (appSourceIcon != null) {
Icon appIcon = appSourceIcon.toIcon(mContext);
- mAppResourceIcon.setColorFilter(mMediaSwitchingController.getColorItemContent());
+ mAppResourceIcon.setColorFilter(
+ mMediaSwitchingController.getColorSchemeLegacy().getColorItemContent());
mAppResourceIcon.setImageIcon(appIcon);
} else {
Drawable appIconDrawable = mMediaSwitchingController.getAppSourceIconFromPackage();
@@ -369,12 +348,6 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
mAppResourceIcon.setVisibility(View.GONE);
}
}
- if (mHeaderIcon.getVisibility() == View.VISIBLE) {
- final int size = getHeaderIconSize();
- final int padding = mContext.getResources().getDimensionPixelSize(
- R.dimen.media_output_dialog_header_icon_padding);
- mHeaderIcon.setLayoutParams(new LinearLayout.LayoutParams(size + padding, size));
- }
if (!mIncludePlaybackAndAppMetadata) {
mHeaderTitle.setVisibility(View.GONE);
@@ -419,18 +392,19 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
private void updateButtonBackgroundColorFilter() {
ColorFilter buttonColorFilter =
new PorterDuffColorFilter(
- mMediaSwitchingController.getColorButtonBackground(),
+ mMediaSwitchingController.getColorSchemeLegacy().getColorButtonBackground(),
PorterDuff.Mode.SRC_IN);
mDoneButton.getBackground().setColorFilter(buttonColorFilter);
mStopButton.getBackground().setColorFilter(buttonColorFilter);
- mDoneButton.setTextColor(mMediaSwitchingController.getColorPositiveButtonText());
+ mDoneButton.setTextColor(
+ mMediaSwitchingController.getColorSchemeLegacy().getColorPositiveButtonText());
}
private void updateDialogBackgroundColor() {
- getDialogView()
- .getBackground()
- .setTint(mMediaSwitchingController.getColorDialogBackground());
- mDeviceListLayout.setBackgroundColor(mMediaSwitchingController.getColorDialogBackground());
+ getDialogView().getBackground().setTint(
+ mMediaSwitchingController.getColorSchemeLegacy().getColorDialogBackground());
+ mDeviceListLayout.setBackgroundColor(
+ mMediaSwitchingController.getColorSchemeLegacy().getColorDialogBackground());
}
public void handleLeBroadcastStarted() {
@@ -520,8 +494,6 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
abstract IconCompat getHeaderIcon();
- abstract int getHeaderIconSize();
-
abstract CharSequence getHeaderText();
abstract CharSequence getHeaderSubtitle();
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
index 9ade9e275ca1..791a61cc73ec 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
@@ -52,6 +52,8 @@ import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.google.zxing.WriterException;
+import java.util.concurrent.Executor;
+
/**
* Dialog for media output broadcast.
*/
@@ -239,13 +241,16 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog {
Context context,
boolean aboveStatusbar,
BroadcastSender broadcastSender,
- MediaSwitchingController mediaSwitchingController) {
+ MediaSwitchingController mediaSwitchingController,
+ Executor mainExecutor,
+ Executor backgroundExecutor) {
super(
context,
broadcastSender,
mediaSwitchingController, /* includePlaybackAndAppMetadata */
true);
- mAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
+ mAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mainExecutor,
+ backgroundExecutor);
// TODO(b/226710953): Move the part to MediaOutputBaseDialog for every class
// that extends MediaOutputBaseDialog
if (!aboveStatusbar) {
@@ -295,12 +300,6 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog {
}
@Override
- int getHeaderIconSize() {
- return mContext.getResources().getDimensionPixelSize(
- R.dimen.media_output_dialog_header_album_icon_size);
- }
-
- @Override
CharSequence getHeaderText() {
return mMediaSwitchingController.getHeaderTitle();
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt
index 2e7e66f5b384..81c85a6ad22d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt
@@ -20,6 +20,9 @@ import android.content.Context
import android.view.View
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.broadcast.BroadcastSender
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import java.util.concurrent.Executor
import javax.inject.Inject
/** Manager to create and show a [MediaOutputBroadcastDialog]. */
@@ -29,7 +32,9 @@ constructor(
private val context: Context,
private val broadcastSender: BroadcastSender,
private val dialogTransitionAnimator: DialogTransitionAnimator,
- private val mediaSwitchingControllerFactory: MediaSwitchingController.Factory
+ private val mediaSwitchingControllerFactory: MediaSwitchingController.Factory,
+ @Main private val mainExecutor: Executor,
+ @Background private val backgroundExecutor: Executor,
) {
var mediaOutputBroadcastDialog: MediaOutputBroadcastDialog? = null
@@ -47,7 +52,14 @@ constructor(
/* token */ null,
)
val dialog =
- MediaOutputBroadcastDialog(context, aboveStatusBar, broadcastSender, controller)
+ MediaOutputBroadcastDialog(
+ context,
+ aboveStatusBar,
+ broadcastSender,
+ controller,
+ mainExecutor,
+ backgroundExecutor,
+ )
mediaOutputBroadcastDialog = dialog
// Show the dialog.
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputColorSchemeLegacy.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputColorSchemeLegacy.kt
new file mode 100644
index 000000000000..7f0fa463811b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputColorSchemeLegacy.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.dialog
+
+import android.content.Context
+import com.android.settingslib.Utils
+import com.android.systemui.monet.ColorScheme
+import com.android.systemui.res.R
+
+abstract class MediaOutputColorSchemeLegacy {
+ companion object Factory {
+ @JvmStatic
+ fun fromSystemColors(context: Context): MediaOutputColorSchemeLegacy {
+ return MediaOutputColorSchemeLegacySystem(context)
+ }
+
+ @JvmStatic
+ fun fromDynamicColors(
+ colorScheme: ColorScheme,
+ isDarkTheme: Boolean,
+ ): MediaOutputColorSchemeLegacy {
+ return MediaOutputColorSchemeLegacyDynamic(colorScheme, isDarkTheme)
+ }
+ }
+
+ abstract fun getColorConnectedItemBackground(): Int
+
+ abstract fun getColorPositiveButtonText(): Int
+
+ abstract fun getColorDialogBackground(): Int
+
+ abstract fun getColorItemContent(): Int
+
+ abstract fun getColorSeekbarProgress(): Int
+
+ abstract fun getColorButtonBackground(): Int
+
+ abstract fun getColorItemBackground(): Int
+}
+
+class MediaOutputColorSchemeLegacySystem(private val mContext: Context) :
+ MediaOutputColorSchemeLegacy() {
+
+ override fun getColorConnectedItemBackground() =
+ Utils.getColorStateListDefaultColor(
+ mContext,
+ R.color.media_dialog_connected_item_background,
+ )
+
+ override fun getColorPositiveButtonText() =
+ Utils.getColorStateListDefaultColor(mContext, R.color.media_dialog_solid_button_text)
+
+ override fun getColorDialogBackground() =
+ Utils.getColorStateListDefaultColor(mContext, R.color.media_dialog_background)
+
+ override fun getColorItemContent() =
+ Utils.getColorStateListDefaultColor(mContext, R.color.media_dialog_item_main_content)
+
+ override fun getColorSeekbarProgress() =
+ Utils.getColorStateListDefaultColor(mContext, R.color.media_dialog_seekbar_progress)
+
+ override fun getColorButtonBackground() =
+ Utils.getColorStateListDefaultColor(mContext, R.color.media_dialog_button_background)
+
+ override fun getColorItemBackground() =
+ Utils.getColorStateListDefaultColor(mContext, R.color.media_dialog_item_background)
+}
+
+class MediaOutputColorSchemeLegacyDynamic(colorScheme: ColorScheme, isDarkTheme: Boolean) :
+ MediaOutputColorSchemeLegacy() {
+ private var mColorItemContent: Int
+ private var mColorSeekbarProgress: Int
+ private var mColorButtonBackground: Int
+ private var mColorItemBackground: Int
+ private var mColorConnectedItemBackground: Int
+ private var mColorPositiveButtonText: Int
+ private var mColorDialogBackground: Int
+
+ init {
+ if (isDarkTheme) {
+ mColorItemContent = colorScheme.accent1.s100 // A1-100
+ mColorSeekbarProgress = colorScheme.accent2.s600 // A2-600
+ mColorButtonBackground = colorScheme.accent1.s300 // A1-300
+ mColorItemBackground = colorScheme.neutral2.s800 // N2-800
+ mColorConnectedItemBackground = colorScheme.accent2.s800 // A2-800
+ mColorPositiveButtonText = colorScheme.accent2.s800 // A2-800
+ mColorDialogBackground = colorScheme.neutral1.s900 // N1-900
+ } else {
+ mColorItemContent = colorScheme.accent1.s800 // A1-800
+ mColorSeekbarProgress = colorScheme.accent1.s300 // A1-300
+ mColorButtonBackground = colorScheme.accent1.s600 // A1-600
+ mColorItemBackground = colorScheme.accent2.s50 // A2-50
+ mColorConnectedItemBackground = colorScheme.accent1.s100 // A1-100
+ mColorPositiveButtonText = colorScheme.neutral1.s50 // N1-50
+ mColorDialogBackground = colorScheme.backgroundColor
+ }
+ }
+
+ override fun getColorConnectedItemBackground() = mColorConnectedItemBackground
+
+ override fun getColorPositiveButtonText() = mColorPositiveButtonText
+
+ override fun getColorDialogBackground() = mColorDialogBackground
+
+ override fun getColorItemContent() = mColorItemContent
+
+ override fun getColorSeekbarProgress() = mColorSeekbarProgress
+
+ override fun getColorButtonBackground() = mColorButtonBackground
+
+ override fun getColorItemBackground() = mColorItemBackground
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
index 2e602be4556e..163ff248b9df 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
@@ -34,6 +34,8 @@ import com.android.systemui.broadcast.BroadcastSender;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.res.R;
+import java.util.concurrent.Executor;
+
/**
* Dialog for media output transferring.
*/
@@ -49,11 +51,14 @@ public class MediaOutputDialog extends MediaOutputBaseDialog {
MediaSwitchingController mediaSwitchingController,
DialogTransitionAnimator dialogTransitionAnimator,
UiEventLogger uiEventLogger,
+ Executor mainExecutor,
+ Executor backgroundExecutor,
boolean includePlaybackAndAppMetadata) {
super(context, broadcastSender, mediaSwitchingController, includePlaybackAndAppMetadata);
mDialogTransitionAnimator = dialogTransitionAnimator;
mUiEventLogger = uiEventLogger;
- mAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
+ mAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mainExecutor,
+ backgroundExecutor);
if (!aboveStatusbar) {
getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
}
@@ -76,12 +81,6 @@ public class MediaOutputDialog extends MediaOutputBaseDialog {
}
@Override
- int getHeaderIconSize() {
- return mContext.getResources().getDimensionPixelSize(
- R.dimen.media_output_dialog_header_album_icon_size);
- }
-
- @Override
CharSequence getHeaderText() {
return mMediaSwitchingController.getHeaderTitle();
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt
index 4e9451a838ad..d3a81a53b6d3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt
@@ -25,6 +25,9 @@ import com.android.internal.logging.UiEventLogger
import com.android.systemui.animation.DialogCuj
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.broadcast.BroadcastSender
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import java.util.concurrent.Executor
import javax.inject.Inject
/** Manager to create and show a [MediaOutputDialog]. */
@@ -37,6 +40,9 @@ constructor(
private val dialogTransitionAnimator: DialogTransitionAnimator,
private val mediaSwitchingControllerFactory: MediaSwitchingController.Factory,
) {
+ @Inject @Main lateinit var mainExecutor: Executor
+ @Inject @Background lateinit var backgroundExecutor: Executor
+
companion object {
const val INTERACTION_JANK_TAG = "media_output"
var mediaOutputDialog: MediaOutputDialog? = null
@@ -51,7 +57,7 @@ constructor(
aboveStatusBar: Boolean,
view: View? = null,
userHandle: UserHandle? = null,
- token: MediaSession.Token? = null
+ token: MediaSession.Token? = null,
) {
createAndShowWithController(
packageName,
@@ -62,8 +68,8 @@ constructor(
it,
DialogCuj(
InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
- INTERACTION_JANK_TAG
- )
+ INTERACTION_JANK_TAG,
+ ),
)
},
userHandle = userHandle,
@@ -128,15 +134,14 @@ constructor(
controller,
dialogTransitionAnimator,
uiEventLogger,
- includePlaybackAndAppMetadata
+ mainExecutor,
+ backgroundExecutor,
+ includePlaybackAndAppMetadata,
)
// Show the dialog.
if (dialogTransitionAnimatorController != null) {
- dialogTransitionAnimator.show(
- mediaOutputDialog,
- dialogTransitionAnimatorController,
- )
+ dialogTransitionAnimator.show(mediaOutputDialog, dialogTransitionAnimatorController)
} else {
mediaOutputDialog.show()
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
index 9d375809786a..f79693138e24 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
@@ -83,6 +83,8 @@ import com.android.settingslib.utils.ThreadUtils;
import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.animation.DialogTransitionAnimator;
import com.android.systemui.broadcast.BroadcastSender;
+import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.media.dialog.MediaItem.MediaItemType;
import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
@@ -114,6 +116,8 @@ import java.util.concurrent.Executor;
import java.util.function.Function;
import java.util.stream.Collectors;
+import javax.inject.Inject;
+
/**
* Controller for a dialog that allows users to switch media output and input devices, control
* volume, connect to new devices, etc.
@@ -141,7 +145,7 @@ public class MediaSwitchingController
@VisibleForTesting
final List<MediaDevice> mGroupMediaDevices = new CopyOnWriteArrayList<>();
final List<MediaDevice> mCachedMediaDevices = new CopyOnWriteArrayList<>();
- private final List<MediaItem> mOutputMediaItemList = new CopyOnWriteArrayList<>();
+ private final OutputMediaItemListProxy mOutputMediaItemListProxy;
private final List<MediaItem> mInputMediaItemList = new CopyOnWriteArrayList<>();
private final AudioManager mAudioManager;
private final PowerExemptionManager mPowerExemptionManager;
@@ -149,7 +153,8 @@ public class MediaSwitchingController
private final NearbyMediaDevicesManager mNearbyMediaDevicesManager;
private final Map<String, Integer> mNearbyDeviceInfoMap = new ConcurrentHashMap<>();
private final MediaSession.Token mToken;
-
+ @Inject @Main Executor mMainExecutor;
+ @Inject @Background Executor mBackgroundExecutor;
@VisibleForTesting
boolean mIsRefreshing = false;
@VisibleForTesting
@@ -163,17 +168,10 @@ public class MediaSwitchingController
@VisibleForTesting
MediaOutputMetricLogger mMetricLogger;
private int mCurrentState;
-
- private int mColorItemContent;
- private int mColorSeekbarProgress;
- private int mColorButtonBackground;
- private int mColorItemBackground;
- private int mColorConnectedItemBackground;
- private int mColorPositiveButtonText;
- private int mColorDialogBackground;
private FeatureFlags mFeatureFlags;
private UserTracker mUserTracker;
private VolumePanelGlobalStateInteractor mVolumePanelGlobalStateInteractor;
+ @NonNull private MediaOutputColorSchemeLegacy mMediaOutputColorSchemeLegacy;
public enum BroadcastNotifyDialog {
ACTION_FIRST_LAUNCH,
@@ -228,22 +226,10 @@ public class MediaSwitchingController
InfoMediaManager.createInstance(mContext, packageName, userHandle, lbm, token);
mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName);
mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName);
+ mOutputMediaItemListProxy = new OutputMediaItemListProxy();
mDialogTransitionAnimator = dialogTransitionAnimator;
mNearbyMediaDevicesManager = nearbyMediaDevicesManager;
- mColorItemContent = Utils.getColorStateListDefaultColor(mContext,
- R.color.media_dialog_item_main_content);
- mColorSeekbarProgress = Utils.getColorStateListDefaultColor(mContext,
- R.color.media_dialog_seekbar_progress);
- mColorButtonBackground = Utils.getColorStateListDefaultColor(mContext,
- R.color.media_dialog_button_background);
- mColorItemBackground = Utils.getColorStateListDefaultColor(mContext,
- R.color.media_dialog_item_background);
- mColorConnectedItemBackground = Utils.getColorStateListDefaultColor(mContext,
- R.color.media_dialog_connected_item_background);
- mColorPositiveButtonText = Utils.getColorStateListDefaultColor(mContext,
- R.color.media_dialog_solid_button_text);
- mColorDialogBackground = Utils.getColorStateListDefaultColor(mContext,
- R.color.media_dialog_background);
+ mMediaOutputColorSchemeLegacy = MediaOutputColorSchemeLegacy.fromSystemColors(mContext);
if (enableInputRouting()) {
mInputRouteManager = new InputRouteManager(mContext, audioManager);
@@ -260,7 +246,7 @@ public class MediaSwitchingController
protected void start(@NonNull Callback cb) {
synchronized (mMediaDevicesLock) {
mCachedMediaDevices.clear();
- mOutputMediaItemList.clear();
+ mOutputMediaItemListProxy.clear();
}
mNearbyDeviceInfoMap.clear();
if (mNearbyMediaDevicesManager != null) {
@@ -306,7 +292,7 @@ public class MediaSwitchingController
mLocalMediaManager.stopScan();
synchronized (mMediaDevicesLock) {
mCachedMediaDevices.clear();
- mOutputMediaItemList.clear();
+ mOutputMediaItemListProxy.clear();
}
if (mNearbyMediaDevicesManager != null) {
mNearbyMediaDevicesManager.unregisterNearbyDevicesCallback(this);
@@ -348,7 +334,7 @@ public class MediaSwitchingController
@Override
public void onDeviceListUpdate(List<MediaDevice> devices) {
- boolean isListEmpty = mOutputMediaItemList.isEmpty();
+ boolean isListEmpty = mOutputMediaItemListProxy.isEmpty();
if (isListEmpty || !mIsRefreshing) {
buildMediaItems(devices);
mCallback.onDeviceListChanged();
@@ -362,11 +348,12 @@ public class MediaSwitchingController
}
@Override
- public void onSelectedDeviceStateChanged(MediaDevice device,
- @LocalMediaManager.MediaDeviceState int state) {
+ public void onSelectedDeviceStateChanged(
+ MediaDevice device, @LocalMediaManager.MediaDeviceState int state) {
mCallback.onRouteChanged();
mMetricLogger.logOutputItemSuccess(
- device.toString(), new ArrayList<>(mOutputMediaItemList));
+ device.toString(),
+ new ArrayList<>(mOutputMediaItemListProxy.getOutputMediaItemList()));
}
@Override
@@ -377,7 +364,8 @@ public class MediaSwitchingController
@Override
public void onRequestFailed(int reason) {
mCallback.onRouteChanged();
- mMetricLogger.logOutputItemFailure(new ArrayList<>(mOutputMediaItemList), reason);
+ mMetricLogger.logOutputItemFailure(
+ new ArrayList<>(mOutputMediaItemListProxy.getOutputMediaItemList()), reason);
}
/**
@@ -396,7 +384,7 @@ public class MediaSwitchingController
}
try {
synchronized (mMediaDevicesLock) {
- mOutputMediaItemList.removeIf((MediaItem::isMutingExpectedDevice));
+ mOutputMediaItemListProxy.removeMutingExpectedDevices();
}
mAudioManager.cancelMuteAwaitConnection(mAudioManager.getMutingExpectedDevice());
} catch (Exception e) {
@@ -568,26 +556,15 @@ public class MediaSwitchingController
return null;
}
- void setCurrentColorScheme(WallpaperColors wallpaperColors, boolean isDarkTheme) {
- ColorScheme mCurrentColorScheme = new ColorScheme(wallpaperColors,
+ void updateCurrentColorScheme(WallpaperColors wallpaperColors, boolean isDarkTheme) {
+ ColorScheme currentColorScheme = new ColorScheme(wallpaperColors,
isDarkTheme);
- if (isDarkTheme) {
- mColorItemContent = mCurrentColorScheme.getAccent1().getS100(); // A1-100
- mColorSeekbarProgress = mCurrentColorScheme.getAccent2().getS600(); // A2-600
- mColorButtonBackground = mCurrentColorScheme.getAccent1().getS300(); // A1-300
- mColorItemBackground = mCurrentColorScheme.getNeutral2().getS800(); // N2-800
- mColorConnectedItemBackground = mCurrentColorScheme.getAccent2().getS800(); // A2-800
- mColorPositiveButtonText = mCurrentColorScheme.getAccent2().getS800(); // A2-800
- mColorDialogBackground = mCurrentColorScheme.getNeutral1().getS900(); // N1-900
- } else {
- mColorItemContent = mCurrentColorScheme.getAccent1().getS800(); // A1-800
- mColorSeekbarProgress = mCurrentColorScheme.getAccent1().getS300(); // A1-300
- mColorButtonBackground = mCurrentColorScheme.getAccent1().getS600(); // A1-600
- mColorItemBackground = mCurrentColorScheme.getAccent2().getS50(); // A2-50
- mColorConnectedItemBackground = mCurrentColorScheme.getAccent1().getS100(); // A1-100
- mColorPositiveButtonText = mCurrentColorScheme.getNeutral1().getS50(); // N1-50
- mColorDialogBackground = mCurrentColorScheme.getBackgroundColor();
- }
+ mMediaOutputColorSchemeLegacy = MediaOutputColorSchemeLegacy.fromDynamicColors(
+ currentColorScheme, isDarkTheme);
+ }
+
+ MediaOutputColorSchemeLegacy getColorSchemeLegacy() {
+ return mMediaOutputColorSchemeLegacy;
}
public void refreshDataSetIfNeeded() {
@@ -598,44 +575,16 @@ public class MediaSwitchingController
}
}
- public int getColorConnectedItemBackground() {
- return mColorConnectedItemBackground;
- }
-
- public int getColorPositiveButtonText() {
- return mColorPositiveButtonText;
- }
-
- public int getColorDialogBackground() {
- return mColorDialogBackground;
- }
-
- public int getColorItemContent() {
- return mColorItemContent;
- }
-
- public int getColorSeekbarProgress() {
- return mColorSeekbarProgress;
- }
-
- public int getColorButtonBackground() {
- return mColorButtonBackground;
- }
-
- public int getColorItemBackground() {
- return mColorItemBackground;
- }
-
private void buildMediaItems(List<MediaDevice> devices) {
synchronized (mMediaDevicesLock) {
- List<MediaItem> updatedMediaItems = buildMediaItems(mOutputMediaItemList, devices);
- mOutputMediaItemList.clear();
- mOutputMediaItemList.addAll(updatedMediaItems);
+ List<MediaItem> updatedMediaItems =
+ buildMediaItems(mOutputMediaItemListProxy.getOutputMediaItemList(), devices);
+ mOutputMediaItemListProxy.clearAndAddAll(updatedMediaItems);
}
}
- protected List<MediaItem> buildMediaItems(List<MediaItem> oldMediaItems,
- List<MediaDevice> devices) {
+ protected List<MediaItem> buildMediaItems(
+ List<MediaItem> oldMediaItems, List<MediaDevice> devices) {
synchronized (mMediaDevicesLock) {
if (!mLocalMediaManager.isPreferenceRouteListingExist()) {
attachRangeInfo(devices);
@@ -698,6 +647,27 @@ public class MediaSwitchingController
List<MediaItem> finalMediaItems = targetMediaDevices.stream()
.map(MediaItem::createDeviceMediaItem)
.collect(Collectors.toList());
+
+ boolean shouldAddFirstSeenSelectedDevice =
+ com.android.media.flags.Flags.enableOutputSwitcherDeviceGrouping();
+
+ if (shouldAddFirstSeenSelectedDevice) {
+ finalMediaItems.clear();
+ Set<String> selectedDevicesIds = getSelectedMediaDevice().stream()
+ .map(MediaDevice::getId)
+ .collect(Collectors.toSet());
+ for (MediaDevice targetMediaDevice : targetMediaDevices) {
+ if (shouldAddFirstSeenSelectedDevice
+ && selectedDevicesIds.contains(targetMediaDevice.getId())) {
+ finalMediaItems.add(MediaItem.createDeviceMediaItem(
+ targetMediaDevice, /* isFirstDeviceInGroup */ true));
+ shouldAddFirstSeenSelectedDevice = false;
+ } else {
+ finalMediaItems.add(MediaItem.createDeviceMediaItem(
+ targetMediaDevice, /* isFirstDeviceInGroup */ false));
+ }
+ }
+ }
dividerItems.forEach(finalMediaItems::add);
attachConnectNewDeviceItemIfNeeded(finalMediaItems);
return finalMediaItems;
@@ -722,7 +692,8 @@ public class MediaSwitchingController
* list.
*/
@GuardedBy("mMediaDevicesLock")
- private List<MediaItem> categorizeMediaItemsLocked(MediaDevice connectedMediaDevice,
+ private List<MediaItem> categorizeMediaItemsLocked(
+ MediaDevice connectedMediaDevice,
List<MediaDevice> devices,
boolean needToHandleMutingExpectedDevice) {
List<MediaItem> finalMediaItems = new ArrayList<>();
@@ -781,6 +752,14 @@ public class MediaSwitchingController
}
private void attachConnectNewDeviceItemIfNeeded(List<MediaItem> mediaItems) {
+ MediaItem connectNewDeviceItem = getConnectNewDeviceItem();
+ if (connectNewDeviceItem != null) {
+ mediaItems.add(connectNewDeviceItem);
+ }
+ }
+
+ @Nullable
+ private MediaItem getConnectNewDeviceItem() {
boolean isSelectedDeviceNotAGroup = getSelectedMediaDevice().size() == 1;
if (enableInputRouting()) {
// When input routing is enabled, there are expected to be at least 2 total selected
@@ -789,9 +768,9 @@ public class MediaSwitchingController
}
// Attach "Connect a device" item only when current output is not remote and not a group
- if (!isCurrentConnectedDeviceRemote() && isSelectedDeviceNotAGroup) {
- mediaItems.add(MediaItem.createPairNewDeviceMediaItem());
- }
+ return (!isCurrentConnectedDeviceRemote() && isSelectedDeviceNotAGroup)
+ ? MediaItem.createPairNewDeviceMediaItem()
+ : null;
}
private void attachRangeInfo(List<MediaDevice> devices) {
@@ -880,13 +859,13 @@ public class MediaSwitchingController
mediaItems.add(
MediaItem.createGroupDividerMediaItem(
mContext.getString(R.string.media_output_group_title)));
- mediaItems.addAll(mOutputMediaItemList);
+ mediaItems.addAll(mOutputMediaItemListProxy.getOutputMediaItemList());
}
public List<MediaItem> getMediaItemList() {
// If input routing is not enabled, only return output media items.
if (!enableInputRouting()) {
- return mOutputMediaItemList;
+ return mOutputMediaItemListProxy.getOutputMediaItemList();
}
// If input routing is enabled, return both output and input media items.
@@ -992,7 +971,7 @@ public class MediaSwitchingController
public boolean isAnyDeviceTransferring() {
synchronized (mMediaDevicesLock) {
- for (MediaItem mediaItem : mOutputMediaItemList) {
+ for (MediaItem mediaItem : mOutputMediaItemListProxy.getOutputMediaItemList()) {
if (mediaItem.getMediaDevice().isPresent()
&& mediaItem.getMediaDevice().get().getState()
== LocalMediaManager.MediaDeviceState.STATE_CONNECTING) {
@@ -1032,8 +1011,11 @@ public class MediaSwitchingController
startActivity(launchIntent, controller);
}
- void launchLeBroadcastNotifyDialog(View mediaOutputDialog, BroadcastSender broadcastSender,
- BroadcastNotifyDialog action, final DialogInterface.OnClickListener listener) {
+ void launchLeBroadcastNotifyDialog(
+ View mediaOutputDialog,
+ BroadcastSender broadcastSender,
+ BroadcastNotifyDialog action,
+ final DialogInterface.OnClickListener listener) {
final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
switch (action) {
case ACTION_FIRST_LAUNCH:
@@ -1076,7 +1058,7 @@ public class MediaSwitchingController
mVolumePanelGlobalStateInteractor,
mUserTracker);
MediaOutputBroadcastDialog dialog = new MediaOutputBroadcastDialog(mContext, true,
- broadcastSender, controller);
+ broadcastSender, controller, mMainExecutor, mBackgroundExecutor);
dialog.show();
}
@@ -1263,8 +1245,8 @@ public class MediaSwitchingController
return !sourceList.isEmpty();
}
- boolean addSourceIntoSinkDeviceWithBluetoothLeAssistant(BluetoothDevice sink,
- BluetoothLeBroadcastMetadata metadata, boolean isGroupOp) {
+ boolean addSourceIntoSinkDeviceWithBluetoothLeAssistant(
+ BluetoothDevice sink, BluetoothLeBroadcastMetadata metadata, boolean isGroupOp) {
LocalBluetoothLeBroadcastAssistant assistant =
mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
if (assistant == null) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java b/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java
new file mode 100644
index 000000000000..1c9c0b102cb7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+
+package com.android.systemui.media.dialog;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/** A proxy of holding the list of Output Switcher's output media items. */
+public class OutputMediaItemListProxy {
+ private final List<MediaItem> mOutputMediaItemList;
+
+ public OutputMediaItemListProxy() {
+ mOutputMediaItemList = new CopyOnWriteArrayList<>();
+ }
+
+ /** Returns the list of output media items. */
+ public List<MediaItem> getOutputMediaItemList() {
+ return mOutputMediaItemList;
+ }
+
+ /** Updates the list of output media items with the given list. */
+ public void clearAndAddAll(List<MediaItem> updatedMediaItems) {
+ mOutputMediaItemList.clear();
+ mOutputMediaItemList.addAll(updatedMediaItems);
+ }
+
+ /** Removes the media items with muting expected devices. */
+ public void removeMutingExpectedDevices() {
+ mOutputMediaItemList.removeIf((MediaItem::isMutingExpectedDevice));
+ }
+
+ /** Clears the output media item list. */
+ public void clear() {
+ mOutputMediaItemList.clear();
+ }
+
+ /** Returns whether the output media item list is empty. */
+ public boolean isEmpty() {
+ return mOutputMediaItemList.isEmpty();
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/domain/interactor/MediaInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/domain/interactor/MediaInteractor.kt
new file mode 100644
index 000000000000..afed141644ff
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/domain/interactor/MediaInteractor.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.remedia.domain.interactor
+
+import com.android.systemui.media.remedia.domain.model.MediaSessionModel
+
+/**
+ * Defines interface for classes that can provide business logic in the domain of the media controls
+ * element.
+ */
+interface MediaInteractor {
+
+ /** The list of sessions. Needs to be backed by a compose snapshot state. */
+ val sessions: List<MediaSessionModel>
+
+ /** Seek to [to], in milliseconds on the media session with the given [sessionKey]. */
+ fun seek(sessionKey: Any, to: Long)
+
+ /** Hide the representation of the media session with the given [sessionKey]. */
+ fun hide(sessionKey: Any)
+
+ /** Open media settings. */
+ fun openMediaSettings()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaActionModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaActionModel.kt
new file mode 100644
index 000000000000..02e4d7a966c2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaActionModel.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.remedia.domain.model
+
+import com.android.systemui.common.shared.model.Icon
+
+sealed interface MediaActionModel {
+ data class Action(val icon: Icon, val onClick: (() -> Unit)?) : MediaActionModel
+
+ data object ReserveSpace : MediaActionModel
+
+ data object None : MediaActionModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaOutputDeviceModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaOutputDeviceModel.kt
new file mode 100644
index 000000000000..d581ae3e4e40
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaOutputDeviceModel.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.remedia.domain.model
+
+import com.android.systemui.common.shared.model.Icon
+
+data class MediaOutputDeviceModel(val name: String, val icon: Icon, val isInProgress: Boolean)
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaSessionModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaSessionModel.kt
new file mode 100644
index 000000000000..e64ce73226f2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaSessionModel.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.remedia.domain.model
+
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.graphics.ImageBitmap
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.media.remedia.shared.model.MediaCardActionButtonLayout
+import com.android.systemui.media.remedia.shared.model.MediaColorScheme
+import com.android.systemui.media.remedia.shared.model.MediaSessionState
+
+/** Data model representing a media session. */
+@Stable
+interface MediaSessionModel {
+ /** Unique identifier. */
+ val key: Any
+
+ val appName: String
+
+ val appIcon: Icon
+
+ val background: ImageBitmap?
+
+ val colorScheme: MediaColorScheme
+
+ val title: String
+
+ val subtitle: String
+
+ val onClick: () -> Unit
+
+ /**
+ * Whether the session is currently active. Under some UIs, only currently active session should
+ * be shown.
+ */
+ val isActive: Boolean
+
+ /** Whether the session can be hidden/dismissed by the user. */
+ val canBeHidden: Boolean
+
+ /**
+ * Whether the session currently supports scrubbing (e.g. moving to a different position iin the
+ * playback.
+ */
+ val canBeScrubbed: Boolean
+
+ val state: MediaSessionState
+
+ /** The position of the playback within the current track. */
+ val positionMs: Long
+
+ /** The total duration of the current track. */
+ val durationMs: Long
+
+ val outputDevice: MediaOutputDeviceModel
+
+ /** How to lay out the action buttons. */
+ val actionButtonLayout: MediaCardActionButtonLayout
+ val playPauseAction: MediaActionModel
+ val leftAction: MediaActionModel
+ val rightAction: MediaActionModel
+ val additionalActions: List<MediaActionModel.Action>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaActionState.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaActionState.kt
new file mode 100644
index 000000000000..c3ce5035accc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaActionState.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.remedia.shared.model
+
+enum class MediaActionState {
+ Enabled,
+ Disabled,
+ Hidden,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaCardActionButtonLayout.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaCardActionButtonLayout.kt
new file mode 100644
index 000000000000..554fb1f2fc43
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaCardActionButtonLayout.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.remedia.shared.model
+
+enum class MediaCardActionButtonLayout {
+ /** Shows the play/pause button and left/right buttons in privileged positions on the card */
+ WithPlayPause,
+ /** Shows all action buttons along the bottom row. */
+ SecondaryActionsOnly,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaColorScheme.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaColorScheme.kt
new file mode 100644
index 000000000000..8dba170f0928
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaColorScheme.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.remedia.shared.model
+
+import androidx.compose.ui.graphics.Color
+
+data class MediaColorScheme(val primary: Color, val onPrimary: Color)
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/DismissibleHorizontalPager.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/DismissibleHorizontalPager.kt
new file mode 100644
index 000000000000..fea5b3267562
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/DismissibleHorizontalPager.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.remedia.ui.compose
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.PagerScope
+import androidx.compose.foundation.pager.PagerState
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.dp
+import com.android.compose.modifiers.thenIf
+import kotlinx.coroutines.launch
+
+/** State for a [DismissibleHorizontalPager] */
+class DismissibleHorizontalPagerState(
+ val isDismissible: Boolean,
+ val isScrollingEnabled: Boolean,
+ val pagerState: PagerState,
+ val offset: Animatable<Float, AnimationVector1D>,
+)
+
+/**
+ * Returns a remembered [DismissibleHorizontalPagerState] that starts at [initialPage] and has
+ * [pageCount] total pages.
+ */
+@Composable
+fun rememberDismissibleHorizontalPagerState(
+ isDismissible: Boolean = true,
+ isScrollingEnabled: Boolean = true,
+ initialPage: Int = 0,
+ pageCount: () -> Int,
+): DismissibleHorizontalPagerState {
+ val pagerState = rememberPagerState(initialPage = initialPage, pageCount = pageCount)
+ val offset = remember { Animatable(0f) }
+
+ return remember(isDismissible, isScrollingEnabled, pagerState, offset) {
+ DismissibleHorizontalPagerState(
+ isDismissible = isDismissible,
+ isScrollingEnabled = isScrollingEnabled,
+ pagerState = pagerState,
+ offset = offset,
+ )
+ }
+}
+
+/**
+ * A [HorizontalPager] that can be swiped-away to dismiss by the user when swiped farther left or
+ * right once fully scrolled to the left-most or right-most page, respectively.
+ */
+@Composable
+fun DismissibleHorizontalPager(
+ state: DismissibleHorizontalPagerState,
+ onDismissed: () -> Unit,
+ modifier: Modifier = Modifier,
+ key: ((Int) -> Any)? = null,
+ pageSpacing: Dp = 0.dp,
+ indicator: @Composable BoxScope.() -> Unit,
+ pageContent: @Composable PagerScope.(page: Int) -> Unit,
+) {
+ val scope = rememberCoroutineScope()
+
+ val nestedScrollConnection = remember {
+ object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ return if (state.offset.value > 0f && available.x < 0f) {
+ scope.launch { state.offset.snapTo(state.offset.value + available.x) }
+ Offset(available.x, 0f)
+ } else if (state.offset.value < 0f && available.x > 0f) {
+ scope.launch { state.offset.snapTo(state.offset.value + available.x) }
+ Offset(available.x, 0f)
+ } else {
+ Offset.Zero
+ }
+ }
+
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource,
+ ): Offset {
+ return if (available.x > 0f) {
+ scope.launch { state.offset.snapTo(state.offset.value + available.x) }
+ Offset(available.x, 0f)
+ } else if (available.x < 0f) {
+ scope.launch { state.offset.snapTo(state.offset.value + available.x) }
+ Offset(available.x, 0f)
+ } else {
+ Offset.Zero
+ }
+ }
+
+ override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+ scope.launch {
+ state.offset.animateTo(
+ if (state.offset.value >= state.pagerState.layoutInfo.pageSize / 2f) {
+ state.pagerState.layoutInfo.pageSize * 2f
+ } else if (
+ state.offset.value <= -state.pagerState.layoutInfo.pageSize / 2f
+ ) {
+ -state.pagerState.layoutInfo.pageSize * 2f
+ } else {
+ 0f
+ }
+ )
+ if (state.offset.value != 0f) {
+ onDismissed()
+ }
+ }
+ return super.onPostFling(consumed, available)
+ }
+ }
+ }
+
+ Box(modifier = modifier) {
+ HorizontalPager(
+ state = state.pagerState,
+ userScrollEnabled = state.isScrollingEnabled,
+ key = key,
+ pageSpacing = pageSpacing,
+ pageContent = pageContent,
+ modifier =
+ Modifier.thenIf(state.isDismissible) {
+ Modifier.nestedScroll(nestedScrollConnection).graphicsLayer {
+ translationX = state.offset.value
+ }
+ },
+ )
+
+ indicator()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt
index c9fb8e877009..9eb55a8eff2e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt
@@ -20,6 +20,7 @@ package com.android.systemui.media.remedia.ui.compose
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
+import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
@@ -36,6 +37,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.hoverable
+import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.DragInteraction
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -43,9 +45,9 @@ import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@@ -63,14 +65,14 @@ import androidx.compose.material3.SliderColors
import androidx.compose.material3.SliderDefaults.colors
import androidx.compose.material3.SliderState
import androidx.compose.material3.Text
+import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateListOf
-import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -90,6 +92,7 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
@@ -105,20 +108,137 @@ import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.SceneTransitionLayout
import com.android.compose.animation.scene.rememberMutableSceneTransitionLayoutState
import com.android.compose.animation.scene.transitions
-import com.android.compose.theme.LocalAndroidColorScheme
+import com.android.compose.ui.graphics.painter.rememberDrawablePainter
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.ui.compose.Icon
+import com.android.systemui.common.ui.compose.PagerDots
import com.android.systemui.common.ui.compose.load
import com.android.systemui.communal.ui.compose.extensions.detectLongPressGesture
+import com.android.systemui.lifecycle.rememberViewModel
+import com.android.systemui.media.remedia.shared.model.MediaCardActionButtonLayout
+import com.android.systemui.media.remedia.shared.model.MediaColorScheme
import com.android.systemui.media.remedia.shared.model.MediaSessionState
import com.android.systemui.media.remedia.ui.viewmodel.MediaCardGutsViewModel
import com.android.systemui.media.remedia.ui.viewmodel.MediaCardViewModel
+import com.android.systemui.media.remedia.ui.viewmodel.MediaCarouselVisibility
+import com.android.systemui.media.remedia.ui.viewmodel.MediaNavigationViewModel
import com.android.systemui.media.remedia.ui.viewmodel.MediaOutputSwitcherChipViewModel
import com.android.systemui.media.remedia.ui.viewmodel.MediaPlayPauseActionViewModel
import com.android.systemui.media.remedia.ui.viewmodel.MediaSecondaryActionViewModel
-import com.android.systemui.media.remedia.ui.viewmodel.MediaSeekBarViewModel
+import com.android.systemui.media.remedia.ui.viewmodel.MediaViewModel
import kotlin.math.max
+/**
+ * Renders a media controls UI element.
+ *
+ * This composable supports a multitude of presentation styles/layouts controlled by the
+ * [presentationStyle] parameter. If the card carousel can be swiped away to dismiss by the user,
+ * the [onDismissed] callback will be invoked when/if that happens.
+ */
+@Composable
+fun Media(
+ viewModelFactory: MediaViewModel.Factory,
+ presentationStyle: MediaPresentationStyle,
+ behavior: MediaUiBehavior,
+ onDismissed: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val context = LocalContext.current
+ val viewModel: MediaViewModel =
+ rememberViewModel("Media.viewModel") {
+ viewModelFactory.create(
+ context = context,
+ carouselVisibility = behavior.carouselVisibility,
+ )
+ }
+
+ CardCarousel(
+ viewModel = viewModel,
+ presentationStyle = presentationStyle,
+ behavior = behavior,
+ onDismissed = onDismissed,
+ modifier = modifier,
+ )
+}
+
+/**
+ * Renders a media controls carousel of cards.
+ *
+ * This composable supports a multitude of presentation styles/layouts controlled by the
+ * [presentationStyle] parameter. The behavior is controlled by [behavior]. If
+ * [MediaUiBehavior.isCarouselDismissible] is `true`, the [onDismissed] callback will be invoked
+ * when/if that happens.
+ */
+@Composable
+private fun CardCarousel(
+ viewModel: MediaViewModel,
+ presentationStyle: MediaPresentationStyle,
+ behavior: MediaUiBehavior,
+ onDismissed: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ AnimatedVisibility(visible = viewModel.isCarouselVisible, modifier = modifier) {
+ CardCarouselContent(
+ viewModel = viewModel,
+ presentationStyle = presentationStyle,
+ behavior = behavior,
+ onDismissed = onDismissed,
+ )
+ }
+}
+
+@Composable
+private fun CardCarouselContent(
+ viewModel: MediaViewModel,
+ presentationStyle: MediaPresentationStyle,
+ behavior: MediaUiBehavior,
+ onDismissed: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val pagerState =
+ rememberDismissibleHorizontalPagerState(
+ isDismissible = behavior.isCarouselDismissible,
+ isScrollingEnabled = behavior.isCarouselScrollingEnabled,
+ ) {
+ viewModel.cards.size
+ }
+
+ val roundedCornerShape = RoundedCornerShape(32.dp)
+
+ LaunchedEffect(pagerState.pagerState.currentPage) {
+ viewModel.onCardSelected(pagerState.pagerState.currentPage)
+ }
+
+ DismissibleHorizontalPager(
+ state = pagerState,
+ onDismissed = onDismissed,
+ pageSpacing = 8.dp,
+ key = { index -> viewModel.cards[index].key },
+ indicator = {
+ if (pagerState.pagerState.pageCount > 1) {
+ PagerDots(
+ pagerState = pagerState.pagerState,
+ activeColor = Color(0xffdee0ff),
+ nonActiveColor = Color(0xffa7a9ca),
+ dotSize = 6.dp,
+ spaceSize = 6.dp,
+ modifier =
+ Modifier.align(Alignment.BottomCenter).padding(8.dp).graphicsLayer {
+ translationX = pagerState.offset.value
+ },
+ )
+ }
+ },
+ modifier = modifier.padding(8.dp).clip(roundedCornerShape),
+ ) { index ->
+ Card(
+ viewModel = viewModel.cards[index],
+ presentationStyle = presentationStyle,
+ modifier = Modifier.clip(roundedCornerShape),
+ )
+ }
+}
+
/** Renders the UI of a single media card. */
@Composable
private fun Card(
@@ -139,7 +259,7 @@ private fun Card(
Box(modifier) {
if (stlState.currentScene != Media.Scenes.Compact) {
- CardBackground(imageLoader = viewModel.artLoader, modifier = Modifier.matchParentSize())
+ CardBackground(image = viewModel.background, modifier = Modifier.matchParentSize())
}
key(stlState) {
@@ -158,6 +278,22 @@ private fun Card(
}
}
+@Composable
+private fun rememberAnimatedColorScheme(colorScheme: MediaColorScheme): AnimatedColorScheme {
+ val animatedPrimary by animateColorAsState(targetValue = colorScheme.primary)
+ val animatedOnPrimary by animateColorAsState(targetValue = colorScheme.onPrimary)
+
+ return remember {
+ object : AnimatedColorScheme {
+ override val primary: Color
+ get() = animatedPrimary
+
+ override val onPrimary: Color
+ get() = animatedOnPrimary
+ }
+ }
+}
+
/**
* Renders the foreground of a card, including all UI content and the internal "guts".
*
@@ -180,6 +316,8 @@ private fun ContentScope.CardForeground(
val isGutsVisible = viewModel.guts.isVisible
LaunchedEffect(isGutsVisible) { gutsAlphaAnimatable.animateTo(if (isGutsVisible) 1f else 0f) }
+ val colorScheme = rememberAnimatedColorScheme(viewModel.colorScheme)
+
// Use a custom layout to measure the content even if the content is being hidden because the
// internal guts are showing. This is needed because only the content knows the size the of the
// card and the guts are set to be the same size of the content.
@@ -189,6 +327,7 @@ private fun ContentScope.CardForeground(
viewModel = viewModel,
threeRows = threeRows,
fillHeight = fillHeight,
+ colorScheme = colorScheme,
modifier =
Modifier.graphicsLayer {
compositingStrategy = CompositingStrategy.ModulateAlpha
@@ -198,6 +337,7 @@ private fun ContentScope.CardForeground(
CardGuts(
viewModel = viewModel.guts,
+ colorScheme = colorScheme,
modifier =
Modifier.graphicsLayer {
compositingStrategy = CompositingStrategy.ModulateAlpha
@@ -232,29 +372,37 @@ private fun ContentScope.CardForegroundContent(
viewModel: MediaCardViewModel,
threeRows: Boolean,
fillHeight: Boolean,
+ colorScheme: AnimatedColorScheme,
modifier: Modifier = Modifier,
) {
Column(
modifier =
- modifier
- .combinedClickable(
- onClick = viewModel.onClick,
- onLongClick = viewModel.onLongClick,
- onClickLabel = viewModel.onClickLabel,
- )
- .padding(16.dp)
+ modifier.combinedClickable(
+ onClick = viewModel.onClick,
+ onLongClick = viewModel.onLongClick,
+ onClickLabel = viewModel.onClickLabel,
+ )
) {
// Always add the first/top row, regardless of presentation style.
- Row(verticalAlignment = Alignment.CenterVertically) {
+ Box(modifier = Modifier.fillMaxWidth()) {
// Icon.
Icon(
icon = viewModel.icon,
- tint = LocalAndroidColorScheme.current.primaryFixed,
- modifier = Modifier.size(24.dp).clip(CircleShape),
+ tint = colorScheme.primary,
+ modifier =
+ Modifier.align(Alignment.TopStart)
+ .padding(top = 16.dp, start = 16.dp)
+ .size(24.dp)
+ .clip(CircleShape),
)
- Spacer(modifier = Modifier.weight(1f))
- viewModel.outputSwitcherChips.fastForEach { chip ->
- OutputSwitcherChip(viewModel = chip, modifier = Modifier.padding(start = 8.dp))
+
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ modifier = Modifier.align(Alignment.TopEnd),
+ ) {
+ viewModel.outputSwitcherChips.fastForEach { chip ->
+ OutputSwitcherChip(viewModel = chip, colorScheme = colorScheme)
+ }
}
}
@@ -271,7 +419,7 @@ private fun ContentScope.CardForegroundContent(
// Second row.
Row(
verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.padding(top = 16.dp),
+ modifier = Modifier.padding(start = 16.dp, top = 16.dp, end = 16.dp),
) {
Metadata(
title = viewModel.title,
@@ -280,14 +428,16 @@ private fun ContentScope.CardForegroundContent(
modifier = Modifier.weight(1f).padding(end = 8.dp),
)
- AnimatedVisibility(visible = viewModel.playPauseAction.isVisible) {
- PlayPauseAction(
- viewModel = viewModel.playPauseAction,
- buttonWidth = 48.dp,
- buttonColor = LocalAndroidColorScheme.current.primaryFixed,
- iconColor = LocalAndroidColorScheme.current.onPrimaryFixed,
- buttonCornerRadius = { isPlaying -> if (isPlaying) 16.dp else 48.dp },
- )
+ if (viewModel.actionButtonLayout == MediaCardActionButtonLayout.WithPlayPause) {
+ AnimatedVisibility(visible = viewModel.playPauseAction != null) {
+ PlayPauseAction(
+ viewModel = checkNotNull(viewModel.playPauseAction),
+ buttonWidth = 48.dp,
+ buttonColor = colorScheme.primary,
+ iconColor = colorScheme.onPrimary,
+ buttonCornerRadius = { isPlaying -> if (isPlaying) 16.dp else 48.dp },
+ )
+ }
}
}
@@ -295,9 +445,16 @@ private fun ContentScope.CardForegroundContent(
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.padding(top = 24.dp),
+ modifier = Modifier.padding(start = 16.dp, top = 24.dp, end = 16.dp, bottom = 16.dp),
) {
- Navigation(viewModel = viewModel.seekBar, isSeekBarVisible = true)
+ Navigation(
+ viewModel = viewModel.navigation,
+ isSeekBarVisible = true,
+ areActionsVisible =
+ viewModel.actionButtonLayout == MediaCardActionButtonLayout.WithPlayPause,
+ modifier = Modifier.weight(1f),
+ )
+
viewModel.additionalActions.fastForEachIndexed { index, action ->
SecondaryAction(
viewModel = action,
@@ -311,7 +468,7 @@ private fun ContentScope.CardForegroundContent(
// Bottom row.
Row(
verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.padding(top = 36.dp),
+ modifier = Modifier.padding(start = 16.dp, top = 36.dp, end = 16.dp, bottom = 16.dp),
) {
Metadata(
title = viewModel.title,
@@ -321,18 +478,34 @@ private fun ContentScope.CardForegroundContent(
)
Navigation(
- viewModel = viewModel.seekBar,
+ viewModel = viewModel.navigation,
isSeekBarVisible = false,
+ areActionsVisible =
+ viewModel.actionButtonLayout == MediaCardActionButtonLayout.WithPlayPause,
modifier = Modifier.padding(end = 8.dp),
)
- PlayPauseAction(
- viewModel = viewModel.playPauseAction,
- buttonWidth = 48.dp,
- buttonColor = LocalAndroidColorScheme.current.primaryFixed,
- iconColor = LocalAndroidColorScheme.current.onPrimaryFixed,
- buttonCornerRadius = { isPlaying -> if (isPlaying) 16.dp else 48.dp },
- )
+ if (
+ viewModel.actionButtonLayout == MediaCardActionButtonLayout.SecondaryActionsOnly
+ ) {
+ viewModel.additionalActions.fastForEachIndexed { index, action ->
+ SecondaryAction(
+ viewModel = action,
+ element = Media.Elements.additionalActionButton(index),
+ modifier = Modifier.padding(end = 8.dp),
+ )
+ }
+ }
+
+ AnimatedVisibility(visible = viewModel.playPauseAction != null) {
+ PlayPauseAction(
+ viewModel = checkNotNull(viewModel.playPauseAction),
+ buttonWidth = 48.dp,
+ buttonColor = colorScheme.primary,
+ iconColor = colorScheme.onPrimary,
+ buttonCornerRadius = { isPlaying -> if (isPlaying) 16.dp else 48.dp },
+ )
+ }
}
}
}
@@ -375,18 +548,18 @@ private fun ContentScope.CompactCardForeground(
iconColor = MaterialTheme.colorScheme.onSurface,
)
- val nextAction = (viewModel.seekBar as? MediaSeekBarViewModel.Showing)?.next
- if (nextAction != null) {
+ val rightAction = (viewModel.navigation as? MediaNavigationViewModel.Showing)?.right
+ if (rightAction != null) {
SecondaryAction(
- viewModel = nextAction,
+ viewModel = rightAction,
element = Media.Elements.NextButton,
iconColor = MaterialTheme.colorScheme.onSurface,
)
}
- AnimatedVisibility(visible = viewModel.playPauseAction.isVisible) {
+ AnimatedVisibility(visible = viewModel.playPauseAction != null) {
PlayPauseAction(
- viewModel = viewModel.playPauseAction,
+ viewModel = checkNotNull(viewModel.playPauseAction),
buttonWidth = 72.dp,
buttonColor = MaterialTheme.colorScheme.primaryContainer,
iconColor = MaterialTheme.colorScheme.onPrimaryContainer,
@@ -398,47 +571,38 @@ private fun ContentScope.CompactCardForeground(
/** Renders the background of a card, loading the artwork and showing an overlay on top of it. */
@Composable
-private fun CardBackground(imageLoader: suspend () -> ImageBitmap, modifier: Modifier = Modifier) {
- var image: ImageBitmap? by remember { mutableStateOf(null) }
- LaunchedEffect(imageLoader) {
- image = null
- image = imageLoader()
- }
-
- val gradientBaseColor = MaterialTheme.colorScheme.onSurface
- Box(
- modifier =
- modifier.drawWithContent {
- // Draw the content of the box (loaded art or placeholder).
- drawContent()
-
- if (image != null) {
- // Then draw the overlay.
- drawRect(
- brush =
- Brush.radialGradient(
- 0f to gradientBaseColor.copy(alpha = 0.65f),
- 1f to gradientBaseColor.copy(alpha = 0.75f),
- center = size.center,
- radius = max(size.width, size.height) / 2,
- )
- )
- }
- }
- ) {
- image?.let { loadedImage ->
+private fun CardBackground(image: ImageBitmap?, modifier: Modifier = Modifier) {
+ Crossfade(targetState = image, modifier = modifier) { imageOrNull ->
+ if (imageOrNull != null) {
// Loaded art.
+ val gradientBaseColor = MaterialTheme.colorScheme.onSurface
Image(
- bitmap = loadedImage,
+ bitmap = imageOrNull,
contentDescription = null,
contentScale = ContentScale.Crop,
- modifier = Modifier.matchParentSize(),
+ modifier =
+ Modifier.fillMaxSize().drawWithContent {
+ // Draw the content (loaded art).
+ drawContent()
+
+ if (image != null) {
+ // Then draw the overlay.
+ drawRect(
+ brush =
+ Brush.radialGradient(
+ 0f to gradientBaseColor.copy(alpha = 0.65f),
+ 1f to gradientBaseColor.copy(alpha = 0.75f),
+ center = size.center,
+ radius = max(size.width, size.height) / 2,
+ )
+ )
+ }
+ },
)
+ } else {
+ // Placeholder.
+ Box(Modifier.background(MaterialTheme.colorScheme.onSurface).fillMaxSize())
}
- ?: run {
- // Placeholder.
- Box(Modifier.background(MaterialTheme.colorScheme.onSurface).matchParentSize())
- }
}
}
@@ -449,22 +613,26 @@ private fun CardBackground(imageLoader: suspend () -> ImageBitmap, modifier: Mod
* would otherwise be showing based on the view-model alone. This is meant for callers to decide
* whether they'd like to show the seek bar in addition to the prev/next buttons or just show the
* buttons.
+ *
+ * If [areActionsVisible] is `false`, the left/right buttons to the left and right of the seek bar
+ * will not be included in the layout.
*/
@Composable
private fun ContentScope.Navigation(
- viewModel: MediaSeekBarViewModel,
+ viewModel: MediaNavigationViewModel,
isSeekBarVisible: Boolean,
+ areActionsVisible: Boolean,
modifier: Modifier = Modifier,
) {
when (viewModel) {
- is MediaSeekBarViewModel.Showing -> {
+ is MediaNavigationViewModel.Showing -> {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = modifier,
) {
- viewModel.previous?.let {
- SecondaryAction(viewModel = it, element = Media.Elements.PrevButton)
+ if (areActionsVisible) {
+ SecondaryAction(viewModel = viewModel.left, element = Media.Elements.PrevButton)
}
val interactionSource = remember { MutableInteractionSource() }
@@ -499,13 +667,16 @@ private fun ContentScope.Navigation(
}
}
- viewModel.next?.let {
- SecondaryAction(viewModel = it, element = Media.Elements.NextButton)
+ if (areActionsVisible) {
+ SecondaryAction(
+ viewModel = viewModel.right,
+ element = Media.Elements.NextButton,
+ )
}
}
}
- is MediaSeekBarViewModel.Hidden -> Unit
+ is MediaNavigationViewModel.Hidden -> Unit
}
}
@@ -647,7 +818,11 @@ private fun SeekBarTrack(
/** Renders the internal "guts" of a card. */
@Composable
-private fun CardGuts(viewModel: MediaCardGutsViewModel, modifier: Modifier = Modifier) {
+private fun CardGuts(
+ viewModel: MediaCardGutsViewModel,
+ colorScheme: AnimatedColorScheme,
+ modifier: Modifier = Modifier,
+) {
Box(
modifier =
modifier.pointerInput(Unit) { detectLongPressGesture { viewModel.onLongClick() } }
@@ -682,7 +857,7 @@ private fun CardGuts(viewModel: MediaCardGutsViewModel, modifier: Modifier = Mod
) {
Text(
text = checkNotNull(viewModel.primaryAction.text),
- color = LocalAndroidColorScheme.current.onPrimaryFixed,
+ color = colorScheme.onPrimary,
)
}
@@ -740,29 +915,51 @@ private fun ContentScope.Metadata(
@Composable
private fun OutputSwitcherChip(
viewModel: MediaOutputSwitcherChipViewModel,
+ colorScheme: AnimatedColorScheme,
modifier: Modifier = Modifier,
) {
- PlatformButton(
- onClick = viewModel.onClick,
- colors =
- ButtonDefaults.buttonColors(
- containerColor = LocalAndroidColorScheme.current.primaryFixed
- ),
- contentPadding = PaddingValues(start = 8.dp, end = 12.dp, top = 4.dp, bottom = 4.dp),
- modifier = modifier.height(24.dp),
+ // For accessibility reasons, the touch area for the chip needs to be at least 48dp in height.
+ // At the same time, the rounded corner chip should only be as tall as it needs to be to contain
+ // its contents and look like a nice design; also, the ripple effect should only be shown within
+ // the bounds of the chip.
+ //
+ // This is achieved by sharing this InteractionSource between the outer and inner composables.
+ //
+ // The outer composable hosts that clickable that writes user events into the InteractionSource.
+ // The inner composable consumes the user events from the InteractionSource and feeds them into
+ // its indication.
+ val clickInteractionSource = remember { MutableInteractionSource() }
+ Box(
+ modifier =
+ modifier
+ .height(48.dp)
+ .clickable(interactionSource = clickInteractionSource, indication = null) {
+ viewModel.onClick()
+ }
+ .padding(top = 16.dp, end = 16.dp, bottom = 8.dp)
) {
- Icon(
- icon = viewModel.icon,
- tint = LocalAndroidColorScheme.current.onPrimaryFixed,
- modifier = Modifier.size(16.dp),
- )
- viewModel.text?.let {
- Spacer(Modifier.size(4.dp))
- Text(
- text = viewModel.text,
- style = MaterialTheme.typography.bodySmall,
- color = LocalAndroidColorScheme.current.onPrimaryFixed,
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ modifier =
+ Modifier.clip(RoundedCornerShape(12.dp))
+ .background(colorScheme.primary)
+ .indication(clickInteractionSource, ripple())
+ .padding(start = 8.dp, end = 12.dp, top = 4.dp, bottom = 4.dp),
+ ) {
+ Icon(
+ icon = viewModel.icon,
+ tint = colorScheme.onPrimary,
+ modifier = Modifier.size(16.dp),
)
+
+ viewModel.text?.let {
+ Text(
+ text = viewModel.text,
+ style = MaterialTheme.typography.bodySmall,
+ color = colorScheme.onPrimary,
+ )
+ }
}
}
}
@@ -785,7 +982,8 @@ private fun ContentScope.PlayPauseAction(
// This element can be animated when switching between scenes inside a media card.
Element(key = Media.Elements.PlayPauseButton, modifier = modifier) {
PlatformButton(
- onClick = viewModel.onClick,
+ onClick = viewModel.onClick ?: {},
+ enabled = viewModel.onClick != null,
colors = ButtonDefaults.buttonColors(containerColor = buttonColor),
shape = RoundedCornerShape(cornerRadius),
modifier = Modifier.size(width = buttonWidth, height = 48.dp),
@@ -793,22 +991,29 @@ private fun ContentScope.PlayPauseAction(
when (viewModel.state) {
is MediaSessionState.Playing,
is MediaSessionState.Paused -> {
- // TODO(b/399860531): load this expensive-to-load animated vector drawable off
- // the main thread.
- val iconResource = checkNotNull(viewModel.icon)
- Icon(
- painter =
- rememberAnimatedVectorPainter(
- animatedImageVector =
- AnimatedImageVector.animatedVectorResource(
- id = iconResource.res
- ),
- atEnd = viewModel.state == MediaSessionState.Playing,
- ),
- contentDescription = iconResource.contentDescription?.load(),
- tint = iconColor,
- modifier = Modifier.size(24.dp),
- )
+ val painterOrNull =
+ when (viewModel.icon) {
+ // TODO(b/399860531): load this expensive-to-load animated vector
+ // drawable off the main thread.
+ is Icon.Resource ->
+ rememberAnimatedVectorPainter(
+ animatedImageVector =
+ AnimatedImageVector.animatedVectorResource(
+ id = viewModel.icon.res
+ ),
+ atEnd = viewModel.state == MediaSessionState.Playing,
+ )
+ is Icon.Loaded -> rememberDrawablePainter(viewModel.icon.drawable)
+ null -> null
+ }
+ painterOrNull?.let { painter ->
+ Icon(
+ painter = painter,
+ contentDescription = viewModel.icon?.contentDescription?.load(),
+ tint = iconColor,
+ modifier = Modifier.size(24.dp),
+ )
+ }
}
is MediaSessionState.Buffering -> {
CircularProgressIndicator(color = iconColor, modifier = Modifier.size(24.dp))
@@ -831,7 +1036,7 @@ private fun ContentScope.SecondaryAction(
element: ElementKey? = null,
iconColor: Color = Color.White,
) {
- if (element != null) {
+ if (viewModel !is MediaSecondaryActionViewModel.None && element != null) {
Element(key = element, modifier = modifier) {
SecondaryActionContent(viewModel = viewModel, iconColor = iconColor)
}
@@ -847,14 +1052,22 @@ private fun SecondaryActionContent(
iconColor: Color,
modifier: Modifier = Modifier,
) {
- PlatformIconButton(
- onClick = viewModel.onClick,
- iconResource = (viewModel.icon as Icon.Resource).res,
- contentDescription = viewModel.icon.contentDescription?.load(),
- colors = IconButtonDefaults.iconButtonColors(contentColor = iconColor),
- enabled = viewModel.isEnabled,
- modifier = modifier.size(48.dp).padding(13.dp),
- )
+ val sharedModifier = modifier.size(48.dp).padding(13.dp)
+ when (viewModel) {
+ is MediaSecondaryActionViewModel.Action ->
+ PlatformIconButton(
+ onClick = viewModel.onClick ?: {},
+ iconResource = (viewModel.icon as Icon.Resource).res,
+ contentDescription = viewModel.icon.contentDescription?.load(),
+ colors = IconButtonDefaults.iconButtonColors(contentColor = iconColor),
+ enabled = viewModel.onClick != null,
+ modifier = sharedModifier,
+ )
+
+ is MediaSecondaryActionViewModel.ReserveSpace -> Spacer(modifier = sharedModifier)
+
+ is MediaSecondaryActionViewModel.None -> Unit
+ }
}
/** Enumerates all supported media presentation styles. */
@@ -867,6 +1080,19 @@ enum class MediaPresentationStyle {
Compact,
}
+data class MediaUiBehavior(
+ val isCarouselDismissible: Boolean = true,
+ val isCarouselScrollingEnabled: Boolean = true,
+ val carouselVisibility: MediaCarouselVisibility = MediaCarouselVisibility.WhenNotEmpty,
+ val isFalsingProtectionNeeded: Boolean = false,
+)
+
+@Stable
+private interface AnimatedColorScheme {
+ val primary: Color
+ val onPrimary: Color
+}
+
private object Media {
/**
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCardViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCardViewModel.kt
index ecd6e6d094d9..833a04ddcb55 100644
--- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCardViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCardViewModel.kt
@@ -19,6 +19,8 @@ package com.android.systemui.media.remedia.ui.viewmodel
import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.ImageBitmap
import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.media.remedia.shared.model.MediaCardActionButtonLayout
+import com.android.systemui.media.remedia.shared.model.MediaColorScheme
/** Models UI state for a media card. */
@Stable
@@ -31,20 +33,19 @@ interface MediaCardViewModel {
val icon: Icon
- /**
- * A callback to load the artwork for the media shown on this card. This callback will be
- * invoked on the main thread, it's up to the implementation to move the loading off the main
- * thread.
- */
- val artLoader: suspend () -> ImageBitmap
+ val background: ImageBitmap?
+
+ val colorScheme: MediaColorScheme
val title: String
val subtitle: String
- val playPauseAction: MediaPlayPauseActionViewModel
+ val actionButtonLayout: MediaCardActionButtonLayout
+
+ val playPauseAction: MediaPlayPauseActionViewModel?
- val seekBar: MediaSeekBarViewModel
+ val navigation: MediaNavigationViewModel
val additionalActions: List<MediaSecondaryActionViewModel>
@@ -53,7 +54,7 @@ interface MediaCardViewModel {
val outputSwitcherChips: List<MediaOutputSwitcherChipViewModel>
/** Simple icon-only version of the output switcher for use in compact UIs. */
- val outputSwitcherChipButton: MediaSecondaryActionViewModel
+ val outputSwitcherChipButton: MediaSecondaryActionViewModel.Action
val onClick: () -> Unit
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCarouselVisibility.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCarouselVisibility.kt
new file mode 100644
index 000000000000..53aa87ce0843
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCarouselVisibility.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.remedia.ui.viewmodel
+
+/** Enumerates the known rules for media carousel visibility. */
+enum class MediaCarouselVisibility {
+
+ /** The carousel should be shown as long as it has at least one card. */
+ WhenNotEmpty,
+
+ /**
+ * The carousel should be shown as long as it has at least one card that represents an active
+ * media session. In other words: if all cards in the carousel represent _inactive_ sessions,
+ * the carousel should _not_ be visible.
+ */
+ WhenAnyCardIsActive,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaNavigationViewModel.kt
index f1ced6bf908d..c34c7337290f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSeekBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaNavigationViewModel.kt
@@ -18,17 +18,26 @@ package com.android.systemui.media.remedia.ui.viewmodel
import androidx.annotation.FloatRange
-/** Models UI state for the seek bar. */
-sealed interface MediaSeekBarViewModel {
+/**
+ * Models UI state for the navigation component of the UI (potentially containing the seek bar and
+ * the buttons to its left and right).
+ */
+sealed interface MediaNavigationViewModel {
/** The seek bar should be showing. */
data class Showing(
/** The progress to show on the seek bar, between `0` and `1`. */
@FloatRange(from = 0.0, to = 1.0) val progress: Float,
- /** The previous button; or `null` if it should be absent in the UI. */
- val previous: MediaSecondaryActionViewModel?,
- /** The next button; or `null` if it should be absent in the UI. */
- val next: MediaSecondaryActionViewModel?,
+ /**
+ * The action button to the left of the seek bar; or `null` if it should be absent in the
+ * UI.
+ */
+ val left: MediaSecondaryActionViewModel,
+ /**
+ * The action button to the right of the seek bar; or `null` if it should be absent in the
+ * UI.
+ */
+ val right: MediaSecondaryActionViewModel,
/**
* Whether the portion of the seek bar track before the thumb should show the squiggle
* animation.
@@ -50,8 +59,8 @@ sealed interface MediaSeekBarViewModel {
* the seek bar). The position/progress should be committed.
*/
val onScrubFinished: () -> Unit,
- ) : MediaSeekBarViewModel
+ ) : MediaNavigationViewModel
/** The seek bar should be hidden. */
- data object Hidden : MediaSeekBarViewModel
+ data object Hidden : MediaNavigationViewModel
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaPlayPauseActionViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaPlayPauseActionViewModel.kt
index 4cb11bc0b8d0..ecc92d778f8e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaPlayPauseActionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaPlayPauseActionViewModel.kt
@@ -21,8 +21,7 @@ import com.android.systemui.media.remedia.shared.model.MediaSessionState
/** Models UI state for the play/pause action button within media controls. */
data class MediaPlayPauseActionViewModel(
- val isVisible: Boolean,
val state: MediaSessionState,
- val icon: Icon.Resource?,
- val onClick: () -> Unit,
+ val icon: Icon?,
+ val onClick: (() -> Unit)?,
)
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSecondaryActionViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSecondaryActionViewModel.kt
index a4806800a7b1..d28ca7ab7121 100644
--- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSecondaryActionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSecondaryActionViewModel.kt
@@ -19,8 +19,10 @@ package com.android.systemui.media.remedia.ui.viewmodel
import com.android.systemui.common.shared.model.Icon
/** Models UI state for a secondary action button within media controls. */
-data class MediaSecondaryActionViewModel(
- val icon: Icon,
- val isEnabled: Boolean,
- val onClick: () -> Unit,
-)
+sealed interface MediaSecondaryActionViewModel {
+ data class Action(val icon: Icon, val onClick: (() -> Unit)?) : MediaSecondaryActionViewModel
+
+ data object ReserveSpace : MediaSecondaryActionViewModel
+
+ data object None : MediaSecondaryActionViewModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaViewModel.kt
new file mode 100644
index 000000000000..b4f3d2724e75
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaViewModel.kt
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.remedia.ui.viewmodel
+
+import android.content.Context
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.graphics.ImageBitmap
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.media.remedia.domain.interactor.MediaInteractor
+import com.android.systemui.media.remedia.domain.model.MediaActionModel
+import com.android.systemui.media.remedia.shared.model.MediaColorScheme
+import com.android.systemui.media.remedia.shared.model.MediaSessionState
+import com.android.systemui.res.R
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlin.math.roundToLong
+import kotlinx.coroutines.awaitCancellation
+
+/** Models UI state for a media element. */
+class MediaViewModel
+@AssistedInject
+constructor(
+ private val interactor: MediaInteractor,
+ @Assisted private val context: Context,
+ @Assisted private val carouselVisibility: MediaCarouselVisibility,
+) : ExclusiveActivatable() {
+
+ /** Whether the user is actively moving the thumb of the seek bar. */
+ private var isScrubbing: Boolean by mutableStateOf(false)
+ /** The position of the thumb of the seek bar as the user is scrubbing it. */
+ private var seekProgress: Float by mutableFloatStateOf(0f)
+ /** Whether the internal "guts" are visible. */
+ private var isGutsVisible: Boolean by mutableStateOf(false)
+ /** The index of the currently-selected card. */
+ private var selectedCardIndex: Int by mutableIntStateOf(0)
+ private set
+
+ /** The current list of cards to show in the UI. */
+ val cards: List<MediaCardViewModel> by derivedStateOf {
+ interactor.sessions.mapIndexed { sessionIndex, session ->
+ val isCurrentSessionAndScrubbing = isScrubbing && sessionIndex == selectedCardIndex
+ object : MediaCardViewModel {
+ override val key = session.key
+ override val icon = session.appIcon
+ override val background: ImageBitmap?
+ get() = session.background
+
+ override val colorScheme: MediaColorScheme
+ get() = session.colorScheme
+
+ override val title = session.title
+ override val subtitle = session.subtitle
+ override val actionButtonLayout = session.actionButtonLayout
+ override val playPauseAction =
+ session.playPauseAction.toPlayPauseActionViewModel(session.state)
+ override val additionalActions: List<MediaSecondaryActionViewModel>
+ get() {
+ return session.additionalActions.map { action ->
+ action.toSecondaryActionViewModel()
+ }
+ }
+
+ override val navigation: MediaNavigationViewModel
+ get() {
+ return if (session.canBeScrubbed) {
+ MediaNavigationViewModel.Showing(
+ progress =
+ if (!isCurrentSessionAndScrubbing) {
+ session.positionMs.toFloat() / session.durationMs
+ } else {
+ seekProgress
+ },
+ left = session.leftAction.toSecondaryActionViewModel(),
+ right = session.rightAction.toSecondaryActionViewModel(),
+ isSquiggly =
+ session.state != MediaSessionState.Paused &&
+ !isCurrentSessionAndScrubbing,
+ isScrubbing = isCurrentSessionAndScrubbing,
+ onScrubChange = { progress ->
+ check(selectedCardIndex == sessionIndex) {
+ "Can't seek on a card that's not the selected card!"
+ }
+ isScrubbing = true
+ seekProgress = progress
+ },
+ onScrubFinished = {
+ interactor.seek(
+ sessionKey = session.key,
+ to = (seekProgress * session.durationMs).roundToLong(),
+ )
+ isScrubbing = false
+ },
+ )
+ } else {
+ MediaNavigationViewModel.Hidden
+ }
+ }
+
+ override val guts: MediaCardGutsViewModel
+ get() {
+ return MediaCardGutsViewModel(
+ isVisible = isGutsVisible,
+ text =
+ if (session.canBeHidden) {
+ context.getString(
+ R.string.controls_media_close_session,
+ session.appName,
+ )
+ } else {
+ context.getString(R.string.controls_media_active_session)
+ },
+ primaryAction =
+ if (session.canBeHidden) {
+ MediaGutsButtonViewModel(
+ text =
+ context.getString(
+ R.string.controls_media_dismiss_button
+ ),
+ onClick = {
+ interactor.hide(session.key)
+ isGutsVisible = false
+ },
+ )
+ } else {
+ MediaGutsButtonViewModel(
+ text = context.getString(R.string.cancel),
+ onClick = { isGutsVisible = false },
+ )
+ },
+ secondaryAction =
+ MediaGutsButtonViewModel(
+ text = context.getString(R.string.cancel),
+ onClick = { isGutsVisible = false },
+ )
+ .takeIf { session.canBeHidden },
+ settingsButton =
+ MediaGutsSettingsButtonViewModel(
+ icon =
+ Icon.Resource(
+ res = R.drawable.ic_settings,
+ contentDescription =
+ ContentDescription.Resource(
+ res = R.string.controls_media_settings_button
+ ),
+ ),
+ onClick = { interactor.openMediaSettings() },
+ ),
+ onLongClick = { isGutsVisible = false },
+ )
+ }
+
+ override val outputSwitcherChips: List<MediaOutputSwitcherChipViewModel>
+ get() {
+ return listOf(
+ MediaOutputSwitcherChipViewModel(
+ icon = session.outputDevice.icon,
+ text = session.outputDevice.name,
+ onClick = {
+ // TODO(b/397989775): tell the UI to show the output switcher.
+ },
+ )
+ )
+ }
+
+ override val outputSwitcherChipButton: MediaSecondaryActionViewModel.Action
+ get() {
+ return MediaSecondaryActionViewModel.Action(
+ icon = session.outputDevice.icon,
+ onClick = {
+ // TODO(b/397989775): tell the UI to show the output switcher.
+ },
+ )
+ }
+
+ override val onClick = session.onClick
+ override val onClickLabel =
+ context.getString(R.string.controls_media_playing_item_description)
+ override val onLongClick = { isGutsVisible = true }
+ }
+ }
+ }
+
+ /** Whether the carousel should be visible. */
+ val isCarouselVisible: Boolean
+ get() =
+ when (carouselVisibility) {
+ MediaCarouselVisibility.WhenNotEmpty -> interactor.sessions.isNotEmpty()
+
+ MediaCarouselVisibility.WhenAnyCardIsActive ->
+ interactor.sessions.any { session -> session.isActive }
+ }
+
+ /** Notifies that the card at [cardIndex] has been selected in the UI. */
+ fun onCardSelected(cardIndex: Int) {
+ check(cardIndex >= 0 && cardIndex < cards.size)
+ selectedCardIndex = cardIndex
+ }
+
+ override suspend fun onActivated(): Nothing {
+ awaitCancellation()
+ }
+
+ private fun MediaActionModel.toPlayPauseActionViewModel(
+ mediaSessionState: MediaSessionState
+ ): MediaPlayPauseActionViewModel? {
+ return when (this) {
+ is MediaActionModel.Action ->
+ MediaPlayPauseActionViewModel(
+ state = mediaSessionState,
+ icon = icon,
+ onClick = onClick ?: {},
+ )
+ is MediaActionModel.None,
+ is MediaActionModel.ReserveSpace -> null
+ }
+ }
+
+ private fun MediaActionModel.toSecondaryActionViewModel(): MediaSecondaryActionViewModel {
+ return when (this) {
+ is MediaActionModel.Action ->
+ MediaSecondaryActionViewModel.Action(icon = icon, onClick = onClick)
+ is MediaActionModel.ReserveSpace -> MediaSecondaryActionViewModel.ReserveSpace
+ is MediaActionModel.None -> MediaSecondaryActionViewModel.None
+ }
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(context: Context, carouselVisibility: MediaCarouselVisibility): MediaViewModel
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
index d8fc52bcc55a..8dc27bf4ac3e 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
@@ -162,10 +162,6 @@ constructor(
): Boolean {
return this@NoteTaskInitializer.handleKeyGestureEvent(event)
}
-
- override fun isKeyGestureSupported(gestureType: Int): Boolean {
- return this@NoteTaskInitializer.isKeyGestureSupported(gestureType)
- }
}
/**
@@ -225,10 +221,6 @@ constructor(
return true
}
- private fun isKeyGestureSupported(gestureType: Int): Boolean {
- return gestureType == KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES
- }
-
companion object {
val MULTI_PRESS_TIMEOUT = ViewConfiguration.getMultiPressTimeout().toLong()
val LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout().toLong()
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 f1f5b267f9c1..8920c86282da 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
@@ -18,6 +18,7 @@ package com.android.systemui.qs.composefragment
import android.annotation.SuppressLint
import android.content.Context
+import android.content.res.Configuration
import android.graphics.PointF
import android.graphics.Rect
import android.os.Bundle
@@ -48,6 +49,7 @@ import androidx.compose.foundation.layout.requiredHeightIn
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -58,6 +60,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerInputChange
@@ -69,6 +72,8 @@ import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.layout.positionOnScreen
import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.CustomAccessibilityAction
@@ -102,6 +107,7 @@ import com.android.mechanics.GestureContext
import com.android.systemui.Dumpable
import com.android.systemui.Flags
import com.android.systemui.brightness.ui.compose.BrightnessSliderContainer
+import com.android.systemui.brightness.ui.compose.ContainerColors
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.dump.DumpManager
import com.android.systemui.keyboard.shortcut.ui.composable.InteractionsConfig
@@ -249,7 +255,7 @@ constructor(
@Composable
private fun Content() {
- PlatformTheme(isDarkTheme = true) {
+ PlatformTheme(isDarkTheme = true /* Delete AlwaysDarkMode when removing this */) {
ProvideShortcutHelperIndication(interactionsConfig = interactionsConfig()) {
// TODO(b/389985793): Make sure that there is no coroutine work or recompositions
// happening when alwaysCompose is true but isQsVisibleAndAnyShadeExpanded is false.
@@ -740,17 +746,26 @@ constructor(
)
val BrightnessSlider =
@Composable {
- BrightnessSliderContainer(
- viewModel = containerViewModel.brightnessSliderViewModel,
- modifier =
+ AlwaysDarkMode {
+ Box(
Modifier.systemGestureExclusionInShade(
- enabled = {
- layoutState.transitionState is
- TransitionState.Idle
- }
- )
- .fillMaxWidth(),
- )
+ enabled = {
+ layoutState.transitionState is TransitionState.Idle
+ }
+ )
+ ) {
+ BrightnessSliderContainer(
+ viewModel =
+ containerViewModel.brightnessSliderViewModel,
+ containerColors =
+ ContainerColors(
+ Color.Transparent,
+ ContainerColors.defaultContainerColor,
+ ),
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ }
}
val TileGrid =
@Composable {
@@ -1226,3 +1241,28 @@ private fun interactionsConfig() =
private inline val alwaysCompose
get() = Flags.alwaysComposeQsUiFragment()
+
+/**
+ * Forces the configuration and themes to be dark theme. This is needed in order to have
+ * [colorResource] retrieve the dark mode colors.
+ *
+ * This should be removed when we remove the force dark mode in [PlatformTheme] at the root of the
+ * compose hierarchy.
+ */
+@Composable
+private fun AlwaysDarkMode(content: @Composable () -> Unit) {
+ val currentConfig = LocalConfiguration.current
+ val darkConfig =
+ Configuration(currentConfig).apply {
+ uiMode =
+ (uiMode and (Configuration.UI_MODE_NIGHT_MASK.inv())) or
+ Configuration.UI_MODE_NIGHT_YES
+ }
+ val newContext = LocalContext.current.createConfigurationContext(darkConfig)
+ CompositionLocalProvider(
+ LocalConfiguration provides darkConfig,
+ LocalContext provides newContext,
+ ) {
+ content()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/ui/dialog/TileRequestDialogComposeDelegate.kt b/packages/SystemUI/src/com/android/systemui/qs/external/ui/dialog/TileRequestDialogComposeDelegate.kt
index 446be9b9ebcb..59844c7ae664 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/ui/dialog/TileRequestDialogComposeDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/ui/dialog/TileRequestDialogComposeDelegate.kt
@@ -85,6 +85,7 @@ constructor(
LargeStaticTile(
uiState = viewModel.uiState,
+ iconProvider = viewModel.iconProvider,
modifier =
Modifier.width(
dimensionResource(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModel.kt
index c756adc07ba4..dd281ccc9c50 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModel.kt
@@ -26,6 +26,7 @@ import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.qs.external.TileData
+import com.android.systemui.qs.panels.ui.viewmodel.toIconProvider
import com.android.systemui.qs.panels.ui.viewmodel.toUiState
import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIcon
import com.android.systemui.qs.tileimpl.QSTileImpl.ResourceIcon
@@ -58,6 +59,8 @@ constructor(
val uiState by derivedStateOf { state.toUiState(dialogContext.resources) }
+ val iconProvider by derivedStateOf { state.toIconProvider() }
+
override suspend fun onActivated(): Nothing {
withContext(backgroundDispatcher) {
tileData.icon
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt
index 701f44e9981c..d40ecc9565ae 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt
@@ -61,6 +61,9 @@ fun TileDetails(modifier: Modifier = Modifier, detailsViewModel: DetailsViewMode
DisposableEffect(Unit) { onDispose { detailsViewModel.closeDetailedView() } }
+ val title = tileDetailedViewModel.title
+ val subTitle = tileDetailedViewModel.subTitle
+
Column(
modifier =
modifier
@@ -90,7 +93,7 @@ fun TileDetails(modifier: Modifier = Modifier, detailsViewModel: DetailsViewMode
)
}
Text(
- text = tileDetailedViewModel.getTitle(),
+ text = title,
modifier = Modifier.align(Alignment.CenterVertically),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleLarge,
@@ -110,7 +113,7 @@ fun TileDetails(modifier: Modifier = Modifier, detailsViewModel: DetailsViewMode
}
}
Text(
- text = tileDetailedViewModel.getSubTitle(),
+ text = subTitle,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleSmall,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
index a56fabcc7dc3..bf63c3858542 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
@@ -20,6 +20,7 @@ package com.android.systemui.qs.panels.ui.compose.infinitegrid
import android.content.Context
import android.content.res.Resources
+import android.os.Trace
import android.service.quicksettings.Tile.STATE_ACTIVE
import android.service.quicksettings.Tile.STATE_INACTIVE
import androidx.compose.animation.animateColorAsState
@@ -47,8 +48,8 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.State
-import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
@@ -59,7 +60,7 @@ import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.role
@@ -69,6 +70,7 @@ import androidx.compose.ui.semantics.toggleableState
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.trace
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.compose.animation.Expandable
import com.android.compose.animation.bounceable
@@ -80,7 +82,6 @@ import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.haptics.msdl.qs.TileHapticsViewModel
import com.android.systemui.haptics.msdl.qs.TileHapticsViewModelFactoryProvider
import com.android.systemui.lifecycle.rememberViewModel
-import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.qs.flags.QsDetailedView
import com.android.systemui.qs.panels.ui.compose.BounceableInfo
import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.InactiveCornerRadius
@@ -88,9 +89,12 @@ import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults
import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.TileHeight
import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.TileStartPadding
import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.longPressLabel
+import com.android.systemui.qs.panels.ui.viewmodel.AccessibilityUiState
import com.android.systemui.qs.panels.ui.viewmodel.DetailsViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.IconProvider
import com.android.systemui.qs.panels.ui.viewmodel.TileUiState
import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.toIconProvider
import com.android.systemui.qs.panels.ui.viewmodel.toUiState
import com.android.systemui.qs.tileimpl.QSTileImpl
import com.android.systemui.qs.ui.compose.borderOnFocus
@@ -119,6 +123,9 @@ fun TileLazyGrid(
)
}
+private val TileViewModel.traceName
+ get() = spec.toString().takeLast(Trace.MAX_SECTION_NAME_LEN)
+
@Composable
fun Tile(
tile: TileViewModel,
@@ -130,105 +137,114 @@ fun Tile(
modifier: Modifier = Modifier,
detailsViewModel: DetailsViewModel?,
) {
- val currentBounceableInfo by rememberUpdatedState(bounceableInfo)
- val resources = resources()
-
- /*
- * Use produce state because [QSTile.State] doesn't have well defined equals (due to
- * inheritance). This way, even if tile.state changes, uiState may not change and lead to
- * recomposition.
- */
- val uiState by
- produceState(tile.currentState.toUiState(resources), tile, resources) {
- tile.state.collect { value = it.toUiState(resources) }
- }
+ trace(tile.traceName) {
+ val currentBounceableInfo by rememberUpdatedState(bounceableInfo)
+ val resources = resources()
+
+ /*
+ * Use produce state because [QSTile.State] doesn't have well defined equals (due to
+ * inheritance). This way, even if tile.state changes, uiState may not change and lead to
+ * recomposition.
+ */
+ val uiState by
+ produceState(tile.currentState.toUiState(resources), tile, resources) {
+ tile.state.collect { value = it.toUiState(resources) }
+ }
- val colors = TileDefaults.getColorForState(uiState, iconOnly)
- val hapticsViewModel: TileHapticsViewModel? =
- rememberViewModel(traceName = "TileHapticsViewModel") {
- tileHapticsViewModelFactoryProvider.getHapticsViewModelFactory()?.create(tile)
- }
+ val icon by
+ produceState(tile.currentState.toIconProvider(), tile) {
+ tile.state.collect { value = it.toIconProvider() }
+ }
- // TODO(b/361789146): Draw the shapes instead of clipping
- val tileShape by TileDefaults.animateTileShapeAsState(uiState.state)
- val animatedColor by animateColorAsState(colors.background, label = "QSTileBackgroundColor")
+ val colors = TileDefaults.getColorForState(uiState, iconOnly)
+ val hapticsViewModel: TileHapticsViewModel? =
+ rememberViewModel(traceName = "TileHapticsViewModel") {
+ tileHapticsViewModelFactoryProvider.getHapticsViewModelFactory()?.create(tile)
+ }
- TileExpandable(
- color = { animatedColor },
- shape = tileShape,
- squishiness = squishiness,
- hapticsViewModel = hapticsViewModel,
- modifier =
- modifier
- .borderOnFocus(color = MaterialTheme.colorScheme.secondary, tileShape.topEnd)
- .fillMaxWidth()
- .bounceable(
- bounceable = currentBounceableInfo.bounceable,
- previousBounceable = currentBounceableInfo.previousTile,
- nextBounceable = currentBounceableInfo.nextTile,
- orientation = Orientation.Horizontal,
- bounceEnd = currentBounceableInfo.bounceEnd,
- ),
- ) { expandable ->
- val longClick: (() -> Unit)? =
- {
- hapticsViewModel?.setTileInteractionState(
- TileHapticsViewModel.TileInteractionState.LONG_CLICKED
+ // TODO(b/361789146): Draw the shapes instead of clipping
+ val tileShape by TileDefaults.animateTileShapeAsState(uiState.state)
+ val animatedColor by animateColorAsState(colors.background, label = "QSTileBackgroundColor")
+
+ TileExpandable(
+ color = { animatedColor },
+ shape = tileShape,
+ squishiness = squishiness,
+ hapticsViewModel = hapticsViewModel,
+ modifier =
+ modifier
+ .borderOnFocus(color = MaterialTheme.colorScheme.secondary, tileShape.topEnd)
+ .fillMaxWidth()
+ .bounceable(
+ bounceable = currentBounceableInfo.bounceable,
+ previousBounceable = currentBounceableInfo.previousTile,
+ nextBounceable = currentBounceableInfo.nextTile,
+ orientation = Orientation.Horizontal,
+ bounceEnd = currentBounceableInfo.bounceEnd,
+ ),
+ ) { expandable ->
+ val longClick: (() -> Unit)? =
+ {
+ hapticsViewModel?.setTileInteractionState(
+ TileHapticsViewModel.TileInteractionState.LONG_CLICKED
+ )
+ tile.onLongClick(expandable)
+ }
+ .takeIf { uiState.handlesLongClick }
+ TileContainer(
+ onClick = {
+ var hasDetails = false
+ if (QsDetailedView.isEnabled) {
+ hasDetails = detailsViewModel?.onTileClicked(tile.spec) == true
+ }
+ if (!hasDetails) {
+ // For those tile's who doesn't have a detailed view, process with their
+ // `onClick` behavior.
+ tile.onClick(expandable)
+ hapticsViewModel?.setTileInteractionState(
+ TileHapticsViewModel.TileInteractionState.CLICKED
+ )
+ if (uiState.accessibilityUiState.toggleableState != null) {
+ coroutineScope.launch {
+ currentBounceableInfo.bounceable.animateBounce()
+ }
+ }
+ }
+ },
+ onLongClick = longClick,
+ accessibilityUiState = uiState.accessibilityUiState,
+ iconOnly = iconOnly,
+ ) {
+ val iconProvider: Context.() -> Icon = { getTileIcon(icon = icon) }
+ if (iconOnly) {
+ SmallTileContent(
+ iconProvider = iconProvider,
+ color = colors.icon,
+ modifier = Modifier.align(Alignment.Center),
)
- tile.onLongClick(expandable)
- }
- .takeIf { uiState.handlesLongClick }
- TileContainer(
- onClick = {
- var hasDetails = false
- if (QsDetailedView.isEnabled) {
- hasDetails = detailsViewModel?.onTileClicked(tile.spec) == true
- }
- if (!hasDetails) {
- // For those tile's who doesn't have a detailed view, process with their
- // `onClick` behavior.
- tile.onClick(expandable)
- hapticsViewModel?.setTileInteractionState(
- TileHapticsViewModel.TileInteractionState.CLICKED
+ } else {
+ val iconShape by TileDefaults.animateIconShapeAsState(uiState.state)
+ val secondaryClick: (() -> Unit)? =
+ {
+ hapticsViewModel?.setTileInteractionState(
+ TileHapticsViewModel.TileInteractionState.CLICKED
+ )
+ tile.onSecondaryClick()
+ }
+ .takeIf { uiState.handlesSecondaryClick }
+ LargeTileContent(
+ label = uiState.label,
+ secondaryLabel = uiState.secondaryLabel,
+ iconProvider = iconProvider,
+ sideDrawable = uiState.sideDrawable,
+ colors = colors,
+ iconShape = iconShape,
+ toggleClick = secondaryClick,
+ onLongClick = longClick,
+ accessibilityUiState = uiState.accessibilityUiState,
+ squishiness = squishiness,
)
- if (uiState.accessibilityUiState.toggleableState != null) {
- coroutineScope.launch { currentBounceableInfo.bounceable.animateBounce() }
- }
}
- },
- onLongClick = longClick,
- uiState = uiState,
- iconOnly = iconOnly,
- ) {
- val iconProvider: Context.() -> Icon = { getTileIcon(icon = uiState.icon) }
- if (iconOnly) {
- SmallTileContent(
- iconProvider = iconProvider,
- color = colors.icon,
- modifier = Modifier.align(Alignment.Center),
- )
- } else {
- val iconShape by TileDefaults.animateIconShapeAsState(uiState.state)
- val secondaryClick: (() -> Unit)? =
- {
- hapticsViewModel?.setTileInteractionState(
- TileHapticsViewModel.TileInteractionState.CLICKED
- )
- tile.onSecondaryClick()
- }
- .takeIf { uiState.handlesSecondaryClick }
- LargeTileContent(
- label = uiState.label,
- secondaryLabel = uiState.secondaryLabel,
- iconProvider = iconProvider,
- sideDrawable = uiState.sideDrawable,
- colors = colors,
- iconShape = iconShape,
- toggleClick = secondaryClick,
- onLongClick = longClick,
- accessibilityUiState = uiState.accessibilityUiState,
- squishiness = squishiness,
- )
}
}
}
@@ -257,7 +273,7 @@ private fun TileExpandable(
fun TileContainer(
onClick: () -> Unit,
onLongClick: (() -> Unit)?,
- uiState: TileUiState,
+ accessibilityUiState: AccessibilityUiState,
iconOnly: Boolean,
content: @Composable BoxScope.() -> Unit,
) {
@@ -268,7 +284,7 @@ fun TileContainer(
.tileCombinedClickable(
onClick = onClick,
onLongClick = onLongClick,
- uiState = uiState,
+ accessibilityUiState = accessibilityUiState,
iconOnly = iconOnly,
)
.sysuiResTag(if (iconOnly) TEST_TAG_SMALL else TEST_TAG_LARGE)
@@ -278,7 +294,11 @@ fun TileContainer(
}
@Composable
-fun LargeStaticTile(uiState: TileUiState, modifier: Modifier = Modifier) {
+fun LargeStaticTile(
+ uiState: TileUiState,
+ iconProvider: IconProvider,
+ modifier: Modifier = Modifier,
+) {
val colors = TileDefaults.getColorForState(uiState = uiState, iconOnly = false)
Box(
@@ -291,7 +311,7 @@ fun LargeStaticTile(uiState: TileUiState, modifier: Modifier = Modifier) {
LargeTileContent(
label = uiState.label,
secondaryLabel = "",
- iconProvider = { getTileIcon(icon = uiState.icon) },
+ iconProvider = { getTileIcon(icon = iconProvider) },
sideDrawable = null,
colors = colors,
squishiness = { 1f },
@@ -299,8 +319,8 @@ fun LargeStaticTile(uiState: TileUiState, modifier: Modifier = Modifier) {
}
}
-private fun Context.getTileIcon(icon: QSTile.Icon?): Icon {
- return icon?.let {
+private fun Context.getTileIcon(icon: IconProvider): Icon {
+ return icon.icon?.let {
if (it is QSTileImpl.ResourceIcon) {
Icon.Resource(it.resId, null)
} else {
@@ -321,28 +341,26 @@ fun Modifier.largeTilePadding(): Modifier {
fun Modifier.tileCombinedClickable(
onClick: () -> Unit,
onLongClick: (() -> Unit)?,
- uiState: TileUiState,
+ accessibilityUiState: AccessibilityUiState,
iconOnly: Boolean,
): Modifier {
val longPressLabel = longPressLabel()
return combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
- onClickLabel = uiState.accessibilityUiState.clickLabel,
+ onClickLabel = accessibilityUiState.clickLabel,
onLongClickLabel = longPressLabel,
hapticFeedbackEnabled = !Flags.msdlFeedback(),
)
.semantics {
- role = uiState.accessibilityUiState.accessibilityRole
- if (uiState.accessibilityUiState.accessibilityRole == Role.Switch) {
- uiState.accessibilityUiState.toggleableState?.let { toggleableState = it }
+ role = accessibilityUiState.accessibilityRole
+ if (accessibilityUiState.accessibilityRole == Role.Switch) {
+ accessibilityUiState.toggleableState?.let { toggleableState = it }
}
- stateDescription = uiState.accessibilityUiState.stateDescription
+ stateDescription = accessibilityUiState.stateDescription
}
.thenIf(iconOnly) {
- Modifier.semantics {
- contentDescription = uiState.accessibilityUiState.contentDescription
- }
+ Modifier.semantics { contentDescription = accessibilityUiState.contentDescription }
}
}
@@ -474,14 +492,15 @@ private object TileDefaults {
label = label,
)
- val corner = remember {
- object : CornerSize {
- override fun toPx(shapeSize: Size, density: Density): Float {
- return with(density) { animatedCornerRadius.toPx() }
+ return remember {
+ val corner =
+ object : CornerSize {
+ override fun toPx(shapeSize: Size, density: Density): Float {
+ return with(density) { animatedCornerRadius.toPx() }
+ }
}
- }
+ mutableStateOf(RoundedCornerShape(corner))
}
- return remember { derivedStateOf { RoundedCornerShape(corner) } }
}
}
@@ -493,5 +512,5 @@ private object TileDefaults {
@ReadOnlyComposable
private fun resources(): Resources {
LocalConfiguration.current
- return LocalContext.current.resources
+ return LocalResources.current
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
index 153238fc91c9..a66b51f6fe50 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
@@ -59,12 +59,14 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.toSize
import androidx.compose.ui.zIndex
import com.android.compose.modifiers.size
import com.android.compose.modifiers.thenIf
import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.InactiveCornerRadius
import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BADGE_ANGLE_RAD
+import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BadgeIconSize
import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BadgeSize
import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BadgeXOffset
import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BadgeYOffset
@@ -149,16 +151,14 @@ fun InteractiveTileContainer(
onClick = onClick,
)
) {
+ val size = with(LocalDensity.current) { BadgeIconSize.toDp() }
Icon(
Icons.Default.Remove,
contentDescription = null,
modifier =
- Modifier.size(
- width = { decorationSize.width.roundToInt() },
- height = { decorationSize.height.roundToInt() },
- )
- .align(Alignment.Center)
- .graphicsLayer { this.alpha = badgeIconAlpha },
+ Modifier.size(size).align(Alignment.Center).graphicsLayer {
+ this.alpha = badgeIconAlpha
+ },
)
}
}
@@ -219,12 +219,13 @@ fun StaticTileBadge(
}
) {
val secondaryColor = MaterialTheme.colorScheme.secondary
+ val size = with(LocalDensity.current) { BadgeIconSize.toDp() }
Icon(
icon,
contentDescription = contentDescription,
modifier =
- Modifier.size(BadgeSize).align(Alignment.Center).drawBehind {
- drawCircle(secondaryColor)
+ Modifier.size(size).align(Alignment.Center).drawBehind {
+ drawCircle(secondaryColor, radius = BadgeSize.toPx() / 2)
},
)
}
@@ -338,6 +339,7 @@ private fun offsetForAngle(angle: Float, radius: Float, center: Offset): Offset
private object SelectionDefaults {
val SelectedBorderWidth = 2.dp
val BadgeSize = 24.dp
+ val BadgeIconSize = 16.sp
val BadgeXOffset = -4.dp
val BadgeYOffset = 4.dp
val ResizingPillWidth = 8.dp
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt
index 99f52c28a137..3ae90d2f976b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt
@@ -16,14 +16,20 @@
package com.android.systemui.qs.panels.ui.compose.toolbar
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.shape.CornerSize
+import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
import com.android.systemui.compose.modifiers.sysuiResTag
+import com.android.systemui.development.ui.compose.BuildNumber
import com.android.systemui.qs.footer.ui.compose.IconButton
import com.android.systemui.qs.panels.ui.viewmodel.toolbar.ToolbarViewModel
+import com.android.systemui.qs.ui.compose.borderOnFocus
@Composable
fun Toolbar(viewModel: ToolbarViewModel, modifier: Modifier = Modifier) {
@@ -44,7 +50,18 @@ fun Toolbar(viewModel: ToolbarViewModel, modifier: Modifier = Modifier) {
Modifier.sysuiResTag("settings_button_container"),
)
- Spacer(modifier = Modifier.weight(1f))
+ Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.Center) {
+ BuildNumber(
+ viewModelFactory = viewModel.buildNumberViewModelFactory,
+ textColor = MaterialTheme.colorScheme.onSurface,
+ modifier =
+ Modifier.borderOnFocus(
+ color = MaterialTheme.colorScheme.secondary,
+ cornerSize = CornerSize(1.dp),
+ )
+ .wrapContentSize(),
+ )
+ }
IconButton(
{ viewModel.powerButtonViewModel },
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModel.kt
index 03f0297e0d54..3287443f0405 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModel.kt
@@ -16,6 +16,7 @@
package com.android.systemui.qs.panels.ui.viewmodel
+import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import com.android.systemui.dagger.SysUISingleton
@@ -25,6 +26,7 @@ import com.android.systemui.qs.pipeline.shared.TileSpec
import javax.inject.Inject
@SysUISingleton
+@Stable
class DetailsViewModel @Inject constructor(val currentTilesInteractor: CurrentTilesInteractor) {
/**
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt
index 19e542e6a21e..15e71c88bea1 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt
@@ -22,12 +22,18 @@ import android.service.quicksettings.Tile
import android.text.TextUtils
import android.widget.Switch
import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.state.ToggleableState
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.qs.tileimpl.SubtitleArrayMapping
import com.android.systemui.res.R
+import java.util.function.Supplier
+/**
+ * Ui State for the tiles. It doesn't contain the icon to be able to invalidate the icon part
+ * separately. For the icon, use [IconProvider].
+ */
@Immutable
data class TileUiState(
val label: String,
@@ -35,7 +41,6 @@ data class TileUiState(
val state: Int,
val handlesLongClick: Boolean,
val handlesSecondaryClick: Boolean,
- val icon: QSTile.Icon?,
val sideDrawable: Drawable?,
val accessibilityUiState: AccessibilityUiState,
)
@@ -90,7 +95,6 @@ fun QSTile.State.toUiState(resources: Resources): TileUiState {
state = if (disabledByPolicy) Tile.STATE_UNAVAILABLE else state,
handlesLongClick = handlesLongClick,
handlesSecondaryClick = handlesSecondaryClick,
- icon = icon ?: iconSupplier?.get(),
sideDrawable = sideViewCustomDrawable,
AccessibilityUiState(
contentDescription?.toString() ?: "",
@@ -104,6 +108,14 @@ fun QSTile.State.toUiState(resources: Resources): TileUiState {
)
}
+fun QSTile.State.toIconProvider(): IconProvider {
+ return when {
+ icon != null -> IconProvider.ConstantIcon(icon)
+ iconSupplier != null -> IconProvider.IconSupplier(iconSupplier)
+ else -> IconProvider.Empty
+ }
+}
+
private fun QSTile.State.getStateText(resources: Resources): CharSequence {
val arrayResId = SubtitleArrayMapping.getSubtitleId(spec)
val array = resources.getStringArray(arrayResId)
@@ -114,3 +126,21 @@ private fun getUnavailableText(spec: String?, resources: Resources): String {
val arrayResId = SubtitleArrayMapping.getSubtitleId(spec)
return resources.getStringArray(arrayResId)[Tile.STATE_UNAVAILABLE]
}
+
+@Stable
+sealed interface IconProvider {
+
+ val icon: QSTile.Icon?
+
+ data class ConstantIcon(override val icon: QSTile.Icon) : IconProvider
+
+ data class IconSupplier(val supplier: Supplier<QSTile.Icon?>) : IconProvider {
+ override val icon: QSTile.Icon?
+ get() = supplier.get()
+ }
+
+ data object Empty : IconProvider {
+ override val icon: QSTile.Icon?
+ get() = null
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt
index e54bfa29d2db..10d7871b8ea2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt
@@ -24,6 +24,7 @@ import androidx.compose.runtime.setValue
import com.android.systemui.animation.Expandable
import com.android.systemui.classifier.domain.interactor.FalsingInteractor
import com.android.systemui.classifier.domain.interactor.runIfNotFalseTap
+import com.android.systemui.development.ui.viewmodel.BuildNumberViewModel
import com.android.systemui.globalactions.GlobalActionsDialogLite
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
@@ -46,6 +47,7 @@ class ToolbarViewModel
@AssistedInject
constructor(
editModeButtonViewModelFactory: EditModeButtonViewModel.Factory,
+ val buildNumberViewModelFactory: BuildNumberViewModel.Factory,
private val footerActionsInteractor: FooterActionsInteractor,
private val globalActionsDialogLiteProvider: Provider<GlobalActionsDialogLite>,
private val falsingInteractor: FalsingInteractor,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt
index 7d396c58630e..8ffba1e5f3dd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt
@@ -19,26 +19,14 @@ package com.android.systemui.qs.tiles.dialog
import android.view.LayoutInflater
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import com.android.systemui.res.R
@Composable
fun InternetDetailsContent(viewModel: InternetDetailsViewModel) {
val coroutineScope = rememberCoroutineScope()
- val context = LocalContext.current
-
- val internetDetailsContentManager = remember {
- viewModel.contentManagerFactory.create(
- canConfigMobileData = viewModel.getCanConfigMobileData(),
- canConfigWifi = viewModel.getCanConfigWifi(),
- coroutineScope = coroutineScope,
- context = context,
- )
- }
AndroidView(
modifier = Modifier.fillMaxSize(),
@@ -46,11 +34,11 @@ fun InternetDetailsContent(viewModel: InternetDetailsViewModel) {
// Inflate with the existing dialog xml layout and bind it with the manager
val view =
LayoutInflater.from(context).inflate(R.layout.internet_connectivity_dialog, null)
- internetDetailsContentManager.bind(view)
+ viewModel.internetDetailsContentManager.bind(view, coroutineScope)
view
// TODO: b/377388104 - Polish the internet details view UI
},
- onRelease = { internetDetailsContentManager.unBind() },
+ onRelease = { viewModel.internetDetailsContentManager.unBind() },
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt
index 659488bdd0d3..d8e1755e6cca 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt
@@ -43,6 +43,9 @@ import android.widget.Switch
import android.widget.TextView
import androidx.annotation.MainThread
import androidx.annotation.WorkerThread
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
@@ -79,8 +82,6 @@ constructor(
private val internetDetailsContentController: InternetDetailsContentController,
@Assisted(CAN_CONFIG_MOBILE_DATA) private val canConfigMobileData: Boolean,
@Assisted(CAN_CONFIG_WIFI) private val canConfigWifi: Boolean,
- @Assisted private val coroutineScope: CoroutineScope,
- @Assisted private var context: Context,
private val uiEventLogger: UiEventLogger,
@Main private val handler: Handler,
@Background private val backgroundExecutor: Executor,
@@ -121,26 +122,29 @@ constructor(
private lateinit var shareWifiButton: Button
private lateinit var airplaneModeButton: Button
private var alertDialog: AlertDialog? = null
-
- private val canChangeWifiState =
- WifiEnterpriseRestrictionUtils.isChangeWifiStateAllowed(context)
+ private var canChangeWifiState = false
private var wifiNetworkHeight = 0
private var backgroundOn: Drawable? = null
private var backgroundOff: Drawable? = null
private var clickJob: Job? = null
private var defaultDataSubId = internetDetailsContentController.defaultDataSubscriptionId
- @VisibleForTesting
- internal var adapter = InternetAdapter(internetDetailsContentController, coroutineScope)
+ @VisibleForTesting internal lateinit var adapter: InternetAdapter
@VisibleForTesting internal var wifiEntriesCount: Int = 0
@VisibleForTesting internal var hasMoreWifiEntries: Boolean = false
+ private lateinit var context: Context
+ private lateinit var coroutineScope: CoroutineScope
+
+ var title by mutableStateOf("")
+ private set
+
+ var subTitle by mutableStateOf("")
+ private set
@AssistedFactory
interface Factory {
fun create(
@Assisted(CAN_CONFIG_MOBILE_DATA) canConfigMobileData: Boolean,
@Assisted(CAN_CONFIG_WIFI) canConfigWifi: Boolean,
- coroutineScope: CoroutineScope,
- context: Context,
): InternetDetailsContentManager
}
@@ -152,12 +156,16 @@ constructor(
*
* @param contentView The view to which the content manager should be bound.
*/
- fun bind(contentView: View) {
+ fun bind(contentView: View, coroutineScope: CoroutineScope) {
if (DEBUG) {
Log.d(TAG, "Bind InternetDetailsContentManager")
}
this.contentView = contentView
+ context = contentView.context
+ this.coroutineScope = coroutineScope
+ adapter = InternetAdapter(internetDetailsContentController, coroutineScope)
+ canChangeWifiState = WifiEnterpriseRestrictionUtils.isChangeWifiStateAllowed(context)
initializeLifecycle()
initializeViews()
@@ -323,11 +331,11 @@ constructor(
}
}
- fun getTitleText(): String {
+ private fun getTitleText(): String {
return internetDetailsContentController.getDialogTitleText().toString()
}
- fun getSubtitleText(): String {
+ private fun getSubtitleText(): String {
return internetDetailsContentController.getSubtitleText(isProgressBarVisible).toString()
}
@@ -336,6 +344,13 @@ constructor(
Log.d(TAG, "updateDetailsUI ")
}
+ if (!::context.isInitialized) {
+ return
+ }
+
+ title = getTitleText()
+ subTitle = getSubtitleText()
+
airplaneModeButton.visibility =
if (internetContent.isAirplaneModeEnabled) View.VISIBLE else View.GONE
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt
index 6709fd2bb508..fb63bea4fb9f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt
@@ -28,38 +28,27 @@ class InternetDetailsViewModel
@AssistedInject
constructor(
private val accessPointController: AccessPointController,
- val contentManagerFactory: InternetDetailsContentManager.Factory,
+ private val contentManagerFactory: InternetDetailsContentManager.Factory,
private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
-) : TileDetailsViewModel() {
- override fun clickOnSettingsButton() {
- qsTileIntentUserActionHandler.handle(
- /* expandable= */ null,
- Intent(Settings.ACTION_WIFI_SETTINGS),
+) : TileDetailsViewModel {
+ val internetDetailsContentManager by lazy {
+ contentManagerFactory.create(
+ canConfigMobileData = accessPointController.canConfigMobileData(),
+ canConfigWifi = accessPointController.canConfigWifi(),
)
}
- override fun getTitle(): String {
- // TODO: b/377388104 make title and sub title mutable states of string
- // by internetDetailsContentManager.getTitleText()
- // TODO: test title change between airplane mode and not airplane mode
- // TODO: b/377388104 Update the placeholder text
- return "Internet"
- }
+ override val title: String
+ get() = internetDetailsContentManager.title
- override fun getSubTitle(): String {
- // TODO: b/377388104 make title and sub title mutable states of string
- // by internetDetailsContentManager.getSubtitleText()
- // TODO: test subtitle change between airplane mode and not airplane mode
- // TODO: b/377388104 Update the placeholder text
- return "Tab a network to connect"
- }
+ override val subTitle: String
+ get() = internetDetailsContentManager.subTitle
- fun getCanConfigMobileData(): Boolean {
- return accessPointController.canConfigMobileData()
- }
-
- fun getCanConfigWifi(): Boolean {
- return accessPointController.canConfigWifi()
+ override fun clickOnSettingsButton() {
+ qsTileIntentUserActionHandler.handle(
+ /* expandable= */ null,
+ Intent(Settings.ACTION_WIFI_SETTINGS),
+ )
}
@AssistedFactory
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt
index 9a39c3c095ef..4f7e03bd3bc3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt
@@ -23,18 +23,14 @@ import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogView
class ModesDetailsViewModel(
private val onSettingsClick: () -> Unit,
val viewModel: ModesDialogViewModel,
-) : TileDetailsViewModel() {
+) : TileDetailsViewModel {
override fun clickOnSettingsButton() {
onSettingsClick()
}
- override fun getTitle(): String {
- // TODO(b/388321032): Replace this string with a string in a translatable xml file.
- return "Modes"
- }
+ // TODO(b/388321032): Replace this string with a string in a translatable xml file.
+ override val title = "Modes"
- override fun getSubTitle(): String {
- // TODO(b/388321032): Replace this string with a string in a translatable xml file.
- return "Silences interruptions from people and apps in different circumstances"
- }
+ // TODO(b/388321032): Replace this string with a string in a translatable xml file.
+ override val subTitle = "Silences interruptions from people and apps in different circumstances"
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt
index c84ddb6fdb36..59f209edb546 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt
@@ -23,19 +23,15 @@ import com.android.systemui.screenrecord.RecordingController
class ScreenRecordDetailsViewModel(
val recordingController: RecordingController,
val onStartRecordingClicked: Runnable,
-) : TileDetailsViewModel() {
+) : TileDetailsViewModel {
override fun clickOnSettingsButton() {
// No settings button in this tile.
}
- override fun getTitle(): String {
- // TODO(b/388321032): Replace this string with a string in a translatable xml file,
- return "Screen recording"
- }
+ // TODO(b/388321032): Replace this string with a string in a translatable xml file,
+ override val title = "Screen recording"
- override fun getSubTitle(): String {
- // No sub-title in this tile.
- return ""
- }
+ // No sub-title in this tile.
+ override val subTitle = ""
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractor.kt
index 5e7172ee3ba7..268efcef9062 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractor.kt
@@ -21,6 +21,7 @@ import android.provider.Settings
import com.android.systemui.accessibility.hearingaid.HearingDevicesDialogManager
import com.android.systemui.accessibility.hearingaid.HearingDevicesUiEventLogger.Companion.LAUNCH_SOURCE_QS_TILE
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.shared.QSSettingsPackageRepository
import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
import com.android.systemui.qs.tiles.base.interactor.QSTileInput
import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
@@ -37,6 +38,7 @@ constructor(
@Main private val mainContext: CoroutineContext,
private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
private val hearingDevicesDialogManager: HearingDevicesDialogManager,
+ private val settingsPackageRepository: QSSettingsPackageRepository,
) : QSTileUserActionInteractor<HearingDevicesTileModel> {
override suspend fun handleInput(input: QSTileInput<HearingDevicesTileModel>) =
@@ -53,7 +55,8 @@ constructor(
is QSTileUserAction.LongClick -> {
qsTileIntentUserActionHandler.handle(
action.expandable,
- Intent(Settings.ACTION_HEARING_DEVICES_SETTINGS),
+ Intent(Settings.ACTION_HEARING_DEVICES_SETTINGS)
+ .setPackage(settingsPackageRepository.getSettingsPackageName()),
)
}
is QSTileUserAction.ToggleClick -> {}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index 4753b9ac0457..7bb831baec20 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -687,7 +687,7 @@ constructor(
if (!isDeviceEntered) {
coroutineScope {
launch {
- deviceEntryHapticsInteractor.playSuccessHaptic
+ deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry
.sample(sceneInteractor.currentScene)
.collect { currentScene ->
if (Flags.msdlFeedback()) {
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
index eae0ba66925d..ade62a9b63d3 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
@@ -164,16 +164,17 @@ public class BrightnessDialog extends Activity {
container.setVisibility(View.VISIBLE);
ViewGroup.MarginLayoutParams lp =
(ViewGroup.MarginLayoutParams) container.getLayoutParams();
- int horizontalMargin =
- getResources().getDimensionPixelSize(R.dimen.notification_side_paddings);
- lp.leftMargin = horizontalMargin;
- lp.rightMargin = horizontalMargin;
-
- int verticalMargin =
- getResources().getDimensionPixelSize(
- R.dimen.notification_guts_option_vertical_padding);
-
- lp.topMargin = verticalMargin;
+ // Remove the margin. Have the container take all the space. Instead, insert padding.
+ // This allows for the background to be visible around the slider.
+ int margin = 0;
+ lp.topMargin = margin;
+ lp.bottomMargin = margin;
+ lp.leftMargin = margin;
+ lp.rightMargin = margin;
+ int padding = getResources().getDimensionPixelSize(
+ R.dimen.rounded_slider_background_padding
+ );
+ container.setPadding(padding, padding, padding, padding);
// If in multi-window or freeform, increase the top margin so the brightness dialog
// doesn't get cut off.
final int windowingMode = configuration.windowConfiguration.getWindowingMode();
@@ -182,17 +183,15 @@ public class BrightnessDialog extends Activity {
lp.topMargin += 50;
}
- lp.bottomMargin = verticalMargin;
-
int orientation = configuration.orientation;
int windowWidth = getWindowAvailableWidth();
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
boolean shouldBeFullWidth = getIntent()
.getBooleanExtra(EXTRA_BRIGHTNESS_DIALOG_IS_FULL_WIDTH, false);
- lp.width = (shouldBeFullWidth ? windowWidth : windowWidth / 2) - horizontalMargin * 2;
+ lp.width = (shouldBeFullWidth ? windowWidth : windowWidth / 2) - margin * 2;
} else if (orientation == Configuration.ORIENTATION_PORTRAIT) {
- lp.width = windowWidth - horizontalMargin * 2;
+ lp.width = windowWidth - margin * 2;
}
container.setLayoutParams(lp);
@@ -202,7 +201,7 @@ public class BrightnessDialog extends Activity {
// Exclude this view (and its horizontal margins) from triggering gestures.
// This prevents back gesture from being triggered by dragging close to the
// edge of the slider (0% or 100%).
- bounds.set(-horizontalMargin, 0, right - left + horizontalMargin, bottom - top);
+ bounds.set(-margin, 0, right - left + margin, bottom - top);
v.setSystemGestureExclusionRects(List.of(bounds));
});
}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/ComposeDialogComposableProvider.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/ComposeDialogComposableProvider.kt
index 9e20055de856..962a3bd2376b 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/ComposeDialogComposableProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/ComposeDialogComposableProvider.kt
@@ -17,12 +17,15 @@
package com.android.systemui.settings.brightness
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.compose.ui.unit.dp
import com.android.compose.theme.PlatformTheme
import com.android.systemui.brightness.ui.compose.BrightnessSliderContainer
+import com.android.systemui.brightness.ui.compose.ContainerColors
import com.android.systemui.brightness.ui.viewmodel.BrightnessSliderViewModel
import com.android.systemui.lifecycle.rememberViewModel
@@ -44,7 +47,11 @@ private fun BrightnessSliderForDialog(
rememberViewModel(traceName = "BrightnessDialog.viewModel") {
brightnessSliderViewModelFactory.create(false)
}
- BrightnessSliderContainer(viewModel = viewModel, Modifier.fillMaxWidth())
+ BrightnessSliderContainer(
+ viewModel = viewModel,
+ containerColors = ContainerColors.singleColor(ContainerColors.defaultContainerColor),
+ modifier = Modifier.fillMaxWidth().padding(8.dp),
+ )
}
class ComposableProvider(
diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
index 3be2f1b7b957..362b5db012e1 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
@@ -17,6 +17,7 @@
package com.android.systemui.shade
import android.content.Context
+import android.content.res.Configuration
import android.graphics.Rect
import android.os.PowerManager
import android.os.SystemClock
@@ -25,11 +26,13 @@ import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
+import android.view.WindowInsets
import android.widget.FrameLayout
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.OnBackPressedDispatcherOwner
import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
import androidx.compose.ui.platform.ComposeView
+import androidx.core.view.updateMargins
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleObserver
@@ -101,7 +104,10 @@ constructor(
) : LifecycleOwner {
private val logger = Logger(logBuffer, TAG)
- private class CommunalWrapper(context: Context) : FrameLayout(context) {
+ private class CommunalWrapper(
+ context: Context,
+ private val communalSettingsInteractor: CommunalSettingsInteractor,
+ ) : FrameLayout(context) {
private val consumers: MutableSet<Consumer<Boolean>> = ArraySet()
override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
@@ -121,6 +127,24 @@ constructor(
consumers.clear()
}
}
+
+ override fun onApplyWindowInsets(windowInsets: WindowInsets): WindowInsets {
+ if (
+ !communalSettingsInteractor.isV2FlagEnabled() ||
+ resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
+ ) {
+ return super.onApplyWindowInsets(windowInsets)
+ }
+ val type = WindowInsets.Type.displayCutout()
+ val insets = windowInsets.getInsets(type)
+
+ // Reset horizontal margins added by window insets, so hub can be edge to edge.
+ if (insets.left > 0 || insets.right > 0) {
+ val lp = layoutParams as LayoutParams
+ lp.updateMargins(0, lp.topMargin, 0, lp.bottomMargin)
+ }
+ return WindowInsets.CONSUMED
+ }
}
/** The container view for the hub. This will not be initialized until [initView] is called. */
@@ -443,7 +467,8 @@ constructor(
collectFlow(containerView, keyguardInteractor.isDreaming, { isDreaming = it })
collectFlow(containerView, communalViewModel.swipeToHubEnabled, { swipeToHubEnabled = it })
- communalContainerWrapper = CommunalWrapper(containerView.context)
+ communalContainerWrapper =
+ CommunalWrapper(containerView.context, communalSettingsInteractor)
communalContainerWrapper?.addView(communalContainerView)
logger.d("Hub container initialized")
return communalContainerWrapper!!
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index 305444f7ab5e..dafb1a559d59 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -30,8 +30,6 @@ import android.graphics.Region;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.Trace;
-import android.os.UserHandle;
-import android.provider.Settings;
import android.util.Log;
import android.view.Display;
import android.view.IWindow;
@@ -75,7 +73,6 @@ import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
-import com.android.systemui.util.settings.SecureSettings;
import dagger.Lazy;
@@ -134,7 +131,6 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW
private final SysuiColorExtractor mColorExtractor;
private final NotificationShadeWindowModel mNotificationShadeWindowModel;
- private final SecureSettings mSecureSettings;
/**
* Layout params would be aggregated and dispatched all at once if this is > 0.
*
@@ -168,7 +164,6 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW
Lazy<SelectedUserInteractor> userInteractor,
UserTracker userTracker,
NotificationShadeWindowModel notificationShadeWindowModel,
- SecureSettings secureSettings,
Lazy<CommunalInteractor> communalInteractor,
@ShadeDisplayAware LayoutParams shadeWindowLayoutParams) {
mContext = context;
@@ -186,7 +181,6 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW
mBackgroundExecutor = backgroundExecutor;
mColorExtractor = colorExtractor;
mNotificationShadeWindowModel = notificationShadeWindowModel;
- mSecureSettings = secureSettings;
// prefix with {slow} to make sure this dumps at the END of the critical section.
dumpManager.registerCriticalDumpable("{slow}NotificationShadeWindowControllerImpl", this);
mAuthController = authController;
@@ -424,7 +418,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW
(long) mLpChanged.preferredMaxDisplayRefreshRate);
}
- if (state.bouncerShowing && !isSecureWindowsDisabled()) {
+ if (state.bouncerShowing) {
mLpChanged.flags |= LayoutParams.FLAG_SECURE;
} else {
mLpChanged.flags &= ~LayoutParams.FLAG_SECURE;
@@ -437,13 +431,6 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW
}
}
- private boolean isSecureWindowsDisabled() {
- return mSecureSettings.getIntForUser(
- Settings.Secure.DISABLE_SECURE_WINDOWS,
- 0,
- UserHandle.USER_CURRENT) == 1;
- }
-
private void adjustScreenOrientation(NotificationShadeWindowState state) {
if (state.bouncerShowing || state.isKeyguardShowingAndNotOccluded() || state.dozing) {
if (mKeyguardStateController.isKeyguardScreenRotationAllowed()) {
@@ -511,17 +498,18 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW
}
private boolean isExpanded(NotificationShadeWindowState state) {
+ boolean visForBlur = !Flags.disableShadeVisibleWithBlur() && state.backgroundBlurRadius > 0;
boolean isExpanded = !state.forceWindowCollapsed && (state.isKeyguardShowingAndNotOccluded()
|| state.panelVisible || state.keyguardFadingAway || state.bouncerShowing
|| state.headsUpNotificationShowing
|| state.scrimsVisibility != ScrimController.TRANSPARENT)
- || state.backgroundBlurRadius > 0
+ || visForBlur
|| state.launchingActivityFromNotification;
mLogger.logIsExpanded(isExpanded, state.forceWindowCollapsed,
state.isKeyguardShowingAndNotOccluded(), state.panelVisible,
state.keyguardFadingAway, state.bouncerShowing, state.headsUpNotificationShowing,
state.scrimsVisibility != ScrimController.TRANSPARENT,
- state.backgroundBlurRadius > 0, state.launchingActivityFromNotification);
+ visForBlur, state.launchingActivityFromNotification);
return isExpanded;
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
index 20b44d73e097..5609326362fc 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
@@ -26,6 +26,7 @@ import androidx.compose.material3.ColorScheme
import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.Color
import com.android.app.tracing.coroutines.launchTraced as launch
+import com.android.compose.animation.scene.OverlayKey
import com.android.systemui.battery.BatteryMeterViewController
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
@@ -86,6 +87,22 @@ constructor(
(ViewGroup, StatusBarLocation) -> BatteryMeterViewController =
batteryMeterViewControllerFactory::create
+ val showClock: Boolean by
+ hydrator.hydratedStateOf(
+ traceName = "showClock",
+ initialValue =
+ shouldShowClock(
+ isShadeLayoutWide = shadeInteractor.isShadeLayoutWide.value,
+ overlays = sceneInteractor.currentOverlays.value,
+ ),
+ source =
+ combine(
+ shadeInteractor.isShadeLayoutWide,
+ sceneInteractor.currentOverlays,
+ ::shouldShowClock,
+ ),
+ )
+
val notificationsChipHighlight: HeaderChipHighlight by
hydrator.hydratedStateOf(
traceName = "notificationsChipHighlight",
@@ -114,13 +131,6 @@ constructor(
},
)
- val isShadeLayoutWide: Boolean by
- hydrator.hydratedStateOf(
- traceName = "isShadeLayoutWide",
- initialValue = shadeInteractor.isShadeLayoutWide.value,
- source = shadeInteractor.isShadeLayoutWide,
- )
-
/** True if there is exactly one mobile connection. */
val isSingleCarrier: StateFlow<Boolean> = mobileIconsInteractor.isSingleCarrier
@@ -271,6 +281,11 @@ constructor(
}
}
+ private fun shouldShowClock(isShadeLayoutWide: Boolean, overlays: Set<OverlayKey>): Boolean {
+ // Notifications shade on narrow layout renders its own clock. Hide the header clock.
+ return isShadeLayoutWide || Overlays.NotificationsShade !in overlays
+ }
+
private fun getFormatFromPattern(pattern: String?): DateFormat {
val format = DateFormat.getInstanceForSkeleton(pattern, Locale.getDefault())
format.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java
index c2e355d07e9c..03c191e40ccf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java
@@ -22,6 +22,7 @@ import android.app.Flags;
import android.app.Notification;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
+import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.TypedValue;
@@ -31,6 +32,8 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
+import androidx.annotation.VisibleForTesting;
+
import com.android.internal.R;
import com.android.internal.widget.CachingIconView;
import com.android.internal.widget.ConversationLayout;
@@ -39,6 +42,7 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.NotificationContentView;
import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation;
import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
import java.util.ArrayList;
import java.util.HashSet;
@@ -214,7 +218,7 @@ public class NotificationGroupingUtil {
}
// in case no view is visible we make sure the time is visible
int timeVisibility = !hasVisibleText
- || row.getEntry().getSbn().getNotification().showsTime()
+ || showsTime(row)
? View.VISIBLE : View.GONE;
time.setVisibility(timeVisibility);
View left = null;
@@ -243,6 +247,17 @@ public class NotificationGroupingUtil {
}
}
+ @VisibleForTesting
+ boolean showsTime(ExpandableNotificationRow row) {
+ StatusBarNotification sbn;
+ if (NotificationBundleUi.isEnabled()) {
+ sbn = row.getEntryAdapter() != null ? row.getEntryAdapter().getSbn() : null;
+ } else {
+ sbn = row.getEntry().getSbn();
+ }
+ return (sbn != null && sbn.getNotification().showsTime());
+ }
+
/**
* Reset the modifications to this row for removing it from the group.
*/
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
index 0d34bdc7e477..041ed6504634 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
@@ -59,11 +59,13 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractor;
import com.android.systemui.statusbar.dagger.CentralSurfacesDependenciesModule;
import com.android.systemui.statusbar.notification.NotifPipelineFlags;
import com.android.systemui.statusbar.notification.RemoteInputControllerLogger;
+import com.android.systemui.statusbar.notification.collection.EntryAdapter;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntry.EditedSuggestionInfo;
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
import com.android.systemui.statusbar.notification.logging.NotificationLogger;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
import com.android.systemui.statusbar.phone.ExpandHeadsUpOnInlineReply;
import com.android.systemui.statusbar.policy.RemoteInputUriController;
import com.android.systemui.statusbar.policy.RemoteInputView;
@@ -134,20 +136,21 @@ public class NotificationRemoteInputManager implements CoreStartable {
view.getTag(com.android.internal.R.id.notification_action_index_tag);
final ExpandableNotificationRow row = getNotificationRowForParent(view.getParent());
- final NotificationEntry entry = getNotificationForParent(view.getParent());
- mLogger.logInitialClick(
- row != null ? row.getLoggingKey() : null, actionIndex, pendingIntent);
+ if (row == null) {
+ return false;
+ }
+ mLogger.logInitialClick(row.getLoggingKey(), actionIndex, pendingIntent);
if (handleRemoteInput(view, pendingIntent)) {
- mLogger.logRemoteInputWasHandled(
- row != null ? row.getLoggingKey() : null, actionIndex);
+ mLogger.logRemoteInputWasHandled(row.getLoggingKey(), actionIndex);
return true;
}
if (DEBUG) {
Log.v(TAG, "Notification click handler invoked for intent: " + pendingIntent);
}
- logActionClick(view, entry, pendingIntent);
+ Notification.Action action = getActionFromView(view, row, pendingIntent);
+ logActionClick(view, row.getKey(), action);
// The intent we are sending is for the application, which
// won't have permission to immediately start an activity after
// the user switches to home. We know it is safe to do at this
@@ -156,33 +159,47 @@ public class NotificationRemoteInputManager implements CoreStartable {
ActivityManager.getService().resumeAppSwitches();
} catch (RemoteException e) {
}
- Notification.Action action = getActionFromView(view, entry, pendingIntent);
return mCallback.handleRemoteViewClick(view, pendingIntent,
action == null ? false : action.isAuthenticationRequired(), actionIndex, () -> {
Pair<Intent, ActivityOptions> options = response.getLaunchOptions(view);
mLogger.logStartingIntentWithDefaultHandler(
- row != null ? row.getLoggingKey() : null, pendingIntent, actionIndex);
+ row.getLoggingKey(), pendingIntent, actionIndex);
boolean started = RemoteViews.startPendingIntent(view, pendingIntent, options);
- if (started) releaseNotificationIfKeptForRemoteInputHistory(entry);
+ if (started) {
+ if (NotificationBundleUi.isEnabled()) {
+ releaseNotificationIfKeptForRemoteInputHistory(row.getEntryAdapter());
+ } else {
+ releaseNotificationIfKeptForRemoteInputHistory(row.getEntry());
+ }
+ }
return started;
});
}
private @Nullable Notification.Action getActionFromView(View view,
- NotificationEntry entry, PendingIntent actionIntent) {
+ ExpandableNotificationRow row, PendingIntent actionIntent) {
Integer actionIndex = (Integer)
view.getTag(com.android.internal.R.id.notification_action_index_tag);
if (actionIndex == null) {
return null;
}
- if (entry == null) {
+ StatusBarNotification statusBarNotification = null;
+ if (NotificationBundleUi.isEnabled()) {
+ if (row.getEntryAdapter() != null) {
+ statusBarNotification = row.getEntryAdapter().getSbn();
+ }
+ } else {
+ if (row.getEntry() != null) {
+ statusBarNotification = row.getEntry().getSbn();
+ }
+ }
+ if (statusBarNotification == null) {
Log.w(TAG, "Couldn't determine notification for click.");
return null;
}
// Notification may be updated before this function is executed, and thus play safe
// here and verify that the action object is still the one that where the click happens.
- StatusBarNotification statusBarNotification = entry.getSbn();
Notification.Action[] actions = statusBarNotification.getNotification().actions;
if (actions == null || actionIndex >= actions.length) {
Log.w(TAG, "statusBarNotification.getNotification().actions is null or invalid");
@@ -199,14 +216,12 @@ public class NotificationRemoteInputManager implements CoreStartable {
private void logActionClick(
View view,
- NotificationEntry entry,
- PendingIntent actionIntent) {
- Notification.Action action = getActionFromView(view, entry, actionIntent);
+ String key,
+ Notification.Action action) {
if (action == null) {
return;
}
ViewParent parent = view.getParent();
- String key = entry.getSbn().getKey();
int buttonIndex = -1;
// If this is a default template, determine the index of the button.
if (view.getId() == com.android.internal.R.id.action0 &&
@@ -214,20 +229,10 @@ public class NotificationRemoteInputManager implements CoreStartable {
ViewGroup actionGroup = (ViewGroup) parent;
buttonIndex = actionGroup.indexOfChild(view);
}
- final NotificationVisibility nv = mVisibilityProvider.obtain(entry, true);
+ final NotificationVisibility nv = mVisibilityProvider.obtain(key, true);
mClickNotifier.onNotificationActionClick(key, buttonIndex, action, nv, false);
}
- private NotificationEntry getNotificationForParent(ViewParent parent) {
- while (parent != null) {
- if (parent instanceof ExpandableNotificationRow) {
- return ((ExpandableNotificationRow) parent).getEntry();
- }
- parent = parent.getParent();
- }
- return null;
- }
-
private @Nullable ExpandableNotificationRow getNotificationRowForParent(ViewParent parent) {
while (parent != null) {
if (parent instanceof ExpandableNotificationRow) {
@@ -394,11 +399,21 @@ public class NotificationRemoteInputManager implements CoreStartable {
}
}
+ /**
+ * Use {@link com.android.systemui.statusbar.notification.row.NotificationActionClickManager}
+ * instead
+ */
public void addActionPressListener(Consumer<NotificationEntry> listener) {
+ NotificationBundleUi.assertInLegacyMode();
mActionPressListeners.addIfAbsent(listener);
}
+ /**
+ * Use {@link com.android.systemui.statusbar.notification.row.NotificationActionClickManager}
+ * instead
+ */
public void removeActionPressListener(Consumer<NotificationEntry> listener) {
+ NotificationBundleUi.assertInLegacyMode();
mActionPressListeners.remove(listener);
}
@@ -681,12 +696,30 @@ public class NotificationRemoteInputManager implements CoreStartable {
* (after unlock, if applicable), and will then wait a short time to allow the app to update the
* notification in response to the action.
*/
+ private void releaseNotificationIfKeptForRemoteInputHistory(EntryAdapter entryAdapter) {
+ if (entryAdapter == null) {
+ return;
+ }
+ if (mRemoteInputListener != null) {
+ mRemoteInputListener.releaseNotificationIfKeptForRemoteInputHistory(
+ entryAdapter.getKey());
+ }
+ entryAdapter.onNotificationActionClicked();
+ }
+
+ /**
+ * Checks if the notification is being kept due to the user sending an inline reply, and if
+ * so, releases that hold. This is called anytime an action on the notification is dispatched
+ * (after unlock, if applicable), and will then wait a short time to allow the app to update the
+ * notification in response to the action.
+ */
private void releaseNotificationIfKeptForRemoteInputHistory(NotificationEntry entry) {
+ NotificationBundleUi.assertInLegacyMode();
if (entry == null) {
return;
}
if (mRemoteInputListener != null) {
- mRemoteInputListener.releaseNotificationIfKeptForRemoteInputHistory(entry);
+ mRemoteInputListener.releaseNotificationIfKeptForRemoteInputHistory(entry.getKey());
}
for (Consumer<NotificationEntry> listener : mActionPressListeners) {
listener.accept(entry);
@@ -866,7 +899,7 @@ public class NotificationRemoteInputManager implements CoreStartable {
boolean isNotificationKeptForRemoteInputHistory(@NonNull String key);
/** Called on user interaction to end lifetime extension for history */
- void releaseNotificationIfKeptForRemoteInputHistory(@NonNull NotificationEntry entry);
+ void releaseNotificationIfKeptForRemoteInputHistory(@NonNull String entryKey);
/** Called when the RemoteInputController is attached to the manager */
void setRemoteInputController(@NonNull RemoteInputController remoteInputController);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsReturnAnimations.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsReturnAnimations.kt
new file mode 100644
index 000000000000..176419454c21
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsReturnAnimations.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.chips
+
+import com.android.systemui.Flags
+import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
+
+/** Helper for reading or using the status_bar_chips_return_animations flag state. */
+object StatusBarChipsReturnAnimations {
+ /** The aconfig flag name */
+ const val FLAG_NAME = Flags.FLAG_STATUS_BAR_CHIPS_RETURN_ANIMATIONS
+
+ /** Is the feature enabled. */
+ @JvmStatic
+ inline val isEnabled
+ get() = StatusBarChipsModernization.isEnabled && Flags.statusBarChipsReturnAnimations()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt
index be3afad4e1d1..7e7031200988 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.chips.call.ui.viewmodel
+import android.app.PendingIntent
import android.content.Context
import android.view.View
import com.android.internal.jank.Cuj
@@ -31,6 +32,7 @@ import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.res.R
import com.android.systemui.statusbar.chips.StatusBarChipLogTags.pad
import com.android.systemui.statusbar.chips.StatusBarChipsLog
+import com.android.systemui.statusbar.chips.StatusBarChipsReturnAnimations
import com.android.systemui.statusbar.chips.call.domain.interactor.CallChipInteractor
import com.android.systemui.statusbar.chips.ui.model.ColorsModel
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
@@ -42,8 +44,10 @@ import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCall
import com.android.systemui.util.time.SystemClock
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
@@ -59,67 +63,110 @@ constructor(
private val activityStarter: ActivityStarter,
@StatusBarChipsLog private val logger: LogBuffer,
) : OngoingActivityChipViewModel {
- override val chip: StateFlow<OngoingActivityChipModel> =
- interactor.ongoingCallState
- .map { state ->
- when (state) {
- is OngoingCallModel.NoCall,
- is OngoingCallModel.InCallWithVisibleApp -> OngoingActivityChipModel.Inactive()
- is OngoingCallModel.InCall -> {
- val key = state.notificationKey
- val contentDescription = getContentDescription(state.appName)
- val icon =
- if (state.notificationIconView != null) {
- StatusBarConnectedDisplays.assertInLegacyMode()
- OngoingActivityChipModel.ChipIcon.StatusBarView(
- state.notificationIconView,
- contentDescription,
- )
- } else if (StatusBarConnectedDisplays.isEnabled) {
- OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon(
- state.notificationKey,
- contentDescription,
- )
+ private val chipWithReturnAnimation: StateFlow<OngoingActivityChipModel> =
+ if (StatusBarChipsReturnAnimations.isEnabled) {
+ interactor.ongoingCallState
+ .map { state ->
+ when (state) {
+ is OngoingCallModel.NoCall -> OngoingActivityChipModel.Inactive()
+ is OngoingCallModel.InCall ->
+ prepareChip(state, systemClock, isHidden = state.isAppVisible)
+ }
+ }
+ .stateIn(
+ scope,
+ SharingStarted.WhileSubscribed(),
+ OngoingActivityChipModel.Inactive(),
+ )
+ } else {
+ MutableStateFlow(OngoingActivityChipModel.Inactive()).asStateFlow()
+ }
+
+ private val chipLegacy: StateFlow<OngoingActivityChipModel> =
+ if (!StatusBarChipsReturnAnimations.isEnabled) {
+ interactor.ongoingCallState
+ .map { state ->
+ when (state) {
+ is OngoingCallModel.NoCall -> OngoingActivityChipModel.Inactive()
+ is OngoingCallModel.InCall ->
+ if (state.isAppVisible) {
+ OngoingActivityChipModel.Inactive()
} else {
- OngoingActivityChipModel.ChipIcon.SingleColorIcon(phoneIcon)
+ prepareChip(state, systemClock, isHidden = false)
}
-
- val colors = ColorsModel.AccentThemed
-
- // This block mimics OngoingCallController#updateChip.
- if (state.startTimeMs <= 0L) {
- // If the start time is invalid, don't show a timer and show just an
- // icon. See b/192379214.
- OngoingActivityChipModel.Active.IconOnly(
- key = key,
- icon = icon,
- colors = colors,
- onClickListenerLegacy = getOnClickListener(state),
- clickBehavior = getClickBehavior(state),
- )
- } else {
- val startTimeInElapsedRealtime =
- state.startTimeMs - systemClock.currentTimeMillis() +
- systemClock.elapsedRealtime()
- OngoingActivityChipModel.Active.Timer(
- key = key,
- icon = icon,
- colors = colors,
- startTimeMs = startTimeInElapsedRealtime,
- onClickListenerLegacy = getOnClickListener(state),
- clickBehavior = getClickBehavior(state),
- )
- }
}
}
+ .stateIn(
+ scope,
+ SharingStarted.WhileSubscribed(),
+ OngoingActivityChipModel.Inactive(),
+ )
+ } else {
+ MutableStateFlow(OngoingActivityChipModel.Inactive()).asStateFlow()
+ }
+
+ override val chip: StateFlow<OngoingActivityChipModel> =
+ if (StatusBarChipsReturnAnimations.isEnabled) {
+ chipWithReturnAnimation
+ } else {
+ chipLegacy
+ }
+
+ /** Builds an [OngoingActivityChipModel.Active] from all the relevant information. */
+ private fun prepareChip(
+ state: OngoingCallModel.InCall,
+ systemClock: SystemClock,
+ isHidden: Boolean,
+ ): OngoingActivityChipModel.Active {
+ val key = state.notificationKey
+ val contentDescription = getContentDescription(state.appName)
+ val icon =
+ if (state.notificationIconView != null) {
+ StatusBarConnectedDisplays.assertInLegacyMode()
+ OngoingActivityChipModel.ChipIcon.StatusBarView(
+ state.notificationIconView,
+ contentDescription,
+ )
+ } else if (StatusBarConnectedDisplays.isEnabled) {
+ OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon(
+ state.notificationKey,
+ contentDescription,
+ )
+ } else {
+ OngoingActivityChipModel.ChipIcon.SingleColorIcon(phoneIcon)
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Inactive())
- private fun getOnClickListener(state: OngoingCallModel.InCall): View.OnClickListener? {
- if (state.intent == null) {
- return null
+ val colors = ColorsModel.AccentThemed
+
+ // This block mimics OngoingCallController#updateChip.
+ if (state.startTimeMs <= 0L) {
+ // If the start time is invalid, don't show a timer and show just an icon.
+ // See b/192379214.
+ return OngoingActivityChipModel.Active.IconOnly(
+ key = key,
+ icon = icon,
+ colors = colors,
+ onClickListenerLegacy = getOnClickListener(state.intent),
+ clickBehavior = getClickBehavior(state.intent),
+ isHidden = isHidden,
+ )
+ } else {
+ val startTimeInElapsedRealtime =
+ state.startTimeMs - systemClock.currentTimeMillis() + systemClock.elapsedRealtime()
+ return OngoingActivityChipModel.Active.Timer(
+ key = key,
+ icon = icon,
+ colors = colors,
+ startTimeMs = startTimeInElapsedRealtime,
+ onClickListenerLegacy = getOnClickListener(state.intent),
+ clickBehavior = getClickBehavior(state.intent),
+ isHidden = isHidden,
+ )
}
+ }
+ private fun getOnClickListener(intent: PendingIntent?): View.OnClickListener? {
+ if (intent == null) return null
return View.OnClickListener { view ->
StatusBarChipsModernization.assertInLegacyMode()
logger.log(TAG, LogLevel.INFO, {}, { "Chip clicked" })
@@ -127,7 +174,7 @@ constructor(
view.requireViewById<ChipBackgroundContainer>(R.id.ongoing_activity_chip_background)
// This mimics OngoingCallController#updateChipClickListener.
activityStarter.postStartActivityDismissingKeyguard(
- state.intent,
+ intent,
ActivityTransitionAnimator.Controller.fromView(
backgroundView,
Cuj.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP,
@@ -136,10 +183,8 @@ constructor(
}
}
- private fun getClickBehavior(
- state: OngoingCallModel.InCall
- ): OngoingActivityChipModel.ClickBehavior =
- if (state.intent == null) {
+ private fun getClickBehavior(intent: PendingIntent?): OngoingActivityChipModel.ClickBehavior =
+ if (intent == null) {
OngoingActivityChipModel.ClickBehavior.None
} else {
OngoingActivityChipModel.ClickBehavior.ExpandAction(
@@ -149,10 +194,7 @@ constructor(
expandable.activityTransitionController(
Cuj.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP
)
- activityStarter.postStartActivityDismissingKeyguard(
- state.intent,
- animationController,
- )
+ activityStarter.postStartActivityDismissingKeyguard(intent, animationController)
}
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
index 02e4ba4d6437..6f8552738d33 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
@@ -19,6 +19,7 @@ package com.android.systemui.statusbar.chips.ui.binder
import android.annotation.IdRes
import android.content.Context
import android.content.res.ColorStateList
+import android.graphics.Typeface
import android.graphics.drawable.GradientDrawable
import android.view.View
import android.view.ViewGroup
@@ -27,6 +28,7 @@ import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.UiThread
+import com.android.systemui.FontStyles
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.ui.binder.ContentDescriptionViewBinder
import com.android.systemui.common.ui.binder.IconViewBinder
@@ -170,6 +172,16 @@ object OngoingActivityChipBinder {
forceLayout()
}
+ /** Updates the typefaces for any text shown in the chip. */
+ fun updateTypefaces(binding: OngoingActivityChipViewBinding) {
+ binding.timeView.typeface =
+ Typeface.create(FontStyles.GSF_LABEL_LARGE_EMPHASIZED, Typeface.NORMAL)
+ binding.textView.typeface =
+ Typeface.create(FontStyles.GSF_LABEL_LARGE_EMPHASIZED, Typeface.NORMAL)
+ binding.shortTimeDeltaView.typeface =
+ Typeface.create(FontStyles.GSF_LABEL_LARGE_EMPHASIZED, Typeface.NORMAL)
+ }
+
private fun setChipIcon(
chipModel: OngoingActivityChipModel.Active,
backgroundView: ChipBackgroundContainer,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt
index ac55bf2bee1c..55d753662a65 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.chips.ui.compose
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -44,6 +45,7 @@ import com.android.systemui.statusbar.chips.ui.viewmodel.rememberChronometerStat
import com.android.systemui.statusbar.chips.ui.viewmodel.rememberTimeRemainingState
import kotlin.math.min
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ChipContent(viewModel: OngoingActivityChipModel.Active, modifier: Modifier = Modifier) {
val context = LocalContext.current
@@ -52,7 +54,7 @@ fun ChipContent(viewModel: OngoingActivityChipModel.Active, modifier: Modifier =
val hasEmbeddedIcon =
viewModel.icon is OngoingActivityChipModel.ChipIcon.StatusBarView ||
viewModel.icon is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon
- val textStyle = MaterialTheme.typography.labelLarge
+ val textStyle = MaterialTheme.typography.labelLargeEmphasized
val textColor = Color(viewModel.colors.text(context))
val maxTextWidth = dimensionResource(id = R.dimen.ongoing_activity_chip_max_text_width)
val startPadding =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt
index d67cb8f81a6c..4edb23dc9f0e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt
@@ -18,24 +18,18 @@ package com.android.systemui.statusbar.chips.ui.compose
import android.content.res.ColorStateList
import android.view.ViewGroup
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.clickable
+import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
@@ -43,7 +37,6 @@ import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.viewinterop.AndroidView
import com.android.compose.animation.Expandable
-import com.android.compose.modifiers.thenIf
import com.android.systemui.animation.Expandable
import com.android.systemui.common.ui.compose.Icon
import com.android.systemui.common.ui.compose.load
@@ -60,32 +53,45 @@ fun OngoingActivityChip(
iconViewStore: NotificationIconContainerViewBinder.IconViewStore?,
modifier: Modifier = Modifier,
) {
- when (val clickBehavior = model.clickBehavior) {
- is OngoingActivityChipModel.ClickBehavior.ExpandAction -> {
- // Wrap the chip in an Expandable so we can animate the expand transition.
- ExpandableChip(
- color = { Color.Transparent },
- shape =
- RoundedCornerShape(
- dimensionResource(id = R.dimen.ongoing_activity_chip_corner_radius)
- ),
- modifier = modifier,
- ) { expandable ->
- ChipBody(model, iconViewStore, onClick = { clickBehavior.onClick(expandable) })
- }
+ val contentDescription =
+ when (val icon = model.icon) {
+ is OngoingActivityChipModel.ChipIcon.StatusBarView -> icon.contentDescription.load()
+ is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon ->
+ icon.contentDescription.load()
+ is OngoingActivityChipModel.ChipIcon.SingleColorIcon,
+ null -> null
}
- is OngoingActivityChipModel.ClickBehavior.ShowHeadsUpNotification -> {
- ChipBody(
- model,
- iconViewStore,
- onClick = { clickBehavior.onClick() },
- modifier = modifier,
- )
+
+ val borderStroke =
+ model.colors.outline(LocalContext.current)?.let {
+ BorderStroke(dimensionResource(R.dimen.ongoing_activity_chip_outline_width), Color(it))
}
- is OngoingActivityChipModel.ClickBehavior.None -> {
- ChipBody(model, iconViewStore, modifier = modifier)
+ val onClick =
+ when (val clickBehavior = model.clickBehavior) {
+ is OngoingActivityChipModel.ClickBehavior.ExpandAction -> { expandable: Expandable ->
+ clickBehavior.onClick(expandable)
+ }
+ is OngoingActivityChipModel.ClickBehavior.ShowHeadsUpNotification -> { _ ->
+ clickBehavior.onClick()
+ }
+ is OngoingActivityChipModel.ClickBehavior.None -> null
}
+
+ Expandable(
+ color = Color(model.colors.background(LocalContext.current).defaultColor),
+ shape =
+ RoundedCornerShape(dimensionResource(id = R.dimen.ongoing_activity_chip_corner_radius)),
+ modifier =
+ modifier.height(dimensionResource(R.dimen.ongoing_appops_chip_height)).semantics {
+ if (contentDescription != null) {
+ this.contentDescription = contentDescription
+ }
+ },
+ borderStroke = borderStroke,
+ onClick = onClick,
+ ) {
+ ChipBody(model, iconViewStore, isClickable = onClick != null)
}
}
@@ -93,22 +99,13 @@ fun OngoingActivityChip(
private fun ChipBody(
model: OngoingActivityChipModel.Active,
iconViewStore: NotificationIconContainerViewBinder.IconViewStore?,
+ isClickable: Boolean,
modifier: Modifier = Modifier,
- onClick: (() -> Unit)? = null,
) {
- val context = LocalContext.current
- val isClickable = onClick != null
val hasEmbeddedIcon =
model.icon is OngoingActivityChipModel.ChipIcon.StatusBarView ||
model.icon is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon
- val contentDescription =
- when (val icon = model.icon) {
- is OngoingActivityChipModel.ChipIcon.StatusBarView -> icon.contentDescription.load()
- is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon ->
- icon.contentDescription.load()
- is OngoingActivityChipModel.ChipIcon.SingleColorIcon -> null
- null -> null
- }
+
val chipSidePadding = dimensionResource(id = R.dimen.ongoing_activity_chip_side_padding)
val minWidth =
if (isClickable) {
@@ -119,68 +116,38 @@ private fun ChipBody(
dimensionResource(id = R.dimen.ongoing_activity_chip_min_text_width) + chipSidePadding
}
- val outline = model.colors.outline(context)
- val outlineWidth = dimensionResource(R.dimen.ongoing_activity_chip_outline_width)
-
- val shape =
- RoundedCornerShape(dimensionResource(id = R.dimen.ongoing_activity_chip_corner_radius))
-
- // Use a Box with `fillMaxHeight` to create a larger click surface for the chip. The visible
- // height of the chip is determined by the height of the background of the Row below.
- Box(
- contentAlignment = Alignment.Center,
+ Row(
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically,
modifier =
modifier
.fillMaxHeight()
- .clickable(enabled = isClickable, onClick = onClick ?: {})
- .semantics {
- if (contentDescription != null) {
- this.contentDescription = contentDescription
- }
- },
- ) {
- Row(
- horizontalArrangement = Arrangement.Center,
- verticalAlignment = Alignment.CenterVertically,
- modifier =
- Modifier.height(dimensionResource(R.dimen.ongoing_appops_chip_height))
- .thenIf(isClickable) { Modifier.widthIn(min = minWidth) }
- .layout { measurable, constraints ->
- val placeable = measurable.measure(constraints)
- layout(placeable.width, placeable.height) {
- if (constraints.maxWidth >= minWidth.roundToPx()) {
- placeable.place(0, 0)
- }
+ .layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ if (constraints.maxWidth >= minWidth.roundToPx()) {
+ placeable.place(0, 0)
}
}
- .background(Color(model.colors.background(context).defaultColor), shape = shape)
- .thenIf(outline != null) {
- Modifier.border(
- width = outlineWidth,
- color = Color(outline!!),
- shape = shape,
- )
- }
- .padding(
- horizontal =
- if (hasEmbeddedIcon) {
- dimensionResource(
- R.dimen
- .ongoing_activity_chip_side_padding_for_embedded_padding_icon
- )
- } else {
- dimensionResource(id = R.dimen.ongoing_activity_chip_side_padding)
- }
- ),
- ) {
- model.icon?.let {
- ChipIcon(viewModel = it, iconViewStore = iconViewStore, colors = model.colors)
- }
+ }
+ .padding(
+ horizontal =
+ if (hasEmbeddedIcon) {
+ dimensionResource(
+ R.dimen.ongoing_activity_chip_side_padding_for_embedded_padding_icon
+ )
+ } else {
+ dimensionResource(id = R.dimen.ongoing_activity_chip_side_padding)
+ }
+ ),
+ ) {
+ model.icon?.let {
+ ChipIcon(viewModel = it, iconViewStore = iconViewStore, colors = model.colors)
+ }
- val isIconOnly = model is OngoingActivityChipModel.Active.IconOnly
- if (!isIconOnly) {
- ChipContent(viewModel = model)
- }
+ val isIconOnly = model is OngoingActivityChipModel.Active.IconOnly
+ if (!isIconOnly) {
+ ChipContent(viewModel = model)
}
}
}
@@ -244,13 +211,3 @@ private fun StatusBarIcon(
update = { iconView -> iconView.imageTintList = colorTintList },
)
}
-
-@Composable
-private fun ExpandableChip(
- color: () -> Color,
- shape: Shape,
- modifier: Modifier = Modifier,
- content: @Composable (Expandable) -> Unit,
-) {
- Expandable(color = color(), shape = shape, modifier = modifier.clip(shape)) { content(it) }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt
index 4954cb0a1b24..b2683762cb2a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt
@@ -19,7 +19,6 @@ package com.android.systemui.statusbar.chips.ui.model
import android.content.Context
import android.content.res.ColorStateList
import androidx.annotation.ColorInt
-import com.android.settingslib.Utils
import com.android.systemui.res.R
/** Model representing how the chip in the status bar should be colored. */
@@ -34,14 +33,14 @@ sealed interface ColorsModel {
@ColorInt fun outline(context: Context): Int?
/** The chip should match the theme's primary accent color. */
- // TODO(b/347717946): The chip's color isn't getting updated when the user switches theme, it
- // only gets updated when a different configuration change happens, like a rotation.
data object AccentThemed : ColorsModel {
override fun background(context: Context): ColorStateList =
- Utils.getColorAttr(context, com.android.internal.R.attr.colorAccent)
+ ColorStateList.valueOf(
+ context.getColor(com.android.internal.R.color.materialColorPrimaryFixedDim)
+ )
override fun text(context: Context) =
- Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.colorPrimary)
+ context.getColor(com.android.internal.R.color.materialColorOnPrimaryFixed)
override fun outline(context: Context) = null
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
index 0cfc3216beb5..8e470742f174 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar.chips.ui.model
+import android.annotation.CurrentTimeMillisLong
+import android.annotation.ElapsedRealtimeLong
import android.view.View
import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.ContentDescription
@@ -102,7 +104,7 @@ sealed class OngoingActivityChipModel {
* [ChipChronometer] is based off of elapsed realtime. See
* [android.widget.Chronometer.setBase].
*/
- val startTimeMs: Long,
+ @ElapsedRealtimeLong val startTimeMs: Long,
override val onClickListenerLegacy: View.OnClickListener?,
override val clickBehavior: ClickBehavior,
override val isHidden: Boolean = false,
@@ -129,10 +131,15 @@ sealed class OngoingActivityChipModel {
override val icon: ChipIcon,
override val colors: ColorsModel,
/**
- * The time of the event that this chip represents, relative to
- * [com.android.systemui.util.time.SystemClock.currentTimeMillis].
+ * The time of the event that this chip represents. Relative to
+ * [com.android.systemui.util.time.SystemClock.currentTimeMillis] because that's what's
+ * required by [android.widget.DateTimeView].
+ *
+ * TODO(b/372657935): When the Compose chips are launched, we should convert this to be
+ * relative to [com.android.systemui.util.time.SystemClock.elapsedRealtime] so that
+ * this model and the [Timer] model use the same units.
*/
- val time: Long,
+ @CurrentTimeMillisLong val time: Long,
override val onClickListenerLegacy: View.OnClickListener?,
override val clickBehavior: ClickBehavior,
override val isHidden: Boolean = false,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
index eae2c25d77d8..8228b5533fca 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
@@ -555,7 +555,6 @@ constructor(
secondary = DEFAULT_INTERNAL_INACTIVE_MODEL,
)
- // TODO(b/392886257): Support 3 chips if there's space available.
- private const val MAX_VISIBLE_CHIPS = 2
+ private const val MAX_VISIBLE_CHIPS = 3
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt
index eb6ebcaa5796..803d422c0f0f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt
@@ -16,7 +16,6 @@
package com.android.systemui.statusbar.chips.ui.viewmodel
-import android.os.SystemClock
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
@@ -39,7 +38,14 @@ import kotlinx.coroutines.delay
* Manages state and updates for the duration remaining between now and a given time in the future.
*/
class TimeRemainingState(private val timeSource: TimeSource, private val futureTimeMillis: Long) {
- private var durationRemaining by mutableStateOf(Duration.ZERO)
+ // Start with the right duration from the outset so we don't use "now" as an initial value.
+ private var durationRemaining by
+ mutableStateOf(
+ calculateDurationRemaining(
+ currentTimeMillis = timeSource.getCurrentTime(),
+ futureTimeMillis = futureTimeMillis,
+ )
+ )
private var startTimeMillis: Long = 0
/**
@@ -56,7 +62,11 @@ class TimeRemainingState(private val timeSource: TimeSource, private val futureT
while (true) {
val currentTime = timeSource.getCurrentTime()
durationRemaining =
- (futureTimeMillis - currentTime).toDuration(DurationUnit.MILLISECONDS)
+ calculateDurationRemaining(
+ currentTimeMillis = currentTime,
+ futureTimeMillis = futureTimeMillis,
+ )
+
// No need to update if duration is more than 1 minute in the past. Because, we will
// stop displaying anything.
if (durationRemaining.inWholeMilliseconds < -1.minutes.inWholeMilliseconds) {
@@ -67,6 +77,13 @@ class TimeRemainingState(private val timeSource: TimeSource, private val futureT
}
}
+ private fun calculateDurationRemaining(
+ currentTimeMillis: Long,
+ futureTimeMillis: Long,
+ ): Duration {
+ return (futureTimeMillis - currentTimeMillis).toDuration(DurationUnit.MILLISECONDS)
+ }
+
private fun calculateNextUpdateDelay(duration: Duration): Long {
val durationAbsolute = duration.absoluteValue
return when {
@@ -85,7 +102,7 @@ class TimeRemainingState(private val timeSource: TimeSource, private val futureT
@Composable
fun rememberTimeRemainingState(
futureTimeMillis: Long,
- timeSource: TimeSource = remember { TimeSource { SystemClock.elapsedRealtime() } },
+ timeSource: TimeSource = remember { TimeSource { System.currentTimeMillis() } },
): TimeRemainingState {
val state =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
index 434eb7d3d410..a7929ecbd801 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
@@ -33,6 +33,7 @@ import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpHandler;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.media.NotificationMediaManager;
import com.android.systemui.media.controls.domain.pipeline.MediaDataManager;
import com.android.systemui.power.domain.interactor.PowerInteractor;
import com.android.systemui.scene.shared.flag.SceneContainerFlag;
@@ -44,7 +45,6 @@ import com.android.systemui.shade.ShadeSurfaceImpl;
import com.android.systemui.shade.carrier.ShadeCarrierGroupController;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.NotificationClickNotifier;
-import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.SmartReplyController;
import com.android.systemui.statusbar.StatusBarStateControllerImpl;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt
index 7fa9f0e9dcc2..a658e1d55f7a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt
@@ -267,7 +267,8 @@ constructor(
if (StatusBarChipsModernization.isEnabled) {
ongoingProcessRequiresStatusBarVisible
} else {
- ongoingCallStateLegacy is OngoingCallModel.InCall
+ ongoingCallStateLegacy is OngoingCallModel.InCall &&
+ !ongoingCallStateLegacy.isAppVisible
}
val statusBarMode =
toBarMode(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java
index 41353b9921bd..4d68f2e6ef1b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java
@@ -63,17 +63,6 @@ public class BundleEntry extends PipelineEntry {
@Nullable
@Override
- public NotifSection getSection() {
- return null;
- }
-
- @Override
- public int getSectionIndex() {
- return 0;
- }
-
- @Nullable
- @Override
public PipelineEntry getParent() {
return null;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt
index 64db9df8270c..26c302bf6409 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt
@@ -20,6 +20,7 @@ import android.app.Notification
import android.content.Context
import android.os.Build
import android.service.notification.StatusBarNotification
+import android.util.Log
import com.android.systemui.statusbar.notification.icon.IconPack
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import kotlinx.coroutines.flow.StateFlow
@@ -118,5 +119,13 @@ class BundleEntryAdapter(val entry: BundleEntry) : EntryAdapter {
override fun onNotificationBubbleIconClicked() {
// do nothing. these cannot be a bubble
+ Log.wtf(TAG, "onNotificationBubbleIconClicked() called")
+ }
+
+ override fun onNotificationActionClicked() {
+ // do nothing. these have no actions
+ Log.wtf(TAG, "onNotificationActionClicked() called")
}
}
+
+private const val TAG = "BundleEntryAdapter"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java
index 0e75b6050678..3118ce56ac69 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java
@@ -140,5 +140,10 @@ public interface EntryAdapter {
* Process a click on a notification bubble icon
*/
void onNotificationBubbleIconClicked();
+
+ /**
+ * Processes a click on a notification action
+ */
+ void onNotificationActionClicked();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapterFactoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapterFactoryImpl.kt
index 779c25a3b402..a5169865c3c9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapterFactoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapterFactoryImpl.kt
@@ -16,11 +16,11 @@
package com.android.systemui.statusbar.notification.collection
-import androidx.annotation.VisibleForTesting
import com.android.internal.logging.MetricsLogger
import com.android.systemui.statusbar.notification.NotificationActivityStarter
import com.android.systemui.statusbar.notification.collection.coordinator.VisualStabilityCoordinator
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
+import com.android.systemui.statusbar.notification.row.NotificationActionClickManager
import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider
import javax.inject.Inject
@@ -33,6 +33,7 @@ constructor(
private val peopleNotificationIdentifier: PeopleNotificationIdentifier,
private val iconStyleProvider: NotificationIconStyleProvider,
private val visualStabilityCoordinator: VisualStabilityCoordinator,
+ private val notificationActionClickManager: NotificationActionClickManager,
) : EntryAdapterFactory {
override fun create(entry: PipelineEntry): EntryAdapter {
return if (entry is NotificationEntry) {
@@ -42,15 +43,11 @@ constructor(
peopleNotificationIdentifier,
iconStyleProvider,
visualStabilityCoordinator,
+ notificationActionClickManager,
entry,
)
} else {
BundleEntryAdapter((entry as BundleEntry))
}
}
-
- @VisibleForTesting
- fun getNotificationActivityStarter() : NotificationActivityStarter {
- return notificationActivityStarter
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt
index 4a1b9568c714..04dc7d5ed3ff 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt
@@ -31,8 +31,8 @@ data class ListAttachState private constructor(
var parent: PipelineEntry?,
/**
- * The section that this ListEntry was sorted into. If the child of the group, this will be the
- * parent's section. Null if not attached to the list.
+ * The section that this PipelineEntry was sorted into. If the child of the group, this will be
+ * the parent's section. Null if not attached to the list.
*/
var section: NotifSection?,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java
index 697d0a06cf9d..caa7abb1aa7a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java
@@ -66,10 +66,6 @@ public abstract class ListEntry extends PipelineEntry {
return mPreviousAttachState.getParent();
}
- public int getSectionIndex() {
- return mAttachState.getSection() != null ? mAttachState.getSection().getIndex() : -1;
- }
-
/**
* Stores the current attach state into {@link #getPreviousAttachState()}} and then starts a
* fresh attach state (all entries will be null/default-initialized).
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.kt
index f662a040fae6..0fc0e9c5eab8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.kt
@@ -24,6 +24,7 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.OnBefo
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeSortListener
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifBundler
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter
@@ -169,6 +170,14 @@ class NotifPipeline @Inject constructor(
}
/**
+ * NotifBundler that is used to determine whether a notification should be bundled according to
+ * classification.
+ */
+ fun setNotifBundler(bundler: NotifBundler) {
+ mShadeListBuilder.setBundler(bundler)
+ }
+
+ /**
* StabilityManager that is used to determine whether to suppress group and section changes.
* This should only be set once.
*/
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt
index 0ff2dd7c7f43..1168c647c26a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt
@@ -24,6 +24,7 @@ import com.android.systemui.statusbar.notification.collection.coordinator.Visual
import com.android.systemui.statusbar.notification.icon.IconPack
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.notification.row.NotificationActionClickManager
import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider
import kotlinx.coroutines.flow.StateFlow
@@ -33,6 +34,7 @@ class NotificationEntryAdapter(
private val peopleNotificationIdentifier: PeopleNotificationIdentifier,
private val iconStyleProvider: NotificationIconStyleProvider,
private val visualStabilityCoordinator: VisualStabilityCoordinator,
+ private val notificationActionClickManager: NotificationActionClickManager,
private val entry: NotificationEntry,
) : EntryAdapter {
@@ -142,4 +144,8 @@ class NotificationEntryAdapter(
override fun onNotificationBubbleIconClicked() {
notificationActivityStarter.onNotificationBubbleIconClicked(entry)
}
+
+ override fun onNotificationActionClicked() {
+ notificationActionClickManager.onNotificationActionClicked(entry)
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java
index 84de77bac352..872cd68e1b21 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java
@@ -70,7 +70,9 @@ public abstract class PipelineEntry {
/**
* @return Index of section assigned to this entry.
*/
- public abstract int getSectionIndex();
+ public int getSectionIndex() {
+ return mAttachState.getSection() != null ? mAttachState.getSection().getIndex() : -1;
+ }
/**
* @return Parent PipelineEntry
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
index bb84ab8f421a..238ba8d9f490 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
@@ -48,6 +48,7 @@ import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.statusbar.NotificationInteractionTracker;
import com.android.systemui.statusbar.notification.NotifPipelineFlags;
+import com.android.systemui.statusbar.notification.collection.coordinator.BundleCoordinator;
import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection;
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeFinalizeFilterListener;
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener;
@@ -58,8 +59,10 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.SemiSt
import com.android.systemui.statusbar.notification.collection.listbuilder.SemiStableSort.StableOrder;
import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderHelper;
import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderLogger;
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.DefaultNotifBundler;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.DefaultNotifStabilityManager;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator;
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifBundler;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter;
@@ -78,6 +81,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
+import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -122,7 +126,8 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable {
private final List<NotifComparator> mNotifComparators = new ArrayList<>();
private final List<NotifSection> mNotifSections = new ArrayList<>();
private NotifStabilityManager mNotifStabilityManager;
-
+ private NotifBundler mNotifBundler;
+ private Map<String, BundleEntry> mIdToBundleEntry = new HashMap<>();
private final NamedListenerSet<OnBeforeTransformGroupsListener>
mOnBeforeTransformGroupsListeners = new NamedListenerSet<>();
private final NamedListenerSet<OnBeforeSortListener>
@@ -273,6 +278,21 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable {
}
}
+ void setBundler(NotifBundler bundler) {
+ Assert.isMainThread();
+ mPipelineState.requireState(STATE_IDLE);
+
+ mNotifBundler = bundler;
+ if (mNotifBundler == null) {
+ throw new IllegalStateException(TAG + ".setBundler: null");
+ }
+
+ mIdToBundleEntry.clear();
+ for (String id: mNotifBundler.getBundleIds()) {
+ mIdToBundleEntry.put(id, new BundleEntry(id));
+ }
+ }
+
void setNotifStabilityManager(@NonNull NotifStabilityManager notifStabilityManager) {
Assert.isMainThread();
mPipelineState.requireState(STATE_IDLE);
@@ -297,6 +317,14 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable {
return mNotifStabilityManager;
}
+ @NonNull
+ private NotifBundler getNotifBundler() {
+ if (mNotifBundler == null) {
+ return DefaultNotifBundler.INSTANCE;
+ }
+ return mNotifBundler;
+ }
+
void setComparators(List<NotifComparator> comparators) {
Assert.isMainThread();
mPipelineState.requireState(STATE_IDLE);
@@ -651,7 +679,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable {
j--;
}
}
- } else {
+ } else if (tle instanceof NotificationEntry) {
// maybe put top-level-entries back into their previous groups
if (maybeSuppressGroupChange(tle.getRepresentativeEntry(), topLevelList)) {
// entry was put back into its previous group, so we remove it from the list of
@@ -659,7 +687,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable {
topLevelList.remove(i);
i--;
}
- }
+ } // Promoters ignore bundles so we don't have to demote any here.
}
Trace.endSection();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinator.kt
index e6d5f4120a20..8833ff1ce20c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinator.kt
@@ -20,15 +20,19 @@ import android.app.NotificationChannel.NEWS_ID
import android.app.NotificationChannel.PROMOTIONS_ID
import android.app.NotificationChannel.RECS_ID
import android.app.NotificationChannel.SOCIAL_MEDIA_ID
-import com.android.systemui.statusbar.notification.collection.PipelineEntry
+import android.app.NotificationChannel.SYSTEM_RESERVED_IDS
import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.PipelineEntry
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifBundler
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
import com.android.systemui.statusbar.notification.collection.render.NodeController
import com.android.systemui.statusbar.notification.dagger.NewsHeader
import com.android.systemui.statusbar.notification.dagger.PromoHeader
import com.android.systemui.statusbar.notification.dagger.RecsHeader
import com.android.systemui.statusbar.notification.dagger.SocialHeader
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
import com.android.systemui.statusbar.notification.stack.BUCKET_NEWS
import com.android.systemui.statusbar.notification.stack.BUCKET_PROMO
import com.android.systemui.statusbar.notification.stack.BUCKET_RECS
@@ -90,6 +94,20 @@ class BundleCoordinator @Inject constructor(
}
}
+ val bundler =
+ object : NotifBundler("NotifBundler") {
+
+ // Use list instead of set to keep fixed order
+ override val bundleIds: List<String> = SYSTEM_RESERVED_IDS
+
+ override fun getBundleIdOrNull(entry: NotificationEntry?): String? {
+ return entry?.representativeEntry?.channel?.id?.takeIf { it in this.bundleIds }
+ }
+ }
+
override fun attach(pipeline: NotifPipeline) {
+ if (NotificationBundleUi.isEnabled) {
+ pipeline.setNotifBundler(bundler)
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
index fdb8cd871dd9..a0eab43f854b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
@@ -26,6 +26,7 @@ import com.android.systemui.statusbar.NotificationRemoteInputManager
import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.notification.NotifPipelineFlags
+import com.android.systemui.statusbar.notification.collection.BundleEntry
import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.NotifCollection
import com.android.systemui.statusbar.notification.collection.NotifPipeline
@@ -47,7 +48,9 @@ import com.android.systemui.statusbar.notification.headsup.PinnedStatus
import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider
import com.android.systemui.statusbar.notification.logKey
+import com.android.systemui.statusbar.notification.row.NotificationActionClickManager
import com.android.systemui.statusbar.notification.shared.GroupHunAnimationFix
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
import com.android.systemui.statusbar.notification.stack.BUCKET_HEADS_UP
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.time.SystemClock
@@ -82,6 +85,7 @@ constructor(
private val mHeadsUpViewBinder: HeadsUpViewBinder,
private val mVisualInterruptionDecisionProvider: VisualInterruptionDecisionProvider,
private val mRemoteInputManager: NotificationRemoteInputManager,
+ private val notificationActionClickManager: NotificationActionClickManager,
private val mLaunchFullScreenIntentProvider: LaunchFullScreenIntentProvider,
private val mFlags: NotifPipelineFlags,
private val statusBarNotificationChipsInteractor: StatusBarNotificationChipsInteractor,
@@ -107,7 +111,11 @@ constructor(
pipeline.addOnBeforeFinalizeFilterListener(::onBeforeFinalizeFilter)
pipeline.addPromoter(mNotifPromoter)
pipeline.addNotificationLifetimeExtender(mLifetimeExtender)
- mRemoteInputManager.addActionPressListener(mActionPressListener)
+ if (NotificationBundleUi.isEnabled) {
+ notificationActionClickManager.addActionClickListener(mActionPressListener)
+ } else {
+ mRemoteInputManager.addActionPressListener(mActionPressListener)
+ }
if (StatusBarNotifChips.isEnabled) {
applicationScope.launch {
@@ -423,6 +431,7 @@ constructor(
map[child.key] = GroupLocation.Child
}
}
+ is BundleEntry -> map[topLevelEntry.key] = GroupLocation.Bundle
else -> error("unhandled type $topLevelEntry")
}
}
@@ -781,7 +790,7 @@ constructor(
*/
private val mActionPressListener =
Consumer<NotificationEntry> { entry ->
- mHeadsUpManager.setUserActionMayIndirectlyRemove(entry)
+ mHeadsUpManager.setUserActionMayIndirectlyRemove(entry.key)
mExecutor.execute { endNotifLifetimeExtensionIfExtended(entry) }
}
@@ -950,6 +959,7 @@ private enum class GroupLocation {
Isolated,
Summary,
Child,
+ Bundle,
}
private fun Map<String, GroupLocation>.getLocation(key: String): GroupLocation =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt
index 56deb18df9ab..d542e67e665a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt
@@ -26,6 +26,7 @@ import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.notification.collection.BundleEntry
import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.PipelineEntry
import com.android.systemui.statusbar.notification.collection.NotifPipeline
@@ -193,6 +194,7 @@ constructor(
when (it) {
is NotificationEntry -> listOfNotNull(it)
is GroupEntry -> it.children
+ is BundleEntry -> emptyList()
else -> error("unhandled type of $it")
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
index df0cde51e8ba..818ef6b335c7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
@@ -24,6 +24,7 @@ import com.android.systemui.statusbar.notification.collection.coordinator.dagger
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
import com.android.systemui.statusbar.notification.promoted.AutomaticPromotionCoordinator
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection
@@ -113,11 +114,12 @@ constructor(
mCoordinators.add(remoteInputCoordinator)
mCoordinators.add(dismissibilityCoordinator)
mCoordinators.add(automaticPromotionCoordinator)
-
+ if (NotificationBundleUi.isEnabled) {
+ mCoordinators.add(bundleCoordinator)
+ }
if (NotificationsLiveDataStoreRefactor.isEnabled) {
mCoordinators.add(statsLoggerCoordinator)
}
-
// Manually add Ordered Sections
if (NotificationMinimalism.isEnabled) {
mOrderedSections.add(lockScreenMinimalismCoordinator.topOngoingSectioner) // Top Ongoing
@@ -135,7 +137,7 @@ constructor(
mOrderedSections.add(conversationCoordinator.peopleSilentSectioner) // People Silent
}
mOrderedSections.add(rankingCoordinator.alertingSectioner) // Alerting
- if (NotificationClassificationFlag.isEnabled) {
+ if (NotificationClassificationFlag.isEnabled && !NotificationBundleUi.isEnabled) {
mOrderedSections.add(bundleCoordinator.newsSectioner)
mOrderedSections.add(bundleCoordinator.socialSectioner)
mOrderedSections.add(bundleCoordinator.recsSectioner)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
index 20c6736b74c8..b54f21b23bba 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
@@ -34,6 +34,7 @@ import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.statusbar.IStatusBarService;
+import com.android.systemui.statusbar.notification.collection.BundleEntry;
import com.android.systemui.statusbar.notification.collection.GroupEntry;
import com.android.systemui.statusbar.notification.collection.PipelineEntry;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
@@ -54,6 +55,7 @@ import com.android.systemui.statusbar.notification.row.icon.AppIconProvider;
import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider;
import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation;
import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -306,7 +308,9 @@ public class PreparationCoordinator implements Coordinator {
private void inflateAllRequiredViews(List<PipelineEntry> entries) {
for (int i = 0, size = entries.size(); i < size; i++) {
PipelineEntry entry = entries.get(i);
- if (entry instanceof GroupEntry) {
+ if (NotificationBundleUi.isEnabled() && entry instanceof BundleEntry) {
+ // TODO(b/399738511) Inflate bundle views.
+ } else if (entry instanceof GroupEntry) {
GroupEntry groupEntry = (GroupEntry) entry;
inflateRequiredGroupViews(groupEntry);
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java
index d1063d95a305..3fad8f0510a1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java
@@ -132,7 +132,12 @@ public class RankingCoordinator implements Coordinator {
public void onEntriesUpdated(@NonNull List<PipelineEntry> entries) {
mHasSilentEntries = false;
for (int i = 0; i < entries.size(); i++) {
- if (entries.get(i).getRepresentativeEntry().getSbn().isClearable()) {
+ NotificationEntry notifEntry = entries.get(i).getRepresentativeEntry();
+ if (notifEntry == null) {
+ // TODO(b/395698521) Handle BundleEntry
+ continue;
+ }
+ if (notifEntry.getSbn().isClearable()) {
mHasSilentEntries = true;
break;
}
@@ -147,6 +152,7 @@ public class RankingCoordinator implements Coordinator {
@Override
public boolean isInSection(PipelineEntry entry) {
return !mHighPriorityProvider.isHighPriority(entry)
+ && entry.getRepresentativeEntry() != null
&& entry.getRepresentativeEntry().isAmbient();
}
@@ -161,7 +167,12 @@ public class RankingCoordinator implements Coordinator {
public void onEntriesUpdated(@NonNull List<PipelineEntry> entries) {
mHasMinimizedEntries = false;
for (int i = 0; i < entries.size(); i++) {
- if (entries.get(i).getRepresentativeEntry().getSbn().isClearable()) {
+ NotificationEntry notifEntry = entries.get(i).getRepresentativeEntry();
+ if (notifEntry == null) {
+ // TODO(b/395698521) Handle BundleEntry
+ continue;
+ }
+ if (notifEntry.getSbn().isClearable()) {
mHasMinimizedEntries = true;
break;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
index e7c767f42c12..27c0dcccfe43 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
@@ -221,20 +221,20 @@ constructor(
mSmartReplyHistoryExtender.isExtending(key)
} else false
- override fun releaseNotificationIfKeptForRemoteInputHistory(entry: NotificationEntry) {
- if (DEBUG) Log.d(TAG, "releaseNotificationIfKeptForRemoteInputHistory(entry=${entry.key})")
+ override fun releaseNotificationIfKeptForRemoteInputHistory(entryKey: String) {
+ if (DEBUG) Log.d(TAG, "releaseNotificationIfKeptForRemoteInputHistory(entry=${entryKey})")
if (!lifetimeExtensionRefactor()) {
mRemoteInputHistoryExtender.endLifetimeExtensionAfterDelay(
- entry.key,
+ entryKey,
REMOTE_INPUT_EXTENDER_RELEASE_DELAY,
)
mSmartReplyHistoryExtender.endLifetimeExtensionAfterDelay(
- entry.key,
+ entryKey,
REMOTE_INPUT_EXTENDER_RELEASE_DELAY,
)
}
mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(
- entry.key,
+ entryKey,
REMOTE_INPUT_EXTENDER_RELEASE_DELAY,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
index c2f0806a9cd6..6b32c6a18ec0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
@@ -17,7 +17,6 @@
package com.android.systemui.statusbar.notification.collection.inflation;
import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE;
-import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT;
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED;
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED;
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC;
@@ -276,7 +275,7 @@ public class NotificationRowBinderImpl implements NotificationRowBinder {
if (LockscreenOtpRedaction.isSingleLineViewEnabled()) {
if (inflaterParams.isChildInGroup()
- && redactionType == REDACTION_TYPE_SENSITIVE_CONTENT) {
+ && redactionType != REDACTION_TYPE_NONE) {
params.requireContentViews(FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE);
} else {
params.markContentViewsFreeable(FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifBundler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifBundler.kt
new file mode 100644
index 000000000000..14a9113f7eb4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifBundler.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification.collection.listbuilder.pluggable
+
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+
+/** Pluggable for bundling notifications according to classification. */
+abstract class NotifBundler protected constructor(name: String?) : Pluggable<NotifBundler?>(name) {
+ abstract val bundleIds: List<String>
+
+ abstract fun getBundleIdOrNull(entry: NotificationEntry?): String?
+}
+
+/** The default, no-op instance of NotifBundler which does not bundle anything. */
+object DefaultNotifBundler : NotifBundler("DefaultNotifBundler") {
+ override val bundleIds: List<String>
+ get() = listOf()
+
+ override fun getBundleIdOrNull(entry: NotificationEntry?): String? {
+ return null
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
index ef3da9498f70..1e5aa01714be 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
@@ -83,6 +83,7 @@ import com.android.systemui.statusbar.notification.logging.dagger.NotificationsL
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor;
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractorImpl;
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel;
+import com.android.systemui.statusbar.notification.row.NotificationActionClickManager;
import com.android.systemui.statusbar.notification.row.NotificationEntryProcessorFactory;
import com.android.systemui.statusbar.notification.row.NotificationEntryProcessorFactoryLooperImpl;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
@@ -346,4 +347,5 @@ public interface NotificationsModule {
/** Provides an instance of {@link EntryAdapterFactory} */
@Binds
EntryAdapterFactory provideEntryAdapterFactory(EntryAdapterFactoryImpl impl);
+
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManager.kt
index 9728fcfcd6ba..25ae50c34659 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManager.kt
@@ -19,7 +19,6 @@ package com.android.systemui.statusbar.notification.headsup
import android.graphics.Region
import com.android.systemui.Dumpable
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.statusbar.notification.collection.EntryAdapter
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import dagger.Binds
@@ -155,9 +154,9 @@ interface HeadsUpManager : Dumpable {
fun setAnimationStateHandler(handler: AnimationStateHandler)
/**
- * Set an entry to be expanded and therefore stick in the heads up area if it's pinned until
- * it's collapsed again.
- */
+ * Set an entry to be expanded and therefore stick in the heads up area if it's pinned until
+ * it's collapsed again.
+ */
fun setExpanded(key: String, row: ExpandableNotificationRow, expanded: Boolean)
/**
@@ -199,12 +198,12 @@ interface HeadsUpManager : Dumpable {
* Notes that the user took an action on an entry that might indirectly cause the system or the
* app to remove the notification.
*
- * @param entry the entry that might be indirectly removed by the user's action
+ * @param entry the key of the entry that might be indirectly removed by the user's action
* @see
* com.android.systemui.statusbar.notification.collection.coordinator.HeadsUpCoordinator.mActionPressListener
* @see .canRemoveImmediately
*/
- fun setUserActionMayIndirectlyRemove(entry: NotificationEntry)
+ fun setUserActionMayIndirectlyRemove(entryKey: String)
/**
* Decides whether a click is invalid for a notification, i.e. it has not been shown long enough
@@ -332,7 +331,7 @@ class HeadsUpManagerEmptyImpl @Inject constructor() : HeadsUpManager {
override fun setUser(user: Int) {}
- override fun setUserActionMayIndirectlyRemove(entry: NotificationEntry) {}
+ override fun setUserActionMayIndirectlyRemove(entryKey: String) {}
override fun shouldSwallowClick(key: String): Boolean = false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java
index ca94655318b9..ca83666079ec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java
@@ -1126,8 +1126,8 @@ public class HeadsUpManagerImpl
* @see HeadsUpCoordinator.mActionPressListener
* @see #canRemoveImmediately(String)
*/
- public void setUserActionMayIndirectlyRemove(@NonNull NotificationEntry entry) {
- HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey());
+ public void setUserActionMayIndirectlyRemove(@NonNull String entryKey) {
+ HeadsUpEntry headsUpEntry = getHeadsUpEntry(entryKey);
if (headsUpEntry != null) {
headsUpEntry.mUserActionMayIndirectlyRemove = true;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
index c3266fc57c2e..92c87e6403ec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
@@ -18,10 +18,10 @@ package com.android.systemui.statusbar.notification.init
import android.service.notification.StatusBarNotification
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.media.NotificationMediaManager
import com.android.systemui.people.widget.PeopleSpaceWidgetManager
import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption
import com.android.systemui.statusbar.NotificationListener
-import com.android.systemui.statusbar.NotificationMediaManager
import com.android.systemui.statusbar.NotificationPresenter
import com.android.systemui.statusbar.notification.AnimatedImageNotificationManager
import com.android.systemui.statusbar.notification.NotificationActivityStarter
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt
index 1abd48c0c50b..a99ca072b6c8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt
@@ -70,9 +70,6 @@ constructor(
private fun OngoingCallModel.getNotifData(): NotifAndPromotedContent? =
when (this) {
is OngoingCallModel.InCall -> NotifAndPromotedContent(notificationKey, promotedContent)
- is OngoingCallModel.InCallWithVisibleApp ->
- // TODO(b/395989259): support InCallWithVisibleApp when it has notif data
- null
is OngoingCallModel.NoCall -> null
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
index 6837cb2a6292..689222608abe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
@@ -128,9 +128,14 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView
updateColors();
}
- private void updateColors() {
- if (usesTransparentBackground()) {
- mNormalColor = SurfaceEffectColors.surfaceEffect1(getContext());
+ protected void updateColors() {
+ if (notificationRowTransparency()) {
+ if (mIsBlurSupported) {
+ mNormalColor = SurfaceEffectColors.surfaceEffect1(getContext());
+ } else {
+ mNormalColor = mContext.getColor(
+ com.android.internal.R.color.materialColorSurfaceContainer);
+ }
} else {
mNormalColor = mContext.getColor(
com.android.internal.R.color.materialColorSurfaceContainerHigh);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt
index 640d364895ae..dccc28f5fe49 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt
@@ -34,11 +34,11 @@ import android.view.LayoutInflater
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
-import android.widget.Switch
import android.widget.TextView
import com.android.settingslib.Utils
import com.android.systemui.res.R
import com.android.systemui.util.Assert
+import com.google.android.material.materialswitch.MaterialSwitch
/** Half-shelf for notification channel controls */
class ChannelEditorListView(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) {
@@ -139,12 +139,12 @@ class ChannelEditorListView(c: Context, attrs: AttributeSet) : LinearLayout(c, a
class AppControlView(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) {
lateinit var iconView: ImageView
lateinit var channelName: TextView
- lateinit var switch: Switch
+ lateinit var switch: MaterialSwitch
override fun onFinishInflate() {
iconView = requireViewById(R.id.icon)
channelName = requireViewById(R.id.app_name)
- switch = requireViewById(R.id.toggle)
+ switch = requireViewById(R.id.material_toggle)
setOnClickListener { switch.toggle() }
}
@@ -155,7 +155,7 @@ class ChannelRow(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) {
lateinit var controller: ChannelEditorDialogController
private lateinit var channelName: TextView
private lateinit var channelDescription: TextView
- private lateinit var switch: Switch
+ private lateinit var switch: MaterialSwitch
private val highlightColor: Int
var gentle = false
@@ -175,7 +175,7 @@ class ChannelRow(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) {
super.onFinishInflate()
channelName = requireViewById(R.id.channel_name)
channelDescription = requireViewById(R.id.channel_description)
- switch = requireViewById(R.id.toggle)
+ switch = requireViewById(R.id.material_toggle)
switch.setOnCheckedChangeListener { _, b ->
channel?.let {
controller.proposeEditForChannel(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 68ad4fad31c1..8da2f768bf71 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -19,9 +19,12 @@ package com.android.systemui.statusbar.notification.row;
import static android.app.Flags.notificationsRedesignTemplates;
import static android.app.Notification.Action.SEMANTIC_ACTION_MARK_CONVERSATION_AS_PRIORITY;
import static android.service.notification.NotificationListenerService.REASON_CANCEL;
+import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_EXPANDED;
+import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
import static com.android.systemui.Flags.notificationRowTransparency;
import static com.android.systemui.Flags.notificationsPinnedHunInShade;
+import static com.android.systemui.Flags.notificationRowAccessibilityExpanded;
import static com.android.systemui.flags.Flags.ENABLE_NOTIFICATIONS_SIMULATE_SLOW_MEASURE;
import static com.android.systemui.statusbar.notification.NotificationUtils.logKey;
import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.PARENT_DISMISSED;
@@ -65,6 +68,7 @@ import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.ViewStub;
import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.widget.Chronometer;
@@ -111,7 +115,6 @@ import com.android.systemui.statusbar.notification.NotificationUtils;
import com.android.systemui.statusbar.notification.SourceType;
import com.android.systemui.statusbar.notification.collection.EntryAdapter;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.collection.NotificationEntryAdapter;
import com.android.systemui.statusbar.notification.collection.PipelineEntry;
import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider;
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
@@ -120,6 +123,7 @@ import com.android.systemui.statusbar.notification.headsup.HeadsUpManager;
import com.android.systemui.statusbar.notification.headsup.PinnedStatus;
import com.android.systemui.statusbar.notification.logging.NotificationCounters;
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
+import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi;
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded;
import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation;
import com.android.systemui.statusbar.notification.row.shared.LockscreenOtpRedaction;
@@ -182,7 +186,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
private boolean mShowSnooze = false;
private boolean mIsFaded;
- private boolean mIsPromotedOngoing = false;
private boolean mHasStatusBarChipDuringHeadsUpAnimation = false;
@Nullable
@@ -406,10 +409,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
private void toggleExpansionState(View v, boolean shouldLogExpandClickMetric) {
- boolean isGroupRoot = NotificationBundleUi.isEnabled()
- ? mGroupMembershipManager.isGroupRoot(mEntryAdapter)
- : mGroupMembershipManager.isGroupSummary(mEntry);
- if (!shouldShowPublic() && (!mIsMinimized || isExpanded()) && isGroupRoot) {
+ if (!shouldShowPublic() && (!mIsMinimized || isExpanded()) && isGroupRoot()) {
mGroupExpansionChanging = true;
if (NotificationBundleUi.isEnabled()) {
final boolean wasExpanded = mGroupExpansionManager.isGroupExpanded(mEntryAdapter);
@@ -871,7 +871,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
private void updateLimitsForView(NotificationContentView layout) {
final int maxExpandedHeight;
- if (isPromotedOngoing()) {
+ if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) {
maxExpandedHeight = mMaxExpandedHeightForPromotedOngoing;
} else {
maxExpandedHeight = mMaxExpandedHeight;
@@ -979,7 +979,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
} else if (isAboveShelf() != wasAboveShelf) {
mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf);
}
- updateBackgroundOpacity();
+ updateColors();
}
/**
@@ -1350,7 +1350,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
if (mIsSummaryWithChildren) {
return mChildrenContainer.getIntrinsicHeight();
}
- if (isPromotedOngoing()) {
+ if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) {
return getMaxExpandHeight();
}
if (mExpandedWhenPinned) {
@@ -2775,7 +2775,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
return false;
}
-
public void applyLaunchAnimationParams(LaunchAnimationParameters params) {
if (params == null) {
// `null` params indicates the animation is over, which means we can't access
@@ -2940,7 +2939,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
if (mIsSummaryWithChildren && !shouldShowPublic()) {
return !mChildrenExpanded;
}
- if (isPromotedOngoing()) {
+ if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) {
return false;
}
return mEnableNonGroupedNotificationExpand && mExpandable;
@@ -2951,17 +2950,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
mPrivateLayout.updateExpandButtons(isExpandable());
}
- /**
- * Set this notification to be promoted ongoing
- */
- public void setPromotedOngoing(boolean promotedOngoing) {
- if (PromotedNotificationUiForceExpanded.isUnexpectedlyInLegacyMode()) {
- return;
- }
-
- mIsPromotedOngoing = promotedOngoing;
- setExpandable(!mIsPromotedOngoing);
- }
/**
* Sets whether the status bar is showing a chip corresponding to this notification.
@@ -3062,10 +3050,13 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
public void setUserLocked(boolean userLocked) {
- if (isPromotedOngoing()) return;
+ if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) return;
mUserLocked = userLocked;
mPrivateLayout.setUserExpanding(userLocked);
+ if (android.app.Flags.expandingPublicView()) {
+ mPublicLayout.setUserExpanding(userLocked);
+ }
// This is intentionally not guarded with mIsSummaryWithChildren since we might have had
// children but not anymore.
if (mChildrenContainer != null) {
@@ -3122,7 +3113,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
mChildrenContainer.setOnKeyguard(onKeyguard);
}
}
- updateBackgroundOpacity();
+ updateColors();
}
}
@@ -3193,6 +3184,12 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
return mGroupExpansionManager.isGroupExpanded(mEntry);
}
+ private boolean isGroupRoot() {
+ return NotificationBundleUi.isEnabled()
+ ? mGroupMembershipManager.isGroupRoot(mEntryAdapter)
+ : mGroupMembershipManager.isGroupSummary(mEntry);
+ }
+
private void onAttachedChildrenCountChanged() {
final boolean wasSummary = mIsSummaryWithChildren;
mIsSummaryWithChildren = mChildrenContainer != null
@@ -3242,7 +3239,16 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
public boolean isPromotedOngoing() {
- return PromotedNotificationUiForceExpanded.isEnabled() && mIsPromotedOngoing;
+ if (!PromotedNotificationUi.isEnabled()) {
+ return false;
+ }
+
+ final NotificationEntry entry = mEntry;
+ if (entry == null) {
+ return false;
+ }
+
+ return entry.isPromotedOngoing();
}
private boolean isPromotedNotificationExpanded(boolean allowOnKeyguard) {
@@ -3271,6 +3277,27 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
/**
+ * Is this row currently showing an expanded state? This method is different from
+ * {@link #isExpanded()}, because it also handles groups, and pinned notifications.
+ */
+ private boolean isShowingExpanded() {
+ if (!shouldShowPublic() && (!mIsMinimized || isExpanded()) && isGroupRoot()) {
+ // is group and expanded?
+ return isGroupExpanded();
+ } else if (mEnableNonGroupedNotificationExpand) {
+ if (isPinned()) {
+ // is pinned and expanded?
+ return mExpandedWhenPinned;
+ } else {
+ // is regular notification and expanded?
+ return isExpanded();
+ }
+ } else {
+ return false;
+ }
+ }
+
+ /**
* Check whether the view state is currently expanded. This is given by the system in {@link
* #setSystemExpanded(boolean)} and can be overridden by user expansion or
* collapsing in {@link #setUserExpanded(boolean)}. Note that the visual appearance of this
@@ -3283,7 +3310,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
public boolean isExpanded(boolean allowOnKeyguard) {
- if (isPromotedOngoing()) {
+ if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) {
return isPromotedNotificationExpanded(allowOnKeyguard);
}
@@ -3952,9 +3979,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
public void onExpandedByGesture(boolean userExpanded) {
int event = MetricsEvent.ACTION_NOTIFICATION_GESTURE_EXPANDER;
- if (NotificationBundleUi.isEnabled()
- ? mGroupMembershipManager.isGroupRoot(mEntryAdapter)
- : mGroupMembershipManager.isGroupSummary(mEntry)) {
+ if (isGroupRoot()) {
event = MetricsEvent.ACTION_NOTIFICATION_GROUP_GESTURE_EXPANDER;
}
mMetricsLogger.action(event, userExpanded);
@@ -4007,6 +4032,18 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
if (mExpansionChangedListener != null) {
mExpansionChangedListener.onExpansionChanged(nowExpanded);
}
+ if (notificationRowAccessibilityExpanded()) {
+ notifyAccessibilityContentExpansionChanged();
+ }
+ }
+ }
+
+ private void notifyAccessibilityContentExpansionChanged() {
+ if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+ AccessibilityEvent event = AccessibilityEvent.obtain();
+ event.setEventType(TYPE_WINDOW_CONTENT_CHANGED);
+ event.setContentChangeTypes(CONTENT_CHANGE_TYPE_EXPANDED);
+ sendAccessibilityEventUnchecked(event);
}
}
@@ -4037,33 +4074,50 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
super.onInitializeAccessibilityNodeInfoInternal(info);
final boolean isLongClickable = isNotificationRowLongClickable();
if (isLongClickable) {
- info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK);
+ info.addAction(AccessibilityAction.ACTION_LONG_CLICK);
}
info.setLongClickable(isLongClickable);
if (canViewBeDismissed() && !mIsSnoozed) {
- info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_DISMISS);
+ info.addAction(AccessibilityAction.ACTION_DISMISS);
}
- boolean expandable = shouldShowPublic();
- boolean isExpanded = false;
- if (!expandable) {
- if (mIsSummaryWithChildren) {
- expandable = true;
- if (!mIsMinimized || isExpanded()) {
- isExpanded = isGroupExpanded();
+
+ if (notificationRowAccessibilityExpanded()) {
+ if (isAccessibilityExpandable()) {
+ if (isShowingExpanded()) {
+ info.addAction(AccessibilityAction.ACTION_COLLAPSE);
+ info.setExpandedState(AccessibilityNodeInfo.EXPANDED_STATE_FULL);
+ } else {
+ info.addAction(AccessibilityAction.ACTION_EXPAND);
+ info.setExpandedState(AccessibilityNodeInfo.EXPANDED_STATE_COLLAPSED);
}
} else {
- expandable = mPrivateLayout.isContentExpandable();
- isExpanded = isExpanded();
+ info.setExpandedState(AccessibilityNodeInfo.EXPANDED_STATE_UNDEFINED);
}
- }
- if (expandable && !mIsSnoozed) {
- if (isExpanded) {
- info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE);
- } else {
- info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND);
+ } else {
+ boolean expandable = shouldShowPublic();
+ boolean isExpanded = false;
+ if (!expandable) {
+ if (mIsSummaryWithChildren) {
+ expandable = true;
+ if (!mIsMinimized || isExpanded()) {
+ isExpanded = isGroupExpanded();
+ }
+ } else {
+ expandable = mPrivateLayout.isContentExpandable();
+ isExpanded = isExpanded();
+ }
+ }
+
+ if (expandable) {
+ if (isExpanded) {
+ info.addAction(AccessibilityAction.ACTION_COLLAPSE);
+ } else {
+ info.addAction(AccessibilityAction.ACTION_EXPAND);
+ }
}
}
+
NotificationMenuRowPlugin provider = getProvider();
if (provider != null) {
MenuItem snoozeMenu = provider.getSnoozeMenuItem(getContext());
@@ -4076,6 +4130,12 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
}
+ /** @return whether this row's expansion state can be toggled by an accessibility action. */
+ private boolean isAccessibilityExpandable() {
+ // don't add expand accessibility actions to snoozed notifications
+ return !mIsSnoozed && isContentExpandable();
+ }
+
@Override
public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
if (super.performAccessibilityActionInternal(action, arguments)) {
@@ -4339,8 +4399,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
+ (!shouldShowPublic() && mIsSummaryWithChildren));
pw.print(", mShowNoBackground: " + mShowNoBackground);
pw.print(", clipBounds: " + getClipBounds());
- if (PromotedNotificationUiForceExpanded.isEnabled()) {
- pw.print(", isPromotedOngoing: " + isPromotedOngoing());
+ pw.print(", isPromotedOngoing: " + isPromotedOngoing());
+ if (notificationRowAccessibilityExpanded()) {
+ pw.print(", isShowingExpanded: " + isShowingExpanded());
+ pw.print(", isAccessibilityExpandable: " + isAccessibilityExpandable());
}
pw.print(", isExpandable: " + isExpandable());
pw.print(", mExpandable: " + mExpandable);
@@ -4569,11 +4631,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
}
- private void updateBackgroundOpacity() {
- if (mBackgroundNormal != null) {
- // Row background should be opaque when it's displayed as a heads-up notification or
- // displayed on keyguard.
- mBackgroundNormal.setForceOpaque(mIsHeadsUp || mOnKeyguard);
- }
+ @Override
+ protected boolean usesTransparentBackground() {
+ return super.usesTransparentBackground() && !mIsHeadsUp && !mOnKeyguard;
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManager.kt
new file mode 100644
index 000000000000..2b451406eaad
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManager.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.util.ListenerSet
+import java.util.function.Consumer
+import javax.inject.Inject
+
+/**
+ * Pipeline components can register consumers here to be informed when a notification action is
+ * clicked
+ */
+@SysUISingleton
+class NotificationActionClickManager @Inject constructor() {
+ private val actionClickListeners = ListenerSet<Consumer<NotificationEntry>>()
+
+ fun addActionClickListener(listener: Consumer<NotificationEntry>) {
+ actionClickListeners.addIfAbsent(listener)
+ }
+
+ fun removeActionClickListener(listener: Consumer<NotificationEntry>) {
+ actionClickListeners.remove(listener)
+ }
+
+ fun onNotificationActionClicked(entry: NotificationEntry) {
+ for (listener in actionClickListeners) {
+ listener.accept(entry)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java
index e1219e88a405..4914e1073059 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java
@@ -39,7 +39,6 @@ import androidx.annotation.Nullable;
import com.android.internal.graphics.ColorUtils;
import com.android.internal.util.ContrastColorUtil;
import com.android.systemui.Dumpable;
-import com.android.systemui.common.shared.colors.SurfaceEffectColors;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.notification.shared.NotificationAddXOnHoverToDismiss;
import com.android.systemui.util.DrawableDumpKt;
@@ -156,14 +155,6 @@ public class NotificationBackgroundView extends View implements Dumpable,
mBgIsColorized = b;
}
- /** Sets if the background should be opaque. */
- public void setForceOpaque(boolean forceOpaque) {
- mForceOpaque = forceOpaque;
- if (notificationRowTransparency()) {
- updateBaseLayerColor();
- }
- }
-
private Path calculateDismissButtonCutoutPath(Rect backgroundBounds) {
// TODO(b/365585705): Adapt to RTL after the UX design is finalized.
@@ -327,10 +318,7 @@ public class NotificationBackgroundView extends View implements Dumpable,
// For colorized notifications, this uses a color that matches the tint color at 90% alpha.
int color = isColorized()
? ColorUtils.setAlphaComponent(mTintColor, (int) (MAX_ALPHA * 0.9f))
- : SurfaceEffectColors.surfaceEffect1(getContext());
- if (mForceOpaque) {
- color = ColorUtils.setAlphaComponent(color, MAX_ALPHA);
- }
+ : mNormalColor;
getBaseBackgroundLayer().setColorFilter(
new PorterDuffColorFilter(
color,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
index 51569c1596ef..3ffc052b5acc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
@@ -59,7 +59,6 @@ import com.android.systemui.statusbar.notification.NmSummarizationUiFlag;
import com.android.systemui.statusbar.notification.NotificationUtils;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor;
-import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded;
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel;
import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation;
import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation;
@@ -1006,10 +1005,6 @@ public class NotificationContentInflater implements NotificationRowContentBinder
entry.setPromotedNotificationContentModel(result.mPromotedContent);
}
- if (PromotedNotificationUiForceExpanded.isEnabled()) {
- row.setPromotedOngoing(entry.isOngoingPromoted());
- }
-
boolean setRepliesAndActions = true;
if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) {
if (result.inflatedContentView != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
index 482b315aa14d..b1c145e08777 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
@@ -53,7 +53,6 @@ import com.android.systemui.statusbar.notification.NmSummarizationUiFlag
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.logKey
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor
-import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED
import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_EXPANDED
@@ -1293,6 +1292,7 @@ constructor(
runningInflations,
e,
row,
+ entry,
callback,
logger,
"applying view synchronously",
@@ -1318,6 +1318,7 @@ constructor(
runningInflations,
InflationException(invalidReason),
row,
+ entry,
callback,
logger,
"applied invalid view",
@@ -1377,6 +1378,7 @@ constructor(
runningInflations,
e,
row,
+ entry,
callback,
logger,
"applying view",
@@ -1480,6 +1482,7 @@ constructor(
runningInflations: HashMap<Int, CancellationSignal>,
e: Exception,
notification: ExpandableNotificationRow?,
+ entry: NotificationEntry,
callback: InflationCallback?,
logger: NotificationRowContentBinderLogger,
logContext: String,
@@ -1487,7 +1490,7 @@ constructor(
Assert.isMainThread()
logger.logAsyncTaskException(notification?.loggingKey, logContext, e)
runningInflations.values.forEach(Consumer { obj: CancellationSignal -> obj.cancel() })
- callback?.handleInflationException(notification?.entry, e)
+ callback?.handleInflationException(entry, e)
}
/**
@@ -1520,10 +1523,6 @@ constructor(
entry.promotedNotificationContentModel = result.promotedContent
}
- if (PromotedNotificationUiForceExpanded.isEnabled) {
- row.setPromotedOngoing(entry.isOngoingPromoted())
- }
-
result.inflatedSmartReplyState?.let { row.privateLayout.setInflatedSmartReplyState(it) }
setContentViewsFromRemoteViews(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt
index f00c3ae20e30..53728c7da62d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt
@@ -89,7 +89,8 @@ data class ActiveNotificationModel(
init {
if (!PromotedNotificationContentModel.featureFlagEnabled()) {
if (promotedContent != null) {
- Log.wtf(TAG, "passing non-null promoted content without feature flag enabled")
+ // TODO(b/401018545): convert to Log.wtf and fix tests (see: ag/32114199)
+ Log.e(TAG, "passing non-null promoted content without feature flag enabled")
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
index 9aa4c54c4292..e4e56c5de65b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -58,10 +58,10 @@ import com.android.systemui.keyguard.shared.model.KeyguardState;
import com.android.systemui.keyguard.shared.model.TransitionState;
import com.android.systemui.keyguard.shared.model.TransitionStep;
import com.android.systemui.log.SessionTracker;
+import com.android.systemui.media.NotificationMediaManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.res.R;
import com.android.systemui.scene.shared.model.Scenes;
-import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.statusbar.policy.KeyguardStateController;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index fc721bfae369..a4ee4ad6f6ec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -140,6 +140,7 @@ import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.media.NotificationMediaManager;
import com.android.systemui.navigationbar.NavigationBarController;
import com.android.systemui.navigationbar.views.NavigationBarView;
import com.android.systemui.notetask.NoteTaskController;
@@ -191,7 +192,6 @@ import com.android.systemui.statusbar.LiftReveal;
import com.android.systemui.statusbar.LightRevealScrim;
import com.android.systemui.statusbar.LockscreenShadeTransitionController;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
-import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.NotificationPresenter;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.NotificationShadeDepthController;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
index 74b1c3bbfd77..2c8866fef030 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
@@ -38,6 +38,7 @@ import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.InitController;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor;
+import com.android.systemui.media.NotificationMediaManager;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
import com.android.systemui.power.domain.interactor.PowerInteractor;
@@ -50,7 +51,6 @@ import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.LockscreenShadeTransitionController;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
-import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.NotificationPresenter;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.NotificationShadeWindowController;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
index b6628926dc4b..61b7d80a8c85 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
@@ -25,7 +25,6 @@ import android.view.View
import androidx.annotation.VisibleForTesting
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.CoreStartable
-import com.android.systemui.Dumpable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
@@ -162,6 +161,10 @@ constructor(
notificationKey = currentInfo.key,
appName = currentInfo.appName,
promotedContent = currentInfo.promotedContent,
+ // [hasOngoingCall()] filters out the case in which the call is ongoing but the app
+ // is visible (we issue [OngoingCallModel.NoCall] below in that case), so this can
+ // be safely made false.
+ isAppVisible = false,
)
} else {
return OngoingCallModel.NoCall
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt
index d6ca656356e3..bed9e7cf4646 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt
@@ -51,9 +51,7 @@ import kotlinx.coroutines.flow.stateIn
* This class monitors call notifications and the visibility of call apps to determine the
* appropriate chip state. It emits:
* * - [OngoingCallModel.NoCall] when there is no call notification
- * * - [OngoingCallModel.InCallWithVisibleApp] when there is a call notification but the call app is
- * visible
- * * - [OngoingCallModel.InCall] when there is a call notification and the call app is not visible
+ * * - [OngoingCallModel.InCall] when there is a call notification
*/
@SysUISingleton
class OngoingCallInteractor
@@ -85,12 +83,14 @@ constructor(
initialValue = OngoingCallModel.NoCall,
)
+ // TODO(b/400720280): maybe put this inside [OngoingCallModel].
@VisibleForTesting
val isStatusBarRequiredForOngoingCall =
combine(ongoingCallState, isChipSwipedAway) { callState, chipSwipedAway ->
- callState is OngoingCallModel.InCall && !chipSwipedAway
+ callState.willCallChipBeVisible() && !chipSwipedAway
}
+ // TODO(b/400720280): maybe put this inside [OngoingCallModel].
@VisibleForTesting
val isGestureListeningEnabled =
combine(
@@ -98,9 +98,12 @@ constructor(
statusBarModeRepositoryStore.defaultDisplay.isInFullscreenMode,
isChipSwipedAway,
) { callState, isFullscreen, chipSwipedAway ->
- callState is OngoingCallModel.InCall && !chipSwipedAway && isFullscreen
+ callState.willCallChipBeVisible() && !chipSwipedAway && isFullscreen
}
+ private fun OngoingCallModel.willCallChipBeVisible() =
+ this is OngoingCallModel.InCall && !isAppVisible
+
private fun createOngoingCallStateFlow(
notification: ActiveNotificationModel?
): Flow<OngoingCallModel> {
@@ -147,34 +150,23 @@ constructor(
model: ActiveNotificationModel,
isVisible: Boolean,
): OngoingCallModel {
- return when {
- isVisible -> {
- logger.d({ "Call app is visible: uid=$int1" }) { int1 = model.uid }
- OngoingCallModel.InCallWithVisibleApp(
- startTimeMs = model.whenTime,
- notificationIconView = model.statusBarChipIconView,
- intent = model.contentIntent,
- notificationKey = model.key,
- appName = model.appName,
- promotedContent = model.promotedContent,
- )
- }
-
- else -> {
- logger.d({ "Active call detected: startTime=$long1 hasIcon=$bool1" }) {
- long1 = model.whenTime
- bool1 = model.statusBarChipIconView != null
- }
- OngoingCallModel.InCall(
- startTimeMs = model.whenTime,
- notificationIconView = model.statusBarChipIconView,
- intent = model.contentIntent,
- notificationKey = model.key,
- appName = model.appName,
- promotedContent = model.promotedContent,
- )
- }
+ logger.d({
+ "Active call detected: uid=$int1 startTime=$long1 hasIcon=$bool1 isAppVisible=$bool2"
+ }) {
+ int1 = model.uid
+ long1 = model.whenTime
+ bool1 = model.statusBarChipIconView != null
+ bool2 = isVisible
}
+ return OngoingCallModel.InCall(
+ startTimeMs = model.whenTime,
+ notificationIconView = model.statusBarChipIconView,
+ intent = model.contentIntent,
+ notificationKey = model.key,
+ appName = model.appName,
+ promotedContent = model.promotedContent,
+ isAppVisible = isVisible,
+ )
}
private fun setStatusBarRequiredForOngoingCall(statusBarRequired: Boolean) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt
index 62f0ba032f36..322dfff8f144 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt
@@ -26,25 +26,6 @@ sealed interface OngoingCallModel {
data object NoCall : OngoingCallModel
/**
- * There is an ongoing call but the call app is currently visible, so we don't need to show the
- * chip.
- *
- * @property startTimeMs see [InCall.startTimeMs].
- * @property notificationIconView see [InCall.notificationIconView].
- * @property intent see [InCall.intent].
- * @property appName see [InCall.appName].
- * @property promotedContent see [InCall.promotedContent].
- */
- data class InCallWithVisibleApp(
- val startTimeMs: Long,
- val notificationIconView: StatusBarIconView?,
- val intent: PendingIntent?,
- val notificationKey: String,
- val appName: String,
- val promotedContent: PromotedNotificationContentModel?,
- ) : OngoingCallModel
-
- /**
* There *is* an ongoing call.
*
* @property startTimeMs the time that the phone call started, based on the notification's
@@ -58,6 +39,7 @@ sealed interface OngoingCallModel {
* @property appName the user-readable name of the app that posted the call notification.
* @property promotedContent if the call notification also meets promoted notification criteria,
* this field is filled in with the content related to promotion. Otherwise null.
+ * @property isAppVisible whether the app to which the call belongs is currently visible.
*/
data class InCall(
val startTimeMs: Long,
@@ -66,5 +48,6 @@ sealed interface OngoingCallModel {
val notificationKey: String,
val appName: String,
val promotedContent: PromotedNotificationContentModel?,
+ val isAppVisible: Boolean,
) : OngoingCallModel
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index 29528502aa03..db1977b3ff45 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -45,6 +45,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.Mobil
import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionsRepositoryKairosImpl
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorImpl
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorKairosAdapter
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorKairosImpl
import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
@@ -92,8 +93,10 @@ import kotlinx.coroutines.flow.Flow
DemoModeMobileConnectionDataSourceKairosImpl.Module::class,
MobileRepositorySwitcherKairos.Module::class,
MobileConnectionsRepositoryKairosImpl.Module::class,
+ MobileIconsInteractorKairosImpl.Module::class,
MobileConnectionRepositoryKairosFactoryImpl.Module::class,
MobileConnectionsRepositoryKairosAdapter.Module::class,
+ MobileIconsInteractorKairosAdapter.Module::class,
]
)
abstract class StatusBarPipelineModule {
@@ -171,7 +174,7 @@ abstract class StatusBarPipelineModule {
@Provides
fun mobileIconsInteractor(
impl: Provider<MobileIconsInteractorImpl>,
- kairosImpl: Provider<MobileIconsInteractorKairosImpl>,
+ kairosImpl: Provider<MobileIconsInteractorKairosAdapter>,
): MobileIconsInteractor {
return if (Flags.statusBarMobileIconKairos()) {
kairosImpl.get()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairos.kt
index 4580ad974b29..a9399593973b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairos.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairos.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 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.
@@ -22,38 +22,37 @@ import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.settingslib.graph.SignalDrawable
import com.android.settingslib.mobile.MobileIconCarrierIdOverrides
import com.android.settingslib.mobile.MobileIconCarrierIdOverridesImpl
-import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.KairosBuilder
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.State
+import com.android.systemui.kairos.combine
+import com.android.systemui.kairos.flatMap
+import com.android.systemui.kairos.map
+import com.android.systemui.kairosBuilder
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.log.table.logDiffsForTable
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Connected
import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepositoryKairos
import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel
import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel.DefaultIcon
import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel.OverriddenIcon
import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
import com.android.systemui.statusbar.pipeline.satellite.ui.model.SatelliteIconModel
import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
+@ExperimentalKairosApi
interface MobileIconInteractorKairos {
/** The table log created for this connection */
val tableLogBuffer: TableLogBuffer
/** The current mobile data activity */
- val activity: Flow<DataActivityModel>
+ val activity: State<DataActivityModel>
/** See [MobileConnectionsRepository.mobileIsDefault]. */
- val mobileIsDefault: Flow<Boolean>
+ val mobileIsDefault: State<Boolean>
/**
* True when telephony tells us that the data state is CONNECTED. See
@@ -61,31 +60,31 @@ interface MobileIconInteractorKairos {
* consider this connection to be serving data, and thus want to show a network type icon, when
* data is connected. Other data connection states would typically cause us not to show the icon
*/
- val isDataConnected: StateFlow<Boolean>
+ val isDataConnected: State<Boolean>
/** True if we consider this connection to be in service, i.e. can make calls */
- val isInService: StateFlow<Boolean>
+ val isInService: State<Boolean>
/** True if this connection is emergency only */
- val isEmergencyOnly: StateFlow<Boolean>
+ val isEmergencyOnly: State<Boolean>
/** Observable for the data enabled state of this connection */
- val isDataEnabled: StateFlow<Boolean>
+ val isDataEnabled: State<Boolean>
/** True if the RAT icon should always be displayed and false otherwise. */
- val alwaysShowDataRatIcon: StateFlow<Boolean>
+ val alwaysShowDataRatIcon: State<Boolean>
/** Canonical representation of the current mobile signal strength as a triangle. */
- val signalLevelIcon: StateFlow<SignalIconModel>
+ val signalLevelIcon: State<SignalIconModel>
/** Observable for RAT type (network type) indicator */
- val networkTypeIconGroup: StateFlow<NetworkTypeIconModel>
+ val networkTypeIconGroup: State<NetworkTypeIconModel>
/** Whether or not to show the slice attribution */
- val showSliceAttribution: StateFlow<Boolean>
+ val showSliceAttribution: State<Boolean>
/** True if this connection is satellite-based */
- val isNonTerrestrial: StateFlow<Boolean>
+ val isNonTerrestrial: State<Boolean>
/**
* Provider name for this network connection. The name can be one of 3 values:
@@ -95,7 +94,7 @@ interface MobileIconInteractorKairos {
* override in [connectionInfo.operatorAlphaShort], a value that is derived from
* [ServiceState]
*/
- val networkName: StateFlow<NetworkNameModel>
+ val networkName: State<NetworkNameModel>
/**
* Provider name for this network connection. The name can be one of 3 values:
@@ -108,119 +107,110 @@ interface MobileIconInteractorKairos {
* TODO(b/296600321): De-duplicate this field with [networkName] after determining the data
* provided is identical
*/
- val carrierName: StateFlow<String>
+ val carrierName: State<String>
/** True if there is only one active subscription. */
- val isSingleCarrier: StateFlow<Boolean>
+ val isSingleCarrier: State<Boolean>
/**
* True if this connection is considered roaming. The roaming bit can come from [ServiceState],
* or directly from the telephony manager's CDMA ERI number value. Note that we don't consider a
* connection to be roaming while carrier network change is active
*/
- val isRoaming: StateFlow<Boolean>
+ val isRoaming: State<Boolean>
/** See [MobileIconsInteractor.isForceHidden]. */
- val isForceHidden: Flow<Boolean>
+ val isForceHidden: State<Boolean>
/** See [MobileConnectionRepository.isAllowedDuringAirplaneMode]. */
- val isAllowedDuringAirplaneMode: StateFlow<Boolean>
+ val isAllowedDuringAirplaneMode: State<Boolean>
/** True when in carrier network change mode */
- val carrierNetworkChangeActive: StateFlow<Boolean>
+ val carrierNetworkChangeActive: State<Boolean>
}
/** Interactor for a single mobile connection. This connection _should_ have one subscription ID */
-@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@ExperimentalKairosApi
class MobileIconInteractorKairosImpl(
- @Background scope: CoroutineScope,
- defaultSubscriptionHasDataEnabled: StateFlow<Boolean>,
- override val alwaysShowDataRatIcon: StateFlow<Boolean>,
- alwaysUseCdmaLevel: StateFlow<Boolean>,
- override val isSingleCarrier: StateFlow<Boolean>,
- override val mobileIsDefault: StateFlow<Boolean>,
- defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>>,
- defaultMobileIconGroup: StateFlow<MobileIconGroup>,
- isDefaultConnectionFailed: StateFlow<Boolean>,
- override val isForceHidden: Flow<Boolean>,
- connectionRepository: MobileConnectionRepository,
+ defaultSubscriptionHasDataEnabled: State<Boolean>,
+ override val alwaysShowDataRatIcon: State<Boolean>,
+ alwaysUseCdmaLevel: State<Boolean>,
+ override val isSingleCarrier: State<Boolean>,
+ override val mobileIsDefault: State<Boolean>,
+ defaultMobileIconMapping: State<Map<String, MobileIconGroup>>,
+ defaultMobileIconGroup: State<MobileIconGroup>,
+ isDefaultConnectionFailed: State<Boolean>,
+ override val isForceHidden: State<Boolean>,
+ private val connectionRepository: MobileConnectionRepositoryKairos,
private val context: Context,
- val carrierIdOverrides: MobileIconCarrierIdOverrides = MobileIconCarrierIdOverridesImpl(),
-) : MobileIconInteractor, MobileIconInteractorKairos {
- override val tableLogBuffer: TableLogBuffer = connectionRepository.tableLogBuffer
+ private val carrierIdOverrides: MobileIconCarrierIdOverrides =
+ MobileIconCarrierIdOverridesImpl(),
+) : MobileIconInteractorKairos, KairosBuilder by kairosBuilder() {
+ override val tableLogBuffer: TableLogBuffer
+ get() = connectionRepository.tableLogBuffer
- override val activity = connectionRepository.dataActivityDirection
+ override val activity: State<DataActivityModel>
+ get() = connectionRepository.dataActivityDirection
- override val isDataEnabled: StateFlow<Boolean> = connectionRepository.dataEnabled
+ override val isDataEnabled: State<Boolean> = connectionRepository.dataEnabled
- override val carrierNetworkChangeActive: StateFlow<Boolean> =
- connectionRepository.carrierNetworkChangeActive
+ override val carrierNetworkChangeActive: State<Boolean>
+ get() = connectionRepository.carrierNetworkChangeActive
// True if there exists _any_ icon override for this carrierId. Note that overrides can include
// any or none of the icon groups defined in MobileMappings, so we still need to check on a
// per-network-type basis whether or not the given icon group is overridden
- private val carrierIdIconOverrideExists =
- connectionRepository.carrierId
- .map { carrierIdOverrides.carrierIdEntryExists(it) }
- .distinctUntilChanged()
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+ private val carrierIdIconOverrideExists: State<Boolean> =
+ connectionRepository.carrierId.map { carrierIdOverrides.carrierIdEntryExists(it) }
- override val networkName =
+ override val networkName: State<NetworkNameModel> =
combine(connectionRepository.operatorAlphaShort, connectionRepository.networkName) {
- operatorAlphaShort,
- networkName ->
- if (networkName is NetworkNameModel.Default && operatorAlphaShort != null) {
- NetworkNameModel.IntentDerived(operatorAlphaShort)
- } else {
- networkName
- }
+ operatorAlphaShort,
+ networkName ->
+ if (networkName is NetworkNameModel.Default && operatorAlphaShort != null) {
+ NetworkNameModel.IntentDerived(operatorAlphaShort)
+ } else {
+ networkName
}
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- connectionRepository.networkName.value,
- )
+ }
- override val carrierName =
+ override val carrierName: State<String> =
combine(connectionRepository.operatorAlphaShort, connectionRepository.carrierName) {
- operatorAlphaShort,
- networkName ->
- if (networkName is NetworkNameModel.Default && operatorAlphaShort != null) {
- operatorAlphaShort
- } else {
- networkName.name
- }
+ operatorAlphaShort,
+ networkName ->
+ if (networkName is NetworkNameModel.Default && operatorAlphaShort != null) {
+ operatorAlphaShort
+ } else {
+ networkName.name
}
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- connectionRepository.carrierName.value.name,
- )
+ }
/** What the mobile icon would be before carrierId overrides */
- private val defaultNetworkType: StateFlow<MobileIconGroup> =
+ private val defaultNetworkType: State<MobileIconGroup> =
combine(
- connectionRepository.resolvedNetworkType,
- defaultMobileIconMapping,
- defaultMobileIconGroup,
- ) { resolvedNetworkType, mapping, defaultGroup ->
- when (resolvedNetworkType) {
- is ResolvedNetworkType.CarrierMergedNetworkType ->
- resolvedNetworkType.iconGroupOverride
- else -> {
- mapping[resolvedNetworkType.lookupKey] ?: defaultGroup
- }
+ connectionRepository.resolvedNetworkType,
+ defaultMobileIconMapping,
+ defaultMobileIconGroup,
+ ) { resolvedNetworkType, mapping, defaultGroup ->
+ when (resolvedNetworkType) {
+ is ResolvedNetworkType.CarrierMergedNetworkType ->
+ resolvedNetworkType.iconGroupOverride
+
+ else -> {
+ mapping[resolvedNetworkType.lookupKey] ?: defaultGroup
}
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), defaultMobileIconGroup.value)
+ }
- override val networkTypeIconGroup =
- combine(defaultNetworkType, carrierIdIconOverrideExists) { networkType, overrideExists ->
+ override val networkTypeIconGroup: State<NetworkTypeIconModel> = buildState {
+ combineTransactionally(defaultNetworkType, carrierIdIconOverrideExists) {
+ networkType,
+ overrideExists ->
// DefaultIcon comes out of the icongroup lookup, we check for overrides here
if (overrideExists) {
val iconOverride =
carrierIdOverrides.getOverrideFor(
- connectionRepository.carrierId.value,
+ connectionRepository.carrierId.sample(),
networkType.name,
context.resources,
)
@@ -233,106 +223,101 @@ class MobileIconInteractorKairosImpl(
DefaultIcon(networkType)
}
}
- .distinctUntilChanged()
- .logDiffsForTable(
- tableLogBuffer = tableLogBuffer,
- initialValue = DefaultIcon(defaultMobileIconGroup.value),
- )
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- DefaultIcon(defaultMobileIconGroup.value),
- )
+ .also { logDiffsForTable(it, tableLogBuffer = tableLogBuffer) }
+ }
- override val showSliceAttribution: StateFlow<Boolean> =
+ override val showSliceAttribution: State<Boolean> =
combine(
- connectionRepository.allowNetworkSliceIndicator,
- connectionRepository.hasPrioritizedNetworkCapabilities,
- ) { allowed, hasPrioritizedNetworkCapabilities ->
- allowed && hasPrioritizedNetworkCapabilities
- }
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+ connectionRepository.allowNetworkSliceIndicator,
+ connectionRepository.hasPrioritizedNetworkCapabilities,
+ ) { allowed, hasPrioritizedNetworkCapabilities ->
+ allowed && hasPrioritizedNetworkCapabilities
+ }
- override val isNonTerrestrial: StateFlow<Boolean> = connectionRepository.isNonTerrestrial
+ override val isNonTerrestrial: State<Boolean>
+ get() = connectionRepository.isNonTerrestrial
- override val isRoaming: StateFlow<Boolean> =
+ override val isRoaming: State<Boolean> =
combine(
- connectionRepository.carrierNetworkChangeActive,
- connectionRepository.isGsm,
- connectionRepository.isRoaming,
- connectionRepository.cdmaRoaming,
- ) { carrierNetworkChangeActive, isGsm, isRoaming, cdmaRoaming ->
- if (carrierNetworkChangeActive) {
- false
- } else if (isGsm) {
- isRoaming
- } else {
- cdmaRoaming
- }
+ connectionRepository.carrierNetworkChangeActive,
+ connectionRepository.isGsm,
+ connectionRepository.isRoaming,
+ connectionRepository.cdmaRoaming,
+ ) { carrierNetworkChangeActive, isGsm, isRoaming, cdmaRoaming ->
+ if (carrierNetworkChangeActive) {
+ false
+ } else if (isGsm) {
+ isRoaming
+ } else {
+ cdmaRoaming
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+ }
- private val level: StateFlow<Int> =
+ private val level: State<Int> =
combine(
- connectionRepository.isGsm,
- connectionRepository.primaryLevel,
- connectionRepository.cdmaLevel,
- alwaysUseCdmaLevel,
- ) { isGsm, primaryLevel, cdmaLevel, alwaysUseCdmaLevel ->
- when {
- // GSM connections should never use the CDMA level
- isGsm -> primaryLevel
- alwaysUseCdmaLevel -> cdmaLevel
- else -> primaryLevel
- }
+ connectionRepository.isGsm,
+ connectionRepository.primaryLevel,
+ connectionRepository.cdmaLevel,
+ alwaysUseCdmaLevel,
+ ) { isGsm, primaryLevel, cdmaLevel, alwaysUseCdmaLevel ->
+ when {
+ // GSM connections should never use the CDMA level
+ isGsm -> primaryLevel
+ alwaysUseCdmaLevel -> cdmaLevel
+ else -> primaryLevel
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), 0)
+ }
- private val numberOfLevels: StateFlow<Int> = connectionRepository.numberOfLevels
+ private val numberOfLevels: State<Int>
+ get() = connectionRepository.numberOfLevels
- override val isDataConnected: StateFlow<Boolean> =
+ override val isDataConnected: State<Boolean> =
connectionRepository.dataConnectionState
.map { it == Connected }
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+ .also {
+ onActivated { logDiffsForTable(it, tableLogBuffer, "icon", "isDataConnected") }
+ }
- override val isInService = connectionRepository.isInService
+ override val isInService
+ get() = connectionRepository.isInService
- override val isEmergencyOnly: StateFlow<Boolean> = connectionRepository.isEmergencyOnly
+ override val isEmergencyOnly: State<Boolean>
+ get() = connectionRepository.isEmergencyOnly
- override val isAllowedDuringAirplaneMode = connectionRepository.isAllowedDuringAirplaneMode
+ override val isAllowedDuringAirplaneMode: State<Boolean>
+ get() = connectionRepository.isAllowedDuringAirplaneMode
/** Whether or not to show the error state of [SignalDrawable] */
- private val showExclamationMark: StateFlow<Boolean> =
+ private val showExclamationMark: State<Boolean> =
combine(defaultSubscriptionHasDataEnabled, isDefaultConnectionFailed, isInService) {
- isDefaultDataEnabled,
- isDefaultConnectionFailed,
- isInService ->
- !isDefaultDataEnabled || isDefaultConnectionFailed || !isInService
- }
- .stateIn(scope, SharingStarted.WhileSubscribed(), true)
+ isDefaultDataEnabled,
+ isDefaultConnectionFailed,
+ isInService ->
+ !isDefaultDataEnabled || isDefaultConnectionFailed || !isInService
+ }
- private val cellularShownLevel: StateFlow<Int> =
+ private val cellularShownLevel: State<Int> =
combine(level, isInService, connectionRepository.inflateSignalStrength) {
- level,
- isInService,
- inflate ->
- if (isInService) {
- if (inflate) level + 1 else level
- } else 0
+ level,
+ isInService,
+ inflate ->
+ when {
+ !isInService -> 0
+ inflate -> level + 1
+ else -> level
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), 0)
+ }
// Satellite level is unaffected by the inflateSignalStrength property
// See b/346904529 for details
- private val satelliteShownLevel: StateFlow<Int> =
+ private val satelliteShownLevel: State<Int> =
if (Flags.carrierRoamingNbIotNtn()) {
- connectionRepository.satelliteLevel
- } else {
- combine(level, isInService) { level, isInService -> if (isInService) level else 0 }
- }
- .stateIn(scope, SharingStarted.WhileSubscribed(), 0)
+ connectionRepository.satelliteLevel
+ } else {
+ combine(level, isInService) { level, isInService -> if (isInService) level else 0 }
+ }
- private val cellularIcon: Flow<SignalIconModel.Cellular> =
+ private val cellularIcon: State<SignalIconModel.Cellular> =
combine(
cellularShownLevel,
numberOfLevels,
@@ -347,7 +332,7 @@ class MobileIconInteractorKairosImpl(
)
}
- private val satelliteIcon: Flow<SignalIconModel.Satellite> =
+ private val satelliteIcon: State<SignalIconModel.Satellite> =
satelliteShownLevel.map {
SignalIconModel.Satellite(
level = it,
@@ -357,24 +342,14 @@ class MobileIconInteractorKairosImpl(
)
}
- override val signalLevelIcon: StateFlow<SignalIconModel> = run {
- val initial =
- SignalIconModel.Cellular(
- cellularShownLevel.value,
- numberOfLevels.value,
- showExclamationMark.value,
- carrierNetworkChangeActive.value,
- )
+ override val signalLevelIcon: State<SignalIconModel> =
isNonTerrestrial
- .flatMapLatest { ntn ->
+ .flatMap { ntn ->
if (ntn) {
satelliteIcon
} else {
cellularIcon
}
}
- .distinctUntilChanged()
- .logDiffsForTable(tableLogBuffer, columnPrefix = "icon", initialValue = initial)
- .stateIn(scope, SharingStarted.WhileSubscribed(), initial)
- }
+ .also { onActivated { logDiffsForTable(it, tableLogBuffer, columnPrefix = "icon") } }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapter.kt
new file mode 100644
index 000000000000..a3b9b17257cf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapter.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
+
+import com.android.systemui.kairos.BuildScope
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.toColdConflatedFlow
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel
+import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
+
+@ExperimentalKairosApi
+fun BuildScope.MobileIconInteractorKairosAdapter(
+ kairosImpl: MobileIconInteractorKairos
+): MobileIconInteractor =
+ with(kairosImpl) {
+ MobileIconInteractorKairosAdapter(
+ tableLogBuffer = tableLogBuffer,
+ activity = activity.toColdConflatedFlow(kairosNetwork),
+ mobileIsDefault = mobileIsDefault.toColdConflatedFlow(kairosNetwork),
+ isDataConnected = isDataConnected.toStateFlow(),
+ isInService = isInService.toStateFlow(),
+ isEmergencyOnly = isEmergencyOnly.toStateFlow(),
+ isDataEnabled = isDataEnabled.toStateFlow(),
+ alwaysShowDataRatIcon = alwaysShowDataRatIcon.toStateFlow(),
+ signalLevelIcon = signalLevelIcon.toStateFlow(),
+ networkTypeIconGroup = networkTypeIconGroup.toStateFlow(),
+ showSliceAttribution = showSliceAttribution.toStateFlow(),
+ isNonTerrestrial = isNonTerrestrial.toStateFlow(),
+ networkName = networkName.toStateFlow(),
+ carrierName = carrierName.toStateFlow(),
+ isSingleCarrier = isSingleCarrier.toStateFlow(),
+ isRoaming = isRoaming.toStateFlow(),
+ isForceHidden = isForceHidden.toColdConflatedFlow(kairosNetwork),
+ isAllowedDuringAirplaneMode = isAllowedDuringAirplaneMode.toStateFlow(),
+ carrierNetworkChangeActive = carrierNetworkChangeActive.toStateFlow(),
+ )
+ }
+
+private class MobileIconInteractorKairosAdapter(
+ override val tableLogBuffer: TableLogBuffer,
+ override val activity: Flow<DataActivityModel>,
+ override val mobileIsDefault: Flow<Boolean>,
+ override val isDataConnected: StateFlow<Boolean>,
+ override val isInService: StateFlow<Boolean>,
+ override val isEmergencyOnly: StateFlow<Boolean>,
+ override val isDataEnabled: StateFlow<Boolean>,
+ override val alwaysShowDataRatIcon: StateFlow<Boolean>,
+ override val signalLevelIcon: StateFlow<SignalIconModel>,
+ override val networkTypeIconGroup: StateFlow<NetworkTypeIconModel>,
+ override val showSliceAttribution: StateFlow<Boolean>,
+ override val isNonTerrestrial: StateFlow<Boolean>,
+ override val networkName: StateFlow<NetworkNameModel>,
+ override val carrierName: StateFlow<String>,
+ override val isSingleCarrier: StateFlow<Boolean>,
+ override val isRoaming: StateFlow<Boolean>,
+ override val isForceHidden: Flow<Boolean>,
+ override val isAllowedDuringAirplaneMode: StateFlow<Boolean>,
+ override val carrierNetworkChangeActive: StateFlow<Boolean>,
+) : MobileIconInteractor
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairos.kt
index e8e0a833af2a..14a276b60933 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairos.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairos.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 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.
@@ -21,41 +21,47 @@ import android.telephony.CarrierConfigManager
import android.telephony.SubscriptionManager
import android.telephony.SubscriptionManager.PROFILE_CLASS_PROVISIONING
import com.android.settingslib.SignalIcon.MobileIconGroup
-import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.Flags
+import com.android.systemui.KairosActivatable
+import com.android.systemui.KairosBuilder
+import com.android.systemui.activated
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.flags.FeatureFlagsClassic
import com.android.systemui.flags.Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS
+import com.android.systemui.kairos.BuildScope
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.Incremental
+import com.android.systemui.kairos.State
+import com.android.systemui.kairos.asyncEvent
+import com.android.systemui.kairos.buildSpec
+import com.android.systemui.kairos.combine
+import com.android.systemui.kairos.filter
+import com.android.systemui.kairos.flatMap
+import com.android.systemui.kairos.flatten
+import com.android.systemui.kairos.map
+import com.android.systemui.kairos.mapValues
+import com.android.systemui.kairos.stateOf
+import com.android.systemui.kairosBuilder
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.log.table.logDiffsForTable
import com.android.systemui.statusbar.core.NewStatusBarIcons
import com.android.systemui.statusbar.core.StatusBarRootModernization
import com.android.systemui.statusbar.pipeline.dagger.MobileSummaryLog
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepositoryKairos
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepositoryKairos
import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
import com.android.systemui.statusbar.policy.data.repository.UserSetupRepository
import com.android.systemui.util.CarrierConfigTracker
-import java.lang.ref.WeakReference
+import dagger.Binds
+import dagger.Provides
+import dagger.multibindings.ElementsIntoSet
import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
+import javax.inject.Provider
+import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.mapLatest
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.flow.transformLatest
/**
* Business layer logic for the set of mobile subscription icons.
@@ -67,98 +73,79 @@ import kotlinx.coroutines.flow.transformLatest
* represents each RAT (LTE, 3G, etc.), as well as can produce an interactor for each individual
* icon
*/
+@ExperimentalKairosApi
interface MobileIconsInteractorKairos {
/** See [MobileConnectionsRepository.mobileIsDefault]. */
- val mobileIsDefault: StateFlow<Boolean>
+ val mobileIsDefault: State<Boolean>
/** List of subscriptions, potentially filtered for CBRS */
- val filteredSubscriptions: Flow<List<SubscriptionModel>>
-
- /** Subscription ID of the current default data subscription */
- val defaultDataSubId: Flow<Int?>
+ val filteredSubscriptions: State<List<SubscriptionModel>>
/**
* The current list of [MobileIconInteractor]s associated with the current list of
* [filteredSubscriptions]
*/
- val icons: StateFlow<List<MobileIconInteractor>>
+ val icons: Incremental<Int, MobileIconInteractorKairos>
/** Whether the mobile icons can be stacked vertically. */
- val isStackable: StateFlow<Boolean>
-
- /**
- * Observable for the subscriptionId of the current mobile data connection. Null if we don't
- * have a valid subscription id
- */
- val activeMobileDataSubscriptionId: StateFlow<Int?>
+ val isStackable: State<Boolean>
/** True if the active mobile data subscription has data enabled */
- val activeDataConnectionHasDataEnabled: StateFlow<Boolean>
+ val activeDataConnectionHasDataEnabled: State<Boolean>
/**
* Flow providing a reference to the Interactor for the active data subId. This represents the
- * [MobileIconInteractor] responsible for the active data connection, if any.
+ * [MobileIconInteractorKairos] responsible for the active data connection, if any.
*/
- val activeDataIconInteractor: StateFlow<MobileIconInteractor?>
+ val activeDataIconInteractor: State<MobileIconInteractorKairos?>
/** True if the RAT icon should always be displayed and false otherwise. */
- val alwaysShowDataRatIcon: StateFlow<Boolean>
+ val alwaysShowDataRatIcon: State<Boolean>
/** True if the CDMA level should be preferred over the primary level. */
- val alwaysUseCdmaLevel: StateFlow<Boolean>
+ val alwaysUseCdmaLevel: State<Boolean>
/** True if there is only one active subscription. */
- val isSingleCarrier: StateFlow<Boolean>
+ val isSingleCarrier: State<Boolean>
/** The icon mapping from network type to [MobileIconGroup] for the default subscription */
- val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>>
+ val defaultMobileIconMapping: State<Map<String, MobileIconGroup>>
/** Fallback [MobileIconGroup] in the case where there is no icon in the mapping */
- val defaultMobileIconGroup: StateFlow<MobileIconGroup>
+ val defaultMobileIconGroup: State<MobileIconGroup>
/** True only if the default network is mobile, and validation also failed */
- val isDefaultConnectionFailed: StateFlow<Boolean>
+ val isDefaultConnectionFailed: State<Boolean>
/** True once the user has been set up */
- val isUserSetUp: StateFlow<Boolean>
+ val isUserSetUp: State<Boolean>
/** True if we're configured to force-hide the mobile icons and false otherwise. */
- val isForceHidden: Flow<Boolean>
+ val isForceHidden: State<Boolean>
/**
* True if the device-level service state (with -1 subscription id) reports emergency calls
* only. This value is only useful when there are no other subscriptions OR all existing
* subscriptions report that they are not in service.
*/
- val isDeviceInEmergencyCallsOnlyMode: Flow<Boolean>
-
- /**
- * Vends out a [MobileIconInteractor] tracking the [MobileConnectionRepository] for the given
- * subId.
- */
- fun getMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor
+ val isDeviceInEmergencyCallsOnlyMode: State<Boolean>
}
-@OptIn(ExperimentalCoroutinesApi::class)
-@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@ExperimentalKairosApi
@SysUISingleton
class MobileIconsInteractorKairosImpl
@Inject
constructor(
- private val mobileConnectionsRepo: MobileConnectionsRepository,
+ private val mobileConnectionsRepo: MobileConnectionsRepositoryKairos,
private val carrierConfigTracker: CarrierConfigTracker,
@MobileSummaryLog private val tableLogger: TableLogBuffer,
connectivityRepository: ConnectivityRepository,
userSetupRepo: UserSetupRepository,
- @Background private val scope: CoroutineScope,
private val context: Context,
private val featureFlagsClassic: FeatureFlagsClassic,
-) : MobileIconsInteractor, MobileIconsInteractorKairos {
-
- // Weak reference lookup for created interactors
- private val reuseCache = mutableMapOf<Int, WeakReference<MobileIconInteractor>>()
+) : MobileIconsInteractorKairos, KairosBuilder by kairosBuilder() {
- override val mobileIsDefault =
+ override val mobileIsDefault: State<Boolean> =
combine(
mobileConnectionsRepo.mobileIsDefault,
mobileConnectionsRepo.hasCarrierMergedConnection,
@@ -167,47 +154,36 @@ constructor(
// the `isDefault` calculation. See b/272586234.
mobileIsDefault || hasCarrierMergedConnection
}
- .logDiffsForTable(
- tableLogger,
- LOGGING_PREFIX,
- columnName = "mobileIsDefault",
- initialValue = false,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
-
- override val activeMobileDataSubscriptionId: StateFlow<Int?> =
- mobileConnectionsRepo.activeMobileDataSubscriptionId
-
- override val activeDataConnectionHasDataEnabled: StateFlow<Boolean> =
- mobileConnectionsRepo.activeMobileDataRepository
- .flatMapLatest { it?.dataEnabled ?: flowOf(false) }
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
-
- override val activeDataIconInteractor: StateFlow<MobileIconInteractor?> =
- mobileConnectionsRepo.activeMobileDataSubscriptionId
- .mapLatest {
- if (it != null) {
- getMobileConnectionInteractorForSubId(it)
- } else {
- null
+ .also {
+ onActivated {
+ logDiffsForTable(
+ it,
+ tableLogger,
+ LOGGING_PREFIX,
+ columnName = "mobileIsDefault",
+ )
}
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), null)
- private val unfilteredSubscriptions: Flow<List<SubscriptionModel>> =
- mobileConnectionsRepo.subscriptions
+ override val activeDataConnectionHasDataEnabled: State<Boolean> =
+ mobileConnectionsRepo.activeMobileDataRepository.flatMap {
+ it?.dataEnabled ?: stateOf(false)
+ }
+
+ private val unfilteredSubscriptions: State<Collection<SubscriptionModel>>
+ get() = mobileConnectionsRepo.subscriptions
/** Any filtering that we can do based purely on the info of each subscription individually. */
- private val subscriptionsBasedFilteredSubs =
- unfilteredSubscriptions
- .map { it.filterBasedOnProvisioning().filterBasedOnNtn() }
- .distinctUntilChanged()
+ private val subscriptionsBasedFilteredSubs: State<List<SubscriptionModel>> =
+ unfilteredSubscriptions.map {
+ it.asSequence().filterBasedOnProvisioning().filterBasedOnNtn().toList()
+ }
- private fun List<SubscriptionModel>.filterBasedOnProvisioning(): List<SubscriptionModel> =
+ private fun Sequence<SubscriptionModel>.filterBasedOnProvisioning() =
if (!featureFlagsClassic.isEnabled(FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS)) {
this
} else {
- this.filter { it.profileClass != PROFILE_CLASS_PROVISIONING }
+ filter { it.profileClass != PROFILE_CLASS_PROVISIONING }
}
/**
@@ -219,9 +195,10 @@ constructor(
* need to filter out those subscriptions here so we guarantee the subscription never turns into
* an icon. See b/336881301.
*/
- private fun List<SubscriptionModel>.filterBasedOnNtn(): List<SubscriptionModel> {
- return this.filter { !it.isExclusivelyNonTerrestrial }
- }
+ private fun Sequence<SubscriptionModel>.filterBasedOnNtn(): Sequence<SubscriptionModel> =
+ filter {
+ !it.isExclusivelyNonTerrestrial
+ }
/**
* Generally, SystemUI wants to show iconography for each subscription that is listed by
@@ -236,22 +213,23 @@ constructor(
* [CarrierConfigManager.KEY_ALWAYS_SHOW_PRIMARY_SIGNAL_BAR_IN_OPPORTUNISTIC_NETWORK_BOOLEAN],
* and by checking which subscription is opportunistic, or which one is active.
*/
- override val filteredSubscriptions: Flow<List<SubscriptionModel>> =
+ override val filteredSubscriptions: State<List<SubscriptionModel>> = buildState {
combine(
subscriptionsBasedFilteredSubs,
mobileConnectionsRepo.activeMobileDataSubscriptionId,
- connectivityRepository.vcnSubId,
+ connectivityRepository.vcnSubId.toState(),
) { preFilteredSubs, activeId, vcnSubId ->
filterSubsBasedOnOpportunistic(preFilteredSubs, activeId, vcnSubId)
}
- .distinctUntilChanged()
- .logDiffsForTable(
- tableLogger,
- LOGGING_PREFIX,
- columnName = "filteredSubscriptions",
- initialValue = listOf(),
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), listOf())
+ .also {
+ logDiffsForTable(
+ it,
+ tableLogger,
+ LOGGING_PREFIX,
+ columnName = "filteredSubscriptions",
+ )
+ }
+ }
private fun filterSubsBasedOnOpportunistic(
subList: List<SubscriptionModel>,
@@ -298,19 +276,25 @@ constructor(
}
}
- override val defaultDataSubId = mobileConnectionsRepo.defaultDataSubId
-
- override val icons =
- filteredSubscriptions
- .mapLatest { subs ->
- subs.map { getMobileConnectionInteractorForSubId(it.subscriptionId) }
+ override val icons: Incremental<Int, MobileIconInteractorKairos> = buildIncremental {
+ val filteredSubIds =
+ filteredSubscriptions.map { it.asSequence().map { sub -> sub.subscriptionId }.toSet() }
+ mobileConnectionsRepo.mobileConnectionsBySubId
+ .filterIncrementally { (subId, _) ->
+ // Filter out repo if subId is not present in the filtered set
+ filteredSubIds.map { subId in it }
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList())
+ // Just map the repos to interactors
+ .mapValues { (subId, repo) -> buildSpec { mobileConnection(repo) } }
+ .applyLatestSpecForKey()
+ }
- override val isStackable =
+ override val isStackable: State<Boolean> =
if (NewStatusBarIcons.isEnabled && StatusBarRootModernization.isEnabled) {
- icons.flatMapLatest { icons ->
- combine(icons.map { it.signalLevelIcon }) { signalLevelIcons ->
+ icons.flatMap { iconsBySubId: Map<Int, MobileIconInteractorKairos> ->
+ iconsBySubId.values
+ .map { it.signalLevelIcon }
+ .combine { signalLevelIcons ->
// These are only stackable if:
// - They are cellular
// - There's exactly two
@@ -319,11 +303,15 @@ constructor(
it.size == 2 && it[0].numberOfLevels == it[1].numberOfLevels
}
}
- }
- } else {
- flowOf(false)
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+ } else {
+ stateOf(false)
+ }
+
+ override val activeDataIconInteractor: State<MobileIconInteractorKairos?> =
+ combine(mobileConnectionsRepo.activeMobileDataSubscriptionId, icons) { activeSubId, icons ->
+ activeSubId?.let { icons[activeSubId] }
+ }
/**
* Copied from the old pipeline. We maintain a 2s period of time where we will keep the
@@ -335,67 +323,59 @@ constructor(
*
* The goal of this is to minimize the flickering in the UI of the cellular indicator
*/
- private val forcingCellularValidation =
+ private val forcingCellularValidation: State<Boolean> = buildState {
mobileConnectionsRepo.activeSubChangedInGroupEvent
- .filter { mobileConnectionsRepo.defaultConnectionIsValidated.value }
- .transformLatest {
- emit(true)
- delay(2000)
- emit(false)
+ .filter(mobileConnectionsRepo.defaultConnectionIsValidated)
+ .mapLatestBuild {
+ asyncEvent {
+ delay(2.seconds)
+ false
+ }
+ .holdState(true)
+ }
+ .holdState(stateOf(false))
+ .flatten()
+ .also {
+ logDiffsForTable(it, tableLogger, LOGGING_PREFIX, columnName = "forcingValidation")
}
- .logDiffsForTable(
- tableLogger,
- LOGGING_PREFIX,
- columnName = "forcingValidation",
- initialValue = false,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+ }
/**
* Mapping from network type to [MobileIconGroup] using the config generated for the default
* subscription Id. This mapping is the same for every subscription.
*/
- override val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>> =
- mobileConnectionsRepo.defaultMobileIconMapping.stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- initialValue = mapOf(),
- )
+ override val defaultMobileIconMapping: State<Map<String, MobileIconGroup>>
+ get() = mobileConnectionsRepo.defaultMobileIconMapping
- override val alwaysShowDataRatIcon: StateFlow<Boolean> =
- mobileConnectionsRepo.defaultDataSubRatConfig
- .mapLatest { it.alwaysShowDataRatIcon }
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+ override val alwaysShowDataRatIcon: State<Boolean> =
+ mobileConnectionsRepo.defaultDataSubRatConfig.map { it.alwaysShowDataRatIcon }
- override val alwaysUseCdmaLevel: StateFlow<Boolean> =
- mobileConnectionsRepo.defaultDataSubRatConfig
- .mapLatest { it.alwaysShowCdmaRssi }
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+ override val alwaysUseCdmaLevel: State<Boolean> =
+ mobileConnectionsRepo.defaultDataSubRatConfig.map { it.alwaysShowCdmaRssi }
- override val isSingleCarrier: StateFlow<Boolean> =
+ override val isSingleCarrier: State<Boolean> =
mobileConnectionsRepo.subscriptions
.map { it.size == 1 }
- .logDiffsForTable(
- tableLogger,
- columnPrefix = LOGGING_PREFIX,
- columnName = "isSingleCarrier",
- initialValue = false,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+ .also {
+ onActivated {
+ logDiffsForTable(
+ it,
+ tableLogger,
+ columnPrefix = LOGGING_PREFIX,
+ columnName = "isSingleCarrier",
+ )
+ }
+ }
/** If there is no mapping in [defaultMobileIconMapping], then use this default icon group */
- override val defaultMobileIconGroup: StateFlow<MobileIconGroup> =
- mobileConnectionsRepo.defaultMobileIconGroup.stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- initialValue = TelephonyIcons.G,
- )
+ override val defaultMobileIconGroup: State<MobileIconGroup>
+ get() = mobileConnectionsRepo.defaultMobileIconGroup
/**
* We want to show an error state when cellular has actually failed to validate, but not if some
* other transport type is active, because then we expect there not to be validation.
*/
- override val isDefaultConnectionFailed: StateFlow<Boolean> =
+ override val isDefaultConnectionFailed: State<Boolean> =
combine(
mobileIsDefault,
mobileConnectionsRepo.defaultConnectionIsValidated,
@@ -407,46 +387,63 @@ constructor(
else -> !defaultConnectionIsValidated
}
}
- .logDiffsForTable(
- tableLogger,
- LOGGING_PREFIX,
- columnName = "isDefaultConnectionFailed",
- initialValue = false,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
-
- override val isUserSetUp: StateFlow<Boolean> = userSetupRepo.isUserSetUp
-
- override val isForceHidden: Flow<Boolean> =
- connectivityRepository.forceHiddenSlots
- .map { it.contains(ConnectivitySlot.MOBILE) }
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
-
- override val isDeviceInEmergencyCallsOnlyMode: Flow<Boolean> =
- mobileConnectionsRepo.isDeviceEmergencyCallCapable
-
- /** Vends out new [MobileIconInteractor] for a particular subId */
- override fun getMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor =
- reuseCache[subId]?.get() ?: createMobileConnectionInteractorForSubId(subId)
-
- private fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor =
- MobileIconInteractorImpl(
- scope,
- activeDataConnectionHasDataEnabled,
- alwaysShowDataRatIcon,
- alwaysUseCdmaLevel,
- isSingleCarrier,
- mobileIsDefault,
- defaultMobileIconMapping,
- defaultMobileIconGroup,
- isDefaultConnectionFailed,
- isForceHidden,
- mobileConnectionsRepo.getRepoForSubId(subId),
- context,
- )
- .also { reuseCache[subId] = WeakReference(it) }
+ .also {
+ onActivated {
+ logDiffsForTable(
+ it,
+ tableLogger,
+ LOGGING_PREFIX,
+ columnName = "isDefaultConnectionFailed",
+ )
+ }
+ }
+
+ override val isUserSetUp: State<Boolean> = buildState { userSetupRepo.isUserSetUp.toState() }
+
+ override val isForceHidden: State<Boolean> = buildState {
+ connectivityRepository.forceHiddenSlots.toState().map {
+ it.contains(ConnectivitySlot.MOBILE)
+ }
+ }
+
+ override val isDeviceInEmergencyCallsOnlyMode: State<Boolean>
+ get() = mobileConnectionsRepo.isDeviceEmergencyCallCapable
+
+ /** Vends out a new [MobileIconInteractorKairos] for a particular subId */
+ private fun BuildScope.mobileConnection(
+ repo: MobileConnectionRepositoryKairos
+ ): MobileIconInteractorKairos = activated {
+ MobileIconInteractorKairosImpl(
+ activeDataConnectionHasDataEnabled,
+ alwaysShowDataRatIcon,
+ alwaysUseCdmaLevel,
+ isSingleCarrier,
+ mobileIsDefault,
+ defaultMobileIconMapping,
+ defaultMobileIconGroup,
+ isDefaultConnectionFailed,
+ isForceHidden,
+ repo,
+ context,
+ )
+ }
companion object {
private const val LOGGING_PREFIX = "Intr"
}
+
+ @dagger.Module
+ interface Module {
+
+ @Binds fun bindImpl(impl: MobileIconsInteractorKairosImpl): MobileIconsInteractorKairos
+
+ companion object {
+ @Provides
+ @ElementsIntoSet
+ fun kairosActivatable(
+ impl: Provider<MobileIconsInteractorKairosImpl>
+ ): Set<@JvmSuppressWildcards KairosActivatable> =
+ if (Flags.statusBarMobileIconKairos()) setOf(impl.get()) else emptySet()
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosAdapter.kt
new file mode 100644
index 000000000000..87877b3e9f43
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosAdapter.kt
@@ -0,0 +1,172 @@
+/*
+ * 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.pipeline.mobile.domain.interactor
+
+import android.content.Context
+import com.android.settingslib.SignalIcon
+import com.android.settingslib.mobile.MobileMappings
+import com.android.systemui.Flags
+import com.android.systemui.KairosActivatable
+import com.android.systemui.KairosBuilder
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosNetwork
+import com.android.systemui.kairos.buildSpec
+import com.android.systemui.kairos.combine
+import com.android.systemui.kairos.map
+import com.android.systemui.kairos.mapValues
+import com.android.systemui.kairos.toColdConflatedFlow
+import com.android.systemui.kairosBuilder
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepositoryKairos
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
+import com.android.systemui.statusbar.policy.data.repository.UserSetupRepository
+import dagger.Provides
+import dagger.multibindings.ElementsIntoSet
+import javax.inject.Inject
+import javax.inject.Provider
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.stateIn
+
+@ExperimentalKairosApi
+@SysUISingleton
+class MobileIconsInteractorKairosAdapter
+@Inject
+constructor(
+ private val kairosInteractor: MobileIconsInteractorKairos,
+ private val repo: MobileConnectionsRepository,
+ repoK: MobileConnectionsRepositoryKairos,
+ kairosNetwork: KairosNetwork,
+ @Application scope: CoroutineScope,
+ context: Context,
+ mobileMappingsProxy: MobileMappingsProxy,
+ private val userSetupRepo: UserSetupRepository,
+) : MobileIconsInteractor, KairosBuilder by kairosBuilder() {
+
+ private val interactorsBySubIdK = buildIncremental {
+ kairosInteractor.icons
+ .mapValues { (subId, interactor) ->
+ buildSpec { MobileIconInteractorKairosAdapter(interactor) }
+ }
+ .applyLatestSpecForKey()
+ }
+
+ private val interactorsBySubId =
+ interactorsBySubIdK
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.Eagerly, emptyMap())
+
+ override val defaultDataSubId: Flow<Int?>
+ get() = repo.defaultDataSubId
+
+ override val mobileIsDefault: StateFlow<Boolean> =
+ kairosInteractor.mobileIsDefault
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), repo.mobileIsDefault.value)
+
+ override val filteredSubscriptions: Flow<List<SubscriptionModel>> =
+ kairosInteractor.filteredSubscriptions.toColdConflatedFlow(kairosNetwork)
+
+ override val icons: StateFlow<List<MobileIconInteractor>> =
+ interactorsBySubIdK
+ .map { it.values.toList() }
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList())
+
+ override val isStackable: StateFlow<Boolean> =
+ kairosInteractor.isStackable
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val activeMobileDataSubscriptionId: StateFlow<Int?>
+ get() = repo.activeMobileDataSubscriptionId
+
+ override val activeDataConnectionHasDataEnabled: StateFlow<Boolean> =
+ kairosInteractor.activeDataConnectionHasDataEnabled
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val activeDataIconInteractor: StateFlow<MobileIconInteractor?> =
+ combine(repoK.activeMobileDataSubscriptionId, interactorsBySubIdK) { subId, interactors ->
+ interactors[subId]
+ }
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+
+ override val alwaysShowDataRatIcon: StateFlow<Boolean> =
+ kairosInteractor.alwaysShowDataRatIcon
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val alwaysUseCdmaLevel: StateFlow<Boolean> =
+ kairosInteractor.alwaysUseCdmaLevel
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val isSingleCarrier: StateFlow<Boolean> =
+ kairosInteractor.isSingleCarrier
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val defaultMobileIconMapping: StateFlow<Map<String, SignalIcon.MobileIconGroup>> =
+ kairosInteractor.defaultMobileIconMapping
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), emptyMap())
+
+ override val defaultMobileIconGroup: StateFlow<SignalIcon.MobileIconGroup> =
+ kairosInteractor.defaultMobileIconGroup
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(
+ scope,
+ SharingStarted.WhileSubscribed(),
+ mobileMappingsProxy.getDefaultIcons(MobileMappings.Config.readConfig(context)),
+ )
+
+ override val isDefaultConnectionFailed: StateFlow<Boolean> =
+ kairosInteractor.isDefaultConnectionFailed
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val isUserSetUp: StateFlow<Boolean>
+ get() = userSetupRepo.isUserSetUp
+
+ override val isForceHidden: Flow<Boolean> =
+ kairosInteractor.isForceHidden
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val isDeviceInEmergencyCallsOnlyMode: Flow<Boolean>
+ get() = repo.isDeviceEmergencyCallCapable
+
+ override fun getMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor =
+ interactorsBySubId.value[subId] ?: error("Unknown subscription id: $subId")
+
+ @dagger.Module
+ object Module {
+ @Provides
+ @ElementsIntoSet
+ fun kairosActivatable(
+ impl: Provider<MobileIconsInteractorKairosAdapter>
+ ): Set<@JvmSuppressWildcards KairosActivatable> =
+ if (Flags.statusBarMobileIconKairos()) setOf(impl.get()) else emptySet()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModelKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModelKairos.kt
new file mode 100644
index 000000000000..fce8c85338f3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModelKairos.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2022 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.pipeline.mobile.ui.viewmodel
+
+import android.graphics.Color
+import com.android.systemui.statusbar.phone.StatusBarLocation
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor
+import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * A view model for an individual mobile icon that embeds the notion of a [StatusBarLocation]. This
+ * allows the mobile icon to change some view parameters at different locations
+ *
+ * @param commonImpl for convenience, this class wraps a base interface that can provides all of the
+ * common implementations between locations. See [MobileIconViewModel]
+ * @property location the [StatusBarLocation] of this VM.
+ * @property verboseLogger an optional logger to log extremely verbose view updates.
+ */
+abstract class LocationBasedMobileViewModelKairos(
+ val commonImpl: MobileIconViewModelCommonKairos,
+ val location: StatusBarLocation,
+ val verboseLogger: VerboseMobileViewLogger?,
+) : MobileIconViewModelCommonKairos by commonImpl {
+ val defaultColor: Int = Color.WHITE
+
+ companion object {
+ fun viewModelForLocation(
+ commonImpl: MobileIconViewModelCommon,
+ interactor: MobileIconInteractor,
+ verboseMobileViewLogger: VerboseMobileViewLogger,
+ location: StatusBarLocation,
+ scope: CoroutineScope,
+ ): LocationBasedMobileViewModel =
+ when (location) {
+ StatusBarLocation.HOME ->
+ HomeMobileIconViewModel(commonImpl, verboseMobileViewLogger)
+ StatusBarLocation.KEYGUARD -> KeyguardMobileIconViewModel(commonImpl)
+ StatusBarLocation.QS -> QsMobileIconViewModel(commonImpl)
+ StatusBarLocation.SHADE_CARRIER_GROUP ->
+ ShadeCarrierGroupMobileIconViewModel(commonImpl, interactor, scope)
+ }
+ }
+}
+
+class HomeMobileIconViewModelKairos(
+ commonImpl: MobileIconViewModelCommonKairos,
+ verboseMobileViewLogger: VerboseMobileViewLogger,
+) :
+ MobileIconViewModelCommonKairos,
+ LocationBasedMobileViewModelKairos(
+ commonImpl,
+ location = StatusBarLocation.HOME,
+ verboseMobileViewLogger,
+ )
+
+class QsMobileIconViewModelKairos(commonImpl: MobileIconViewModelCommonKairos) :
+ MobileIconViewModelCommonKairos,
+ LocationBasedMobileViewModelKairos(
+ commonImpl,
+ location = StatusBarLocation.QS,
+ // Only do verbose logging for the Home location.
+ verboseLogger = null,
+ )
+
+class ShadeCarrierGroupMobileIconViewModelKairos(
+ commonImpl: MobileIconViewModelCommonKairos,
+ interactor: MobileIconInteractor,
+ scope: CoroutineScope,
+) :
+ MobileIconViewModelCommonKairos,
+ LocationBasedMobileViewModelKairos(
+ commonImpl,
+ location = StatusBarLocation.SHADE_CARRIER_GROUP,
+ // Only do verbose logging for the Home location.
+ verboseLogger = null,
+ ) {
+ private val isSingleCarrier = interactor.isSingleCarrier
+ val carrierName = interactor.carrierName
+
+ override val isVisible: StateFlow<Boolean> =
+ combine(super.isVisible, isSingleCarrier) { isVisible, isSingleCarrier ->
+ if (isSingleCarrier) false else isVisible
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), super.isVisible.value)
+}
+
+class KeyguardMobileIconViewModelKairos(commonImpl: MobileIconViewModelCommonKairos) :
+ MobileIconViewModelCommonKairos,
+ LocationBasedMobileViewModelKairos(
+ commonImpl,
+ location = StatusBarLocation.KEYGUARD,
+ // Only do verbose logging for the Home location.
+ verboseLogger = null,
+ )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt
new file mode 100644
index 000000000000..cc7fc0964dae
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2022 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.pipeline.mobile.ui.viewmodel
+
+import com.android.systemui.Flags.statusBarStaticInoutIndicators
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.log.table.logDiffsForTable
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.core.NewStatusBarIcons
+import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
+import com.android.systemui.statusbar.pipeline.mobile.ui.model.MobileContentDescription
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.stateIn
+
+/** Common interface for all of the location-based mobile icon view models. */
+interface MobileIconViewModelCommonKairos : MobileIconViewModelCommon {
+ override val subscriptionId: Int
+ /** True if this view should be visible at all. */
+ override val isVisible: StateFlow<Boolean>
+ override val icon: Flow<SignalIconModel>
+ override val contentDescription: Flow<MobileContentDescription?>
+ override val roaming: Flow<Boolean>
+ /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */
+ override val networkTypeIcon: Flow<Icon.Resource?>
+ /** The slice attribution. Drawn as a background layer */
+ override val networkTypeBackground: StateFlow<Icon.Resource?>
+ override val activityInVisible: Flow<Boolean>
+ override val activityOutVisible: Flow<Boolean>
+ override val activityContainerVisible: Flow<Boolean>
+}
+
+/**
+ * View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over
+ * a single line of service via [MobileIconInteractor] and update the UI based on that
+ * subscription's information.
+ *
+ * There will be exactly one [MobileIconViewModel] per filtered subscription offered from
+ * [MobileIconsInteractor.filteredSubscriptions].
+ *
+ * For the sake of keeping log spam in check, every flow funding the [MobileIconViewModelCommon]
+ * interface is implemented as a [StateFlow]. This ensures that each location-based mobile icon view
+ * model gets the exact same information, as well as allows us to log that unified state only once
+ * per icon.
+ */
+class MobileIconViewModelKairos(
+ override val subscriptionId: Int,
+ iconInteractor: MobileIconInteractor,
+ airplaneModeInteractor: AirplaneModeInteractor,
+ constants: ConnectivityConstants,
+ scope: CoroutineScope,
+) : MobileIconViewModelCommonKairos {
+ private val cellProvider by lazy {
+ CellularIconViewModelKairos(
+ subscriptionId,
+ iconInteractor,
+ airplaneModeInteractor,
+ constants,
+ scope,
+ )
+ }
+
+ private val satelliteProvider by lazy {
+ CarrierBasedSatelliteViewModelKairosImpl(
+ subscriptionId,
+ airplaneModeInteractor,
+ iconInteractor,
+ scope,
+ )
+ }
+
+ /**
+ * Similar to repository switching, this allows us to split up the logic of satellite/cellular
+ * states, since they are different by nature
+ */
+ private val vmProvider: Flow<MobileIconViewModelCommon> =
+ iconInteractor.isNonTerrestrial
+ .mapLatest { nonTerrestrial ->
+ if (nonTerrestrial) {
+ satelliteProvider
+ } else {
+ cellProvider
+ }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), cellProvider)
+
+ override val isVisible: StateFlow<Boolean> =
+ vmProvider
+ .flatMapLatest { it.isVisible }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val icon: Flow<SignalIconModel> = vmProvider.flatMapLatest { it.icon }
+
+ override val contentDescription: Flow<MobileContentDescription?> =
+ vmProvider.flatMapLatest { it.contentDescription }
+
+ override val roaming: Flow<Boolean> = vmProvider.flatMapLatest { it.roaming }
+
+ override val networkTypeIcon: Flow<Icon.Resource?> =
+ vmProvider.flatMapLatest { it.networkTypeIcon }
+
+ override val networkTypeBackground: StateFlow<Icon.Resource?> =
+ vmProvider
+ .flatMapLatest { it.networkTypeBackground }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+
+ override val activityInVisible: Flow<Boolean> =
+ vmProvider.flatMapLatest { it.activityInVisible }
+
+ override val activityOutVisible: Flow<Boolean> =
+ vmProvider.flatMapLatest { it.activityOutVisible }
+
+ override val activityContainerVisible: Flow<Boolean> =
+ vmProvider.flatMapLatest { it.activityContainerVisible }
+}
+
+/** Representation of this network when it is non-terrestrial (e.g., satellite) */
+private class CarrierBasedSatelliteViewModelKairosImpl(
+ override val subscriptionId: Int,
+ airplaneModeInteractor: AirplaneModeInteractor,
+ interactor: MobileIconInteractor,
+ scope: CoroutineScope,
+) : MobileIconViewModelCommon, MobileIconViewModelCommonKairos {
+ override val isVisible: StateFlow<Boolean> =
+ airplaneModeInteractor.isAirplaneMode
+ .map { !it }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val icon: Flow<SignalIconModel> = interactor.signalLevelIcon
+
+ override val contentDescription: Flow<MobileContentDescription?> = MutableStateFlow(null)
+
+ /** These fields are not used for satellite icons currently */
+ override val roaming: Flow<Boolean> = flowOf(false)
+ override val networkTypeIcon: Flow<Icon.Resource?> = flowOf(null)
+ override val networkTypeBackground: StateFlow<Icon.Resource?> = MutableStateFlow(null)
+ override val activityInVisible: Flow<Boolean> = flowOf(false)
+ override val activityOutVisible: Flow<Boolean> = flowOf(false)
+ override val activityContainerVisible: Flow<Boolean> = flowOf(false)
+}
+
+/** Terrestrial (cellular) icon. */
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+private class CellularIconViewModelKairos(
+ override val subscriptionId: Int,
+ iconInteractor: MobileIconInteractor,
+ airplaneModeInteractor: AirplaneModeInteractor,
+ constants: ConnectivityConstants,
+ scope: CoroutineScope,
+) : MobileIconViewModelCommon, MobileIconViewModelCommonKairos {
+ override val isVisible: StateFlow<Boolean> =
+ if (!constants.hasDataCapabilities) {
+ flowOf(false)
+ } else {
+ combine(
+ airplaneModeInteractor.isAirplaneMode,
+ iconInteractor.isAllowedDuringAirplaneMode,
+ iconInteractor.isForceHidden,
+ ) { isAirplaneMode, isAllowedDuringAirplaneMode, isForceHidden ->
+ if (isForceHidden) {
+ false
+ } else if (isAirplaneMode) {
+ isAllowedDuringAirplaneMode
+ } else {
+ true
+ }
+ }
+ }
+ .distinctUntilChanged()
+ .logDiffsForTable(
+ iconInteractor.tableLogBuffer,
+ columnName = "visible",
+ initialValue = false,
+ )
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val icon: Flow<SignalIconModel> = iconInteractor.signalLevelIcon
+
+ override val contentDescription: Flow<MobileContentDescription?> =
+ combine(iconInteractor.signalLevelIcon, iconInteractor.networkName) { icon, nameModel ->
+ when (icon) {
+ is SignalIconModel.Cellular ->
+ MobileContentDescription.Cellular(
+ nameModel.name,
+ icon.levelDescriptionRes(),
+ )
+ else -> null
+ }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+
+ private fun SignalIconModel.Cellular.levelDescriptionRes() =
+ when (level) {
+ 0 -> R.string.accessibility_no_signal
+ 1 -> R.string.accessibility_one_bar
+ 2 -> R.string.accessibility_two_bars
+ 3 -> R.string.accessibility_three_bars
+ 4 -> {
+ if (numberOfLevels == 6) {
+ R.string.accessibility_four_bars
+ } else {
+ R.string.accessibility_signal_full
+ }
+ }
+ 5 -> {
+ if (numberOfLevels == 6) {
+ R.string.accessibility_signal_full
+ } else {
+ R.string.accessibility_no_signal
+ }
+ }
+ else -> R.string.accessibility_no_signal
+ }
+
+ private val showNetworkTypeIcon: Flow<Boolean> =
+ combine(
+ iconInteractor.isDataConnected,
+ iconInteractor.isDataEnabled,
+ iconInteractor.alwaysShowDataRatIcon,
+ iconInteractor.mobileIsDefault,
+ iconInteractor.carrierNetworkChangeActive,
+ ) { dataConnected, dataEnabled, alwaysShow, mobileIsDefault, carrierNetworkChange ->
+ alwaysShow ||
+ (!carrierNetworkChange && (dataEnabled && dataConnected && mobileIsDefault))
+ }
+ .distinctUntilChanged()
+ .logDiffsForTable(
+ iconInteractor.tableLogBuffer,
+ columnName = "showNetworkTypeIcon",
+ initialValue = false,
+ )
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val networkTypeIcon: Flow<Icon.Resource?> =
+ combine(iconInteractor.networkTypeIconGroup, showNetworkTypeIcon) {
+ networkTypeIconGroup,
+ shouldShow ->
+ val desc =
+ if (networkTypeIconGroup.contentDescription != 0)
+ ContentDescription.Resource(networkTypeIconGroup.contentDescription)
+ else null
+ val icon =
+ if (networkTypeIconGroup.iconId != 0)
+ Icon.Resource(networkTypeIconGroup.iconId, desc)
+ else null
+ return@combine when {
+ !shouldShow -> null
+ else -> icon
+ }
+ }
+ .distinctUntilChanged()
+ .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+
+ override val networkTypeBackground =
+ iconInteractor.showSliceAttribution
+ .map {
+ when {
+ it && NewStatusBarIcons.isEnabled ->
+ Icon.Resource(R.drawable.mobile_network_type_background_updated, null)
+ it -> Icon.Resource(R.drawable.mobile_network_type_background, null)
+ else -> null
+ }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+
+ override val roaming: StateFlow<Boolean> =
+ iconInteractor.isRoaming
+ .logDiffsForTable(
+ iconInteractor.tableLogBuffer,
+ columnName = "roaming",
+ initialValue = false,
+ )
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ private val activity: Flow<DataActivityModel?> =
+ if (!constants.shouldShowActivityConfig) {
+ flowOf(null)
+ } else {
+ iconInteractor.activity
+ }
+
+ override val activityInVisible: Flow<Boolean> =
+ activity
+ .map { it?.hasActivityIn ?: false }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val activityOutVisible: Flow<Boolean> =
+ activity
+ .map { it?.hasActivityOut ?: false }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val activityContainerVisible: Flow<Boolean> =
+ if (statusBarStaticInoutIndicators()) {
+ flowOf(constants.shouldShowActivityConfig)
+ } else {
+ activity.map { it != null && (it.hasActivityIn || it.hasActivityOut) }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt
new file mode 100644
index 000000000000..a65540738828
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2022 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.pipeline.mobile.ui.viewmodel
+
+import androidx.annotation.VisibleForTesting
+import com.android.app.tracing.coroutines.launchTraced as launch
+import com.android.systemui.coroutines.newTracingContext
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.phone.StatusBarLocation
+import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger
+import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger
+import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
+import java.util.concurrent.ConcurrentHashMap
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * View model for describing the system's current mobile cellular connections. The result is a list
+ * of [MobileIconViewModel]s which describe the individual icons and can be bound to
+ * [ModernStatusBarMobileView].
+ */
+@SysUISingleton
+class MobileIconsViewModelKairos
+@Inject
+constructor(
+ val logger: MobileViewLogger,
+ private val verboseLogger: VerboseMobileViewLogger,
+ private val interactor: MobileIconsInteractor,
+ private val airplaneModeInteractor: AirplaneModeInteractor,
+ private val constants: ConnectivityConstants,
+ @Background private val scope: CoroutineScope,
+) {
+ @VisibleForTesting
+ val reuseCache = ConcurrentHashMap<Int, Pair<MobileIconViewModel, CoroutineScope>>()
+
+ val activeMobileDataSubscriptionId: StateFlow<Int?> = interactor.activeMobileDataSubscriptionId
+
+ val subscriptionIdsFlow: StateFlow<List<Int>> =
+ interactor.filteredSubscriptions
+ .mapLatest { subscriptions ->
+ subscriptions.map { subscriptionModel -> subscriptionModel.subscriptionId }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), listOf())
+
+ val mobileSubViewModels: StateFlow<List<MobileIconViewModelCommon>> =
+ subscriptionIdsFlow
+ .map { ids -> ids.map { commonViewModelForSub(it) } }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList())
+
+ private val firstMobileSubViewModel: StateFlow<MobileIconViewModelCommon?> =
+ mobileSubViewModels
+ .map {
+ if (it.isEmpty()) {
+ null
+ } else {
+ // Mobile icons get reversed by [StatusBarIconController], so the last element
+ // in this list will show up visually first.
+ it.last()
+ }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+
+ /**
+ * A flow that emits `true` if the mobile sub that's displayed first visually is showing its
+ * network type icon and `false` otherwise.
+ */
+ val firstMobileSubShowingNetworkTypeIcon: StateFlow<Boolean> =
+ firstMobileSubViewModel
+ .flatMapLatest { firstMobileSubViewModel ->
+ firstMobileSubViewModel?.networkTypeIcon?.map { it != null } ?: flowOf(false)
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ val isStackable: StateFlow<Boolean> = interactor.isStackable
+
+ init {
+ scope.launch { subscriptionIdsFlow.collect { invalidateCaches(it) } }
+ }
+
+ fun viewModelForSub(subId: Int, location: StatusBarLocation): LocationBasedMobileViewModel {
+ val common = commonViewModelForSub(subId)
+ return LocationBasedMobileViewModel.viewModelForLocation(
+ common,
+ interactor.getMobileConnectionInteractorForSubId(subId),
+ verboseLogger,
+ location,
+ scope,
+ )
+ }
+
+ private fun commonViewModelForSub(subId: Int): MobileIconViewModelCommon {
+ return reuseCache.getOrPut(subId) { createViewModel(subId) }.first
+ }
+
+ private fun createViewModel(subId: Int): Pair<MobileIconViewModel, CoroutineScope> {
+ // Create a child scope so we can cancel it
+ val vmScope = scope.createChildScope(newTracingContext("MobileIconViewModel"))
+ val vm =
+ MobileIconViewModel(
+ subId,
+ interactor.getMobileConnectionInteractorForSubId(subId),
+ airplaneModeInteractor,
+ constants,
+ vmScope,
+ )
+
+ return Pair(vm, vmScope)
+ }
+
+ private fun CoroutineScope.createChildScope(extraContext: CoroutineContext) =
+ CoroutineScope(coroutineContext + Job(coroutineContext[Job]) + extraContext)
+
+ private fun invalidateCaches(subIds: List<Int>) {
+ reuseCache.keys
+ .filter { !subIds.contains(it) }
+ .forEach { id ->
+ reuseCache
+ .remove(id)
+ // Cancel the view model's scope after removing it
+ ?.second
+ ?.cancel()
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairos.kt
new file mode 100644
index 000000000000..2dbb02c8f095
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairos.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
+
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.lifecycle.Hydrator
+import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class StackedMobileIconViewModelKairos
+@AssistedInject
+constructor(mobileIconsViewModel: MobileIconsViewModel) : ExclusiveActivatable() {
+ private val hydrator = Hydrator("StackedMobileIconViewModel")
+
+ private val isStackable: Boolean by
+ hydrator.hydratedStateOf(
+ traceName = "isStackable",
+ source = mobileIconsViewModel.isStackable,
+ initialValue = false,
+ )
+
+ private val iconViewModelFlow: Flow<List<MobileIconViewModelCommon>> =
+ combine(
+ mobileIconsViewModel.mobileSubViewModels,
+ mobileIconsViewModel.activeMobileDataSubscriptionId,
+ ) { viewModels, activeSubId ->
+ // Sort to get the active subscription first, if it's set
+ viewModels.sortedByDescending { it.subscriptionId == activeSubId }
+ }
+
+ val dualSim: DualSim? by
+ hydrator.hydratedStateOf(
+ traceName = "dualSim",
+ source =
+ iconViewModelFlow.flatMapLatest { viewModels ->
+ combine(viewModels.map { it.icon }) { icons ->
+ icons
+ .toList()
+ .filterIsInstance<SignalIconModel.Cellular>()
+ .takeIf { it.size == 2 }
+ ?.let { DualSim(it[0], it[1]) }
+ }
+ },
+ initialValue = null,
+ )
+
+ val networkTypeIcon: Icon.Resource? by
+ hydrator.hydratedStateOf(
+ traceName = "networkTypeIcon",
+ source =
+ iconViewModelFlow.flatMapLatest { viewModels ->
+ viewModels.firstOrNull()?.networkTypeIcon ?: flowOf(null)
+ },
+ initialValue = null,
+ )
+
+ val isIconVisible: Boolean by derivedStateOf { isStackable && dualSim != null }
+
+ override suspend fun onActivated(): Nothing {
+ hydrator.activate()
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(): StackedMobileIconViewModelKairos
+ }
+
+ data class DualSim(
+ val primary: SignalIconModel.Cellular,
+ val secondary: SignalIconModel.Cellular,
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt
index cfd50973924d..6fada264e397 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt
@@ -203,6 +203,8 @@ constructor(
OngoingActivityChipBinder.createBinding(
view.requireViewById(R.id.ongoing_activity_chip_secondary)
)
+ OngoingActivityChipBinder.updateTypefaces(primaryChipViewBinding)
+ OngoingActivityChipBinder.updateTypefaces(secondaryChipViewBinding)
launch {
combine(
viewModel.ongoingActivityChipsLegacy,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
index 2b0bf1a3d343..f1f2b88e9943 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -461,6 +461,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
}
}
}
+ unregisterBackCallback();
if (logClose) {
mUiEventLogger.logWithInstanceId(
@@ -558,11 +559,6 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
@Override
public void onVisibilityAggregated(boolean isVisible) {
- if (isVisible) {
- registerBackCallback();
- } else {
- unregisterBackCallback();
- }
super.onVisibilityAggregated(isVisible);
mEditText.setEnabled(isVisible && !mSending);
}
@@ -623,6 +619,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
setAttachment(mEntry.remoteInputAttachment);
updateSendButton();
+ registerBackCallback();
}
public void onNotificationUpdateOrReset() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt
index d3af1e5b65fe..6d959be1c5f4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.policy
import android.app.ActivityOptions
+import android.app.Flags.notificationsRedesignTemplates
import android.app.Notification
import android.app.Notification.Action.SEMANTIC_ACTION_MARK_CONVERSATION_AS_PRIORITY
import android.app.PendingIntent
@@ -53,7 +54,6 @@ import com.android.systemui.statusbar.SmartReplyController
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.headsup.HeadsUpManager
import com.android.systemui.statusbar.notification.logging.NotificationLogger
-import com.android.systemui.statusbar.notification.row.MagicActionBackgroundDrawable
import com.android.systemui.statusbar.phone.KeyguardDismissUtil
import com.android.systemui.statusbar.policy.InflatedSmartReplyState.SuppressedActions
import com.android.systemui.statusbar.policy.SmartReplyView.SmartActions
@@ -397,16 +397,21 @@ constructor(
delayOnClickListener: Boolean,
packageContext: Context,
): Button {
- val isMagicAction = Flags.notificationMagicActionsTreatment() &&
+ val isMagicAction =
+ Flags.notificationMagicActionsTreatment() &&
smartActions.fromAssistant &&
action.extras.getBoolean(Notification.Action.EXTRA_IS_MAGIC, false)
- val layoutRes = if (isMagicAction) {
- R.layout.magic_action_button
- } else {
- R.layout.smart_action_button
- }
- return (LayoutInflater.from(parent.context).inflate(layoutRes, parent, false)
- as Button)
+ val layoutRes =
+ if (isMagicAction) {
+ R.layout.magic_action_button
+ } else {
+ if (notificationsRedesignTemplates()) {
+ R.layout.notification_2025_smart_action_button
+ } else {
+ R.layout.smart_action_button
+ }
+ }
+ return (LayoutInflater.from(parent.context).inflate(layoutRes, parent, false) as Button)
.apply {
text = action.title
@@ -435,7 +440,6 @@ constructor(
// Mark this as an Action button
(layoutParams as SmartReplyView.LayoutParams).mButtonType = SmartButtonType.ACTION
}
-
}
private fun onSmartActionClick(
@@ -499,9 +503,11 @@ constructor(
replyIndex: Int,
choice: CharSequence,
delayOnClickListener: Boolean,
- ): Button =
- (LayoutInflater.from(parent.context).inflate(R.layout.smart_reply_button, parent, false)
- as Button)
+ ): Button {
+ val layoutRes =
+ if (notificationsRedesignTemplates()) R.layout.notification_2025_smart_reply_button
+ else R.layout.smart_reply_button
+ return (LayoutInflater.from(parent.context).inflate(layoutRes, parent, false) as Button)
.apply {
text = choice
val onClickListener =
@@ -531,6 +537,7 @@ constructor(
// Mark this as a Reply button
(layoutParams as SmartReplyView.LayoutParams).mButtonType = SmartButtonType.REPLY
}
+ }
private fun onSmartReplyClick(
entry: NotificationEntry,
diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/TopLevelWindowEffects.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/TopLevelWindowEffects.kt
new file mode 100644
index 000000000000..c11e4c507914
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/TopLevelWindowEffects.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects;
+
+import android.content.Context
+import android.graphics.PixelFormat
+import android.view.Gravity
+import android.view.WindowInsets
+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.Application
+import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor
+import com.android.systemui.topwindoweffects.domain.interactor.SqueezeEffectInteractor
+import com.android.systemui.topwindoweffects.ui.compose.EffectsWindowRoot
+import com.android.systemui.topwindoweffects.ui.viewmodel.SqueezeEffectViewModel
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@SysUISingleton
+class TopLevelWindowEffects @Inject constructor(
+ @Application private val context: Context,
+ @Application private val applicationScope: CoroutineScope,
+ private val windowManager: ViewCaptureAwareWindowManager,
+ private val squeezeEffectInteractor: SqueezeEffectInteractor,
+ private val keyEventInteractor: KeyEventInteractor,
+ private val viewModelFactory: SqueezeEffectViewModel.Factory
+) : CoreStartable {
+
+ override fun start() {
+ applicationScope.launch {
+ var root: EffectsWindowRoot? = null
+ squeezeEffectInteractor.isSqueezeEffectEnabled.collectLatest { enabled ->
+ // TODO: move window ops to a separate UI thread
+ if (enabled) {
+ keyEventInteractor.isPowerButtonDown.collectLatest { down ->
+ // TODO: ignore new window creation when ignoring short power press duration
+ if (down && root == null) {
+ root = EffectsWindowRoot(
+ context = context,
+ viewModelFactory = viewModelFactory,
+ onEffectFinished = {
+ if (root?.isAttachedToWindow == true) {
+ windowManager.removeView(root)
+ root = null
+ }
+ }
+ )
+ root?.let {
+ windowManager.addView(it, getWindowManagerLayoutParams())
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun getWindowManagerLayoutParams(): WindowManager.LayoutParams {
+ val lp = WindowManager.LayoutParams(
+ WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+ or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+ or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
+ PixelFormat.TRANSPARENT
+ )
+
+ lp.privateFlags = lp.privateFlags or
+ (WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS
+ or WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
+ or WindowManager.LayoutParams.PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED
+ or WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_HARDWARE_ACCELERATED
+ or WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY)
+
+ lp.layoutInDisplayCutoutMode =
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+
+ lp.title = "TopLevelWindowEffects"
+ lp.fitInsetsTypes = WindowInsets.Type.systemOverlays()
+ lp.gravity = Gravity.TOP
+
+ return lp
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/dagger/SqueezeEffectRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/dagger/SqueezeEffectRepositoryModule.kt
new file mode 100644
index 000000000000..5a2af9228771
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/dagger/SqueezeEffectRepositoryModule.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects.dagger
+
+import com.android.systemui.topwindoweffects.data.repository.SqueezeEffectRepository
+import com.android.systemui.topwindoweffects.data.repository.SqueezeEffectRepositoryImpl
+import dagger.Binds
+import dagger.Module
+
+@Module
+interface SqueezeEffectRepositoryModule {
+
+ @Binds
+ fun squeezeEffectRepository(
+ squeezeEffectRepositoryImpl: SqueezeEffectRepositoryImpl
+ ) : SqueezeEffectRepository
+} \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/dagger/TopLevelWindowEffectsModule.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/dagger/TopLevelWindowEffectsModule.kt
new file mode 100644
index 000000000000..6fbfedc94774
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/dagger/TopLevelWindowEffectsModule.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects.dagger
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.topwindoweffects.TopLevelWindowEffects
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+
+@Module
+interface TopLevelWindowEffectsModule {
+
+ @Binds
+ @IntoMap
+ @ClassKey(TopLevelWindowEffects::class)
+ fun bindTopLevelWindowEffectsCoreStartable(impl: TopLevelWindowEffects): CoreStartable
+} \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepository.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepository.kt
new file mode 100644
index 000000000000..9e0b25633d5f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepository.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects.data.repository
+
+import kotlinx.coroutines.flow.Flow
+
+interface SqueezeEffectRepository {
+ val isSqueezeEffectEnabled: Flow<Boolean>
+} \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryImpl.kt
new file mode 100644
index 000000000000..9e0feed35016
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryImpl.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects.data.repository
+
+import android.database.ContentObserver
+import android.os.Handler
+import android.provider.Settings.Global.POWER_BUTTON_LONG_PRESS
+import com.android.internal.R
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.shared.Flags
+import com.android.systemui.util.settings.GlobalSettings
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOn
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+
+@SysUISingleton
+class SqueezeEffectRepositoryImpl @Inject constructor(
+ @Background private val bgHandler: Handler?,
+ @Background private val bgCoroutineContext: CoroutineContext,
+ private val globalSettings: GlobalSettings
+) : SqueezeEffectRepository {
+
+ override val isSqueezeEffectEnabled: Flow<Boolean> = conflatedCallbackFlow {
+ val observer = object : ContentObserver(bgHandler) {
+ override fun onChange(selfChange: Boolean) {
+ trySendWithFailureLogging(squeezeEffectEnabled, TAG,
+ "updated isSqueezeEffectEnabled")
+ }
+ }
+ trySendWithFailureLogging(squeezeEffectEnabled, TAG, "init isSqueezeEffectEnabled")
+ globalSettings.registerContentObserverAsync(POWER_BUTTON_LONG_PRESS, observer)
+ awaitClose { globalSettings.unregisterContentObserverAsync(observer) }
+ }.flowOn(bgCoroutineContext)
+
+ private val squeezeEffectEnabled
+ get() = Flags.enableLppSqueezeEffect() && globalSettings.getInt(
+ POWER_BUTTON_LONG_PRESS, R.integer.config_longPressOnPowerBehavior
+ ) == 5 // 5 corresponds to launch assistant in config_longPressOnPowerBehavior
+
+ companion object {
+ private const val TAG = "SqueezeEffectRepository"
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/domain/interactor/SqueezeEffectInteractor.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/domain/interactor/SqueezeEffectInteractor.kt
new file mode 100644
index 000000000000..879fde769aee
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/domain/interactor/SqueezeEffectInteractor.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.topwindoweffects.data.repository.SqueezeEffectRepository
+import javax.inject.Inject
+
+@SysUISingleton
+class SqueezeEffectInteractor @Inject constructor(
+ squeezeEffectRepository: SqueezeEffectRepository
+) {
+ val isSqueezeEffectEnabled = squeezeEffectRepository.isSqueezeEffectEnabled
+} \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/EffectsWindowRoot.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/EffectsWindowRoot.kt
new file mode 100644
index 000000000000..61448f45c0e2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/EffectsWindowRoot.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects.ui.compose
+
+import android.annotation.SuppressLint
+import android.content.Context
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.AbstractComposeView
+import com.android.systemui.compose.ComposeInitializer
+import com.android.systemui.topwindoweffects.ui.viewmodel.SqueezeEffectViewModel
+
+@SuppressLint("ViewConstructor")
+class EffectsWindowRoot(
+ context: Context,
+ private val onEffectFinished: () -> Unit,
+ private val viewModelFactory: SqueezeEffectViewModel.Factory
+) : AbstractComposeView(context) {
+
+ override fun onAttachedToWindow() {
+ ComposeInitializer.onAttachedToWindow(this)
+ super.onAttachedToWindow()
+ }
+
+ override fun onDetachedFromWindow() {
+ super.onDetachedFromWindow()
+ ComposeInitializer.onDetachedFromWindow(this)
+ }
+
+ @Composable
+ override fun Content() {
+ SqueezeEffect(
+ viewModelFactory = viewModelFactory,
+ onEffectFinished = onEffectFinished
+ )
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/SqueezeEffect.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/SqueezeEffect.kt
new file mode 100644
index 000000000000..9809b0592dcf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/SqueezeEffect.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects.ui.compose
+
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Matrix
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.graphics.drawscope.withTransform
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.VectorPainter
+import androidx.compose.ui.graphics.vector.rememberVectorPainter
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.unit.dp
+import com.android.systemui.lifecycle.rememberViewModel
+import com.android.systemui.res.R
+import com.android.systemui.topwindoweffects.ui.viewmodel.SqueezeEffectViewModel
+
+private val SqueezeEffectMaxThickness = 12.dp
+private val SqueezeColor = Color.Black
+
+@Composable
+fun SqueezeEffect(
+ viewModelFactory: SqueezeEffectViewModel.Factory,
+ onEffectFinished: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val viewModel = rememberViewModel(traceName = "SqueezeEffect") { viewModelFactory.create() }
+ val down = viewModel.isPowerButtonPressed
+ val longPressed = viewModel.isPowerButtonLongPressed
+ // TODO: Choose the correct resource based on primary / secondary display
+ val top = rememberVectorPainter(ImageVector.vectorResource(R.drawable.rounded_corner_top))
+ val bottom = rememberVectorPainter(ImageVector.vectorResource(R.drawable.rounded_corner_bottom))
+
+ val squeezeProgress by animateFloatAsState(
+ targetValue =
+ if (down && !longPressed) {
+ 1f
+ } else {
+ 0f
+ },
+ animationSpec = tween(durationMillis = 400),
+ finishedListener = { onEffectFinished() }
+ )
+
+ Canvas(modifier = modifier.fillMaxSize()) {
+ if (squeezeProgress <= 0) {
+ return@Canvas
+ }
+
+ val squeezeThickness = SqueezeEffectMaxThickness.toPx() * squeezeProgress
+
+ drawRect(color = SqueezeColor, size = Size(size.width, squeezeThickness))
+
+ drawRect(
+ color = SqueezeColor,
+ topLeft = Offset(0f, size.height - squeezeThickness),
+ size = Size(size.width, squeezeThickness)
+ )
+
+ drawRect(color = SqueezeColor, size = Size(squeezeThickness, size.height))
+
+ drawRect(
+ color = SqueezeColor,
+ topLeft = Offset(size.width - squeezeThickness, 0f),
+ size = Size(squeezeThickness, size.height)
+ )
+
+ drawTransform(
+ dx = squeezeThickness,
+ dy = squeezeThickness,
+ rotation = 0f,
+ corner = top
+ )
+
+ drawTransform(
+ dx = size.width - squeezeThickness,
+ dy = squeezeThickness,
+ rotation = 90f,
+ corner = top
+ )
+
+ drawTransform(
+ dx = squeezeThickness,
+ dy = size.height - squeezeThickness,
+ rotation = 270f,
+ corner = bottom
+ )
+
+ drawTransform(
+ dx = size.width - squeezeThickness,
+ dy = size.height - squeezeThickness,
+ rotation = 180f,
+ corner = bottom
+ )
+ }
+}
+
+private fun DrawScope.drawTransform(
+ dx: Float,
+ dy: Float,
+ rotation: Float = 0f,
+ corner: VectorPainter,
+) {
+ withTransform(transformBlock = {
+ transform(matrix = Matrix().apply {
+ translate(dx, dy)
+ if (rotation != 0f) {
+ rotateZ(rotation)
+ }
+ })
+ }) {
+ with(corner) {
+ draw(size = intrinsicSize)
+ }
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/viewmodel/SqueezeEffectViewModel.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/viewmodel/SqueezeEffectViewModel.kt
new file mode 100644
index 000000000000..1cab327740ce
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/viewmodel/SqueezeEffectViewModel.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects.ui.viewmodel
+
+import androidx.compose.runtime.getValue
+import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.lifecycle.Hydrator
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+class SqueezeEffectViewModel
+@AssistedInject
+constructor(
+ keyEventInteractor: KeyEventInteractor
+) : ExclusiveActivatable() {
+ private val hydrator = Hydrator("SqueezeEffectViewModel.hydrator")
+
+ val isPowerButtonPressed: Boolean by hydrator.hydratedStateOf(
+ traceName = "isPowerButtonPressed",
+ initialValue = false,
+ source = keyEventInteractor.isPowerButtonDown
+ )
+
+ val isPowerButtonLongPressed: Boolean by hydrator.hydratedStateOf(
+ traceName = "isPowerButtonLongPressed",
+ initialValue = false,
+ source = keyEventInteractor.isPowerButtonLongPressed
+ )
+
+ override suspend fun onActivated(): Nothing {
+ hydrator.activate()
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(): SqueezeEffectViewModel
+ }
+} \ No newline at end of file
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 eecea9228ea3..2f8da2eae68a 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 @@ fun TutorialSelectionScreen(
),
) {
val isCompactWindow = hasCompactWindowSize()
- val padding = if (isCompactWindow) 24.dp else 60.dp
+ val padding = if (isCompactWindow) 24.dp else 48.dp
val configuration = LocalConfiguration.current
when (configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> {
@@ -121,7 +121,7 @@ fun TutorialSelectionScreen(
// because other composables have weight 1, Done button will be positioned first
DoneButton(
onDoneButtonClicked = onDoneButtonClicked,
- modifier = Modifier.padding(horizontal = padding),
+ modifier = Modifier.padding(start = padding, top = 0.dp, end = padding, bottom = 32.dp),
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt b/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt
index 720d5507e224..f6582a005035 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt
@@ -72,10 +72,10 @@ fun Slider(
valueRange: ClosedFloatingPointRange<Float>,
onValueChanged: (Float) -> Unit,
onValueChangeFinished: ((Float) -> Unit)?,
- stepDistance: Float,
isEnabled: Boolean,
accessibilityParams: AccessibilityParams,
modifier: Modifier = Modifier,
+ stepDistance: Float = 0f,
colors: SliderColors = SliderDefaults.colors(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
haptics: Haptics = Haptics.Disabled,
@@ -83,7 +83,7 @@ fun Slider(
isReverseDirection: Boolean = false,
track: (@Composable (SliderState) -> Unit)? = null,
) {
- require(stepDistance > 0) { "stepDistance must be positive" }
+ require(stepDistance >= 0) { "stepDistance must not be negative" }
val coroutineScope = rememberCoroutineScope()
val snappedValue = snapValue(value, valueRange, stepDistance)
val hapticsViewModel = haptics.createViewModel(snappedValue, valueRange, interactionSource)
@@ -192,16 +192,20 @@ private fun AccessibilityParams.createSemantics(
setProgress { targetValue ->
val targetDirection =
when {
- targetValue > value -> 1
- targetValue < value -> -1
- else -> 0
+ targetValue > value -> 1f
+ targetValue < value -> -1f
+ else -> 0f
+ }
+ val offset =
+ if (stepDistance > 0) {
+ // advance to the next step when stepDistance is > 0
+ targetDirection * stepDistance
+ } else {
+ // advance to the desired value otherwise
+ targetValue - value
}
- val newValue =
- (value + targetDirection * stepDistance).coerceIn(
- valueRange.start,
- valueRange.endInclusive,
- )
+ val newValue = (value + offset).coerceIn(valueRange.start, valueRange.endInclusive)
onValueChanged(newValue)
true
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index e8054c07eac8..8105ae0960ad 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -283,7 +283,6 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
() -> mSelectedUserInteractor,
mock(UserTracker.class),
mKosmos.getNotificationShadeWindowModel(),
- mSecureSettings,
mKosmos::getCommunalInteractor,
mKosmos.getShadeLayoutParams());
mFeatureFlags = new FakeFeatureFlags();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt
index b274f1c2c6df..d9c59d37b031 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt
@@ -88,11 +88,6 @@ class KeyguardQuickAffordanceInteractorSceneContainerTest : SysuiTestCase() {
companion object {
private val INTENT = Intent("some.intent.action")
- private val DRAWABLE =
- mock<Icon> {
- whenever(this.contentDescription)
- .thenReturn(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID))
- }
private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337
@Parameters(
@@ -337,7 +332,17 @@ class KeyguardQuickAffordanceInteractorSceneContainerTest : SysuiTestCase() {
homeControls.setState(
lockScreenState =
- KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = DRAWABLE)
+ KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+ icon =
+ mock<Icon> {
+ whenever(contentDescription)
+ .thenReturn(
+ ContentDescription.Resource(
+ res = CONTENT_DESCRIPTION_RESOURCE_ID
+ )
+ )
+ }
+ )
)
homeControls.onTriggeredResult =
if (startActivity) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt
index 175e8d4331a1..e048d462ef8d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt
@@ -33,8 +33,8 @@ import android.platform.test.annotations.EnableFlags
import android.platform.test.annotations.RequiresFlagsDisabled
import android.platform.test.annotations.RequiresFlagsEnabled
import android.platform.test.flag.junit.DeviceFlagsValueProvider
+import android.platform.test.flag.junit.FlagsParameterization
import android.testing.TestableLooper
-import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
import com.android.settingslib.bluetooth.LocalBluetoothManager
@@ -77,6 +77,8 @@ import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit
import org.mockito.kotlin.eq
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
private const val KEY = "TEST_KEY"
private const val KEY_OLD = "TEST_KEY_OLD"
@@ -89,12 +91,24 @@ private const val BROADCAST_APP_NAME = "BROADCAST_APP_NAME"
private const val NORMAL_APP_NAME = "NORMAL_APP_NAME"
@SmallTest
-@RunWith(AndroidJUnit4::class)
+@RunWith(ParameterizedAndroidJunit4::class)
@TestableLooper.RunWithLooper
-public class MediaDeviceManagerTest : SysuiTestCase() {
+public class MediaDeviceManagerTest(flags: FlagsParameterization) : SysuiTestCase() {
- private companion object {
+ companion object {
val OTHER_DEVICE_ICON_STUB = TestStubDrawable()
+
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return FlagsParameterization.progressionOf(
+ com.android.systemui.Flags.FLAG_MEDIA_CONTROLS_DEVICE_MANAGER_BACKGROUND_EXECUTION
+ )
+ }
+ }
+
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
}
@get:Rule val checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
@@ -187,6 +201,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
@Test
fun loadMediaData() {
manager.onMediaDataLoaded(KEY, null, mediaData)
+ fakeBgExecutor.runAllReady()
+ fakeFgExecutor.runAllReady()
verify(lmmFactory).create(PACKAGE)
}
@@ -195,6 +211,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
manager.onMediaDataLoaded(KEY, null, mediaData)
manager.onMediaDataRemoved(KEY, false)
fakeBgExecutor.runAllReady()
+ fakeFgExecutor.runAllReady()
verify(lmm).unregisterCallback(any())
verify(muteAwaitManager).stopListening()
}
@@ -406,6 +423,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
manager.onMediaDataLoaded(KEY, null, mediaData)
// WHEN the notification is removed
manager.onMediaDataRemoved(KEY, true)
+ fakeBgExecutor.runAllReady()
+ fakeFgExecutor.runAllReady()
// THEN the listener receives key removed event
verify(listener).onKeyRemoved(eq(KEY), eq(true))
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
index 205ccea657df..9ad2235965bd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
@@ -406,11 +406,6 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase {
}
@Override
- int getHeaderIconSize() {
- return 10;
- }
-
- @Override
CharSequence getHeaderText() {
return mHeaderTitle;
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java
index f0902e35b837..f1bf7c0bcf13 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java
@@ -50,6 +50,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
import com.android.settingslib.media.BluetoothMediaDevice;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
+import com.android.settingslib.utils.ThreadUtils;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.SysuiTestCaseExtKt;
import com.android.systemui.animation.DialogTransitionAnimator;
@@ -152,9 +153,9 @@ public class MediaOutputBroadcastDialogTest extends SysuiTestCase {
volumePanelGlobalStateInteractor,
mUserTracker);
mMediaSwitchingController.mLocalMediaManager = mLocalMediaManager;
- mMediaOutputBroadcastDialog =
- new MediaOutputBroadcastDialog(
- mContext, false, mBroadcastSender, mMediaSwitchingController);
+ mMediaOutputBroadcastDialog = new MediaOutputBroadcastDialog(mContext, false,
+ mBroadcastSender, mMediaSwitchingController, mContext.getMainExecutor(),
+ ThreadUtils.getBackgroundExecutor());
mMediaOutputBroadcastDialog.show();
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
index d3ecb3d8c944..420fd6e33abc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
@@ -53,6 +53,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
import com.android.settingslib.flags.Flags;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
+import com.android.settingslib.utils.ThreadUtils;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.SysuiTestCaseExtKt;
import com.android.systemui.animation.DialogTransitionAnimator;
@@ -455,6 +456,8 @@ public class MediaOutputDialogTest extends SysuiTestCase {
controller,
mDialogTransitionAnimator,
mUiEventLogger,
+ mContext.getMainExecutor(),
+ ThreadUtils.getBackgroundExecutor(),
true);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
index 88c2697fe2f2..5c26dac5eb30 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
@@ -1573,6 +1573,25 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
assertThat(items.get(1).isFirstDeviceInGroup()).isFalse();
}
+ @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
+ @Test
+ public void deviceListUpdateWithDifferentDevices_firstSelectedDeviceIsFirstDeviceInGroup() {
+ when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(true);
+ doReturn(mMediaDevices)
+ .when(mLocalMediaManager)
+ .getSelectedMediaDevice();
+ mMediaSwitchingController.start(mCb);
+ reset(mCb);
+ mMediaSwitchingController.getMediaItemList().clear();
+ mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
+ mMediaDevices.clear();
+ mMediaDevices.add(mMediaDevice2);
+ mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
+
+ List<MediaItem> items = mMediaSwitchingController.getMediaItemList();
+ assertThat(items.get(0).isFirstDeviceInGroup()).isTrue();
+ }
+
private int getNumberOfConnectDeviceButtons() {
int numberOfConnectDeviceButtons = 0;
for (MediaItem item : mMediaSwitchingController.getMediaItemList()) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt
index c20a801cd5e3..a8bfbd18d0c5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt
@@ -103,6 +103,8 @@ class InternetDetailsContentManagerTest : SysuiTestCase() {
whenever(internetWifiEntry.hasInternetAccess()).thenReturn(true)
whenever(wifiEntries.size).thenReturn(1)
whenever(internetDetailsContentController.getDialogTitleText()).thenReturn(TITLE)
+ whenever(internetDetailsContentController.getSubtitleText(ArgumentMatchers.anyBoolean()))
+ .thenReturn("")
whenever(internetDetailsContentController.getMobileNetworkTitle(ArgumentMatchers.anyInt()))
.thenReturn(MOBILE_NETWORK_TITLE)
whenever(
@@ -128,15 +130,13 @@ class InternetDetailsContentManagerTest : SysuiTestCase() {
internetDetailsContentController,
canConfigMobileData = true,
canConfigWifi = true,
- coroutineScope = scope,
- context = mContext,
uiEventLogger = mock<UiEventLogger>(),
handler = handler,
backgroundExecutor = bgExecutor,
keyguard = keyguard,
)
- internetDetailsContentManager.bind(contentView)
+ internetDetailsContentManager.bind(contentView, scope)
internetDetailsContentManager.adapter = internetAdapter
internetDetailsContentManager.connectedWifiEntry = internetWifiEntry
internetDetailsContentManager.wifiEntriesCount = wifiEntries.size
@@ -777,6 +777,26 @@ class InternetDetailsContentManagerTest : SysuiTestCase() {
}
}
+ @Test
+ fun updateTitleAndSubtitle() {
+ assertThat(internetDetailsContentManager.title).isEqualTo("Internet")
+ assertThat(internetDetailsContentManager.subTitle).isEqualTo("")
+
+ whenever(internetDetailsContentController.getDialogTitleText()).thenReturn("New title")
+ whenever(internetDetailsContentController.getSubtitleText(ArgumentMatchers.anyBoolean()))
+ .thenReturn("New subtitle")
+
+ internetDetailsContentManager.updateContent(true)
+ bgExecutor.runAllReady()
+
+ internetDetailsContentManager.internetContentData.observe(
+ internetDetailsContentManager.lifecycleOwner!!
+ ) {
+ assertThat(internetDetailsContentManager.title).isEqualTo("New title")
+ assertThat(internetDetailsContentManager.subTitle).isEqualTo("New subtitle")
+ }
+ }
+
companion object {
private const val TITLE = "Internet"
private const val MOBILE_NETWORK_TITLE = "Mobile Title"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt
index 2d3f538689b3..155059ea5ed9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt
@@ -36,6 +36,7 @@ import com.android.systemui.dump.DumpManager
import com.android.systemui.keyguard.KeyguardUnlockAnimationController
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager
+import com.android.systemui.log.assertLogsWtf
import com.android.systemui.model.sysUiState
import com.android.systemui.navigationbar.NavigationBarController
import com.android.systemui.navigationbar.NavigationModeController
@@ -221,7 +222,7 @@ class LauncherProxyServiceTest : SysuiTestCase() {
`when`(processWrapper.isSystemUser).thenReturn(false)
`when`(userManager.isVisibleBackgroundUsersSupported()).thenReturn(false)
val spyContext = spy(context)
- val ops = createLauncherProxyService(spyContext)
+ val ops = assertLogsWtf { createLauncherProxyService(spyContext) }.result
ops.startConnectionToCurrentUser()
verify(spyContext, times(0)).bindServiceAsUser(any(), any(), anyInt(), any())
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ringtone/RingtonePlayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/ringtone/RingtonePlayerTest.java
new file mode 100644
index 000000000000..c231be181977
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/ringtone/RingtonePlayerTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.ringtone;
+
+import static org.junit.Assert.assertThrows;
+
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.UserHandle;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RingtonePlayerTest extends SysuiTestCase {
+
+ private AudioManager mAudioManager;
+
+ private static final String TAG = "RingtonePlayerTest";
+
+ @Before
+ public void setup() throws Exception {
+ mAudioManager = getContext().getSystemService(AudioManager.class);
+ }
+
+ @Test
+ public void testRingtonePlayerUriUserCheck() {
+ android.media.IRingtonePlayer irp = mAudioManager.getRingtonePlayer();
+ final AudioAttributes aa = new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE).build();
+ // get a UserId that doesn't belong to mine
+ final int otherUserId = UserHandle.myUserId() == 0 ? 10 : 0;
+ // build a URI that I shouldn't have access to
+ final Uri uri = new Uri.Builder()
+ .scheme("content").authority(otherUserId + "@media")
+ .appendPath("external").appendPath("downloads")
+ .appendPath("bogusPathThatDoesNotMatter.mp3")
+ .build();
+ if (android.media.audio.Flags.ringtoneUserUriCheck()) {
+ assertThrows(SecurityException.class, () ->
+ irp.play(new Binder(), uri, aa, 1.0f /*volume*/, false /*looping*/)
+ );
+
+ assertThrows(SecurityException.class, () ->
+ irp.getTitle(uri));
+ }
+ }
+
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
index a64ff321cd4d..11aaeefe8214 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
@@ -134,10 +134,7 @@ class BrightnessDialogTest(val flags: FlagsParameterization) : SysuiTestCase() {
val frame = activityRule.activity.requireViewById<View>(viewId)
val lp = frame.layoutParams as ViewGroup.MarginLayoutParams
- val horizontalMargin =
- activityRule.activity.resources.getDimensionPixelSize(
- R.dimen.notification_side_paddings
- )
+ val horizontalMargin = 0
assertThat(lp.leftMargin).isEqualTo(horizontalMargin)
assertThat(lp.rightMargin).isEqualTo(horizontalMargin)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
index 81213caaa5f4..3570cf827808 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
@@ -77,7 +77,6 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifStabilityManager;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable;
import com.android.systemui.statusbar.notification.collection.notifcollection.CollectionReadyForBuildListener;
-import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
import com.android.systemui.util.time.FakeSystemClock;
@@ -1795,7 +1794,7 @@ public class ShadeListBuilderTest extends SysuiTestCase {
invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS);
// THEN an exception is NOT thrown directly, but a WTF IS logged.
- LogAssertKt.assertLogsWtfs(() -> {
+ LogAssertKt.assertRunnableLogsWtfs(() -> {
dispatchBuild();
runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
});
@@ -1818,7 +1817,7 @@ public class ShadeListBuilderTest extends SysuiTestCase {
addNotif(0, PACKAGE_2);
invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 1);
- LogAssertKt.assertLogsWtfs(() -> {
+ LogAssertKt.assertRunnableLogsWtfs(() -> {
Assert.assertThrows(IllegalStateException.class, () -> {
dispatchBuild();
runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
@@ -1844,13 +1843,13 @@ public class ShadeListBuilderTest extends SysuiTestCase {
addNotif(0, PACKAGE_2);
invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS);
- LogAssertKt.assertLogsWtfs(() -> {
+ LogAssertKt.assertRunnableLogsWtfs(() -> {
dispatchBuild();
runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
});
invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS);
- LogAssertKt.assertLogsWtfs(() -> {
+ LogAssertKt.assertRunnableLogsWtfs(() -> {
// Note: dispatchBuild itself triggers a non-reentrant pipeline run.
dispatchBuild();
runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
@@ -1874,7 +1873,7 @@ public class ShadeListBuilderTest extends SysuiTestCase {
addNotif(0, PACKAGE_1);
invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS);
- LogAssertKt.assertLogsWtfs(() -> {
+ LogAssertKt.assertRunnableLogsWtfs(() -> {
dispatchBuild();
runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
});
@@ -1897,7 +1896,7 @@ public class ShadeListBuilderTest extends SysuiTestCase {
addNotif(0, PACKAGE_1);
invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 1);
- LogAssertKt.assertLogsWtfs(() -> {
+ LogAssertKt.assertRunnableLogsWtfs(() -> {
Assert.assertThrows(IllegalStateException.class, () -> {
dispatchBuild();
runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
@@ -1922,7 +1921,7 @@ public class ShadeListBuilderTest extends SysuiTestCase {
addNotif(0, PACKAGE_2);
invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS);
- LogAssertKt.assertLogsWtfs(() -> {
+ LogAssertKt.assertRunnableLogsWtfs(() -> {
dispatchBuild();
runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
});
@@ -1945,7 +1944,7 @@ public class ShadeListBuilderTest extends SysuiTestCase {
addNotif(0, PACKAGE_2);
invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 1);
- LogAssertKt.assertLogsWtfs(() -> {
+ LogAssertKt.assertRunnableLogsWtfs(() -> {
Assert.assertThrows(IllegalStateException.class, () -> {
dispatchBuild();
runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
@@ -1970,7 +1969,7 @@ public class ShadeListBuilderTest extends SysuiTestCase {
addNotif(0, PACKAGE_2);
invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS);
- LogAssertKt.assertLogsWtfs(() -> {
+ LogAssertKt.assertRunnableLogsWtfs(() -> {
dispatchBuild();
runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
});
@@ -1993,7 +1992,7 @@ public class ShadeListBuilderTest extends SysuiTestCase {
addNotif(0, PACKAGE_2);
invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 1);
- LogAssertKt.assertLogsWtfs(() -> {
+ LogAssertKt.assertRunnableLogsWtfs(() -> {
Assert.assertThrows(IllegalStateException.class, () -> {
dispatchBuild();
runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
index cd2ea7d25699..4315c0f638ac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
@@ -73,6 +73,7 @@ import com.android.systemui.statusbar.notification.FeedbackIcon;
import com.android.systemui.statusbar.notification.SourceType;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.headsup.PinnedStatus;
+import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi;
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded;
import com.android.systemui.statusbar.notification.row.ExpandableView.OnHeightChangedListener;
import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
@@ -936,11 +937,13 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
}
@Test
- @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME)
+ @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME})
public void isExpanded_sensitivePromotedNotification_notExpanded() throws Exception {
// GIVEN
final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
- row.setPromotedOngoing(true);
+ NotificationEntry entry = mock(NotificationEntry.class);
+ when(entry.isPromotedOngoing()).thenReturn(true);
+ row.setEntry(entry);
row.setSensitive(/* sensitive= */true, /* hideSensitive= */false);
row.setHideSensitiveForIntrinsicHeight(/* hideSensitive= */true);
@@ -949,11 +952,13 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
}
@Test
- @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME)
+ @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME})
public void isExpanded_promotedNotificationNotOnKeyguard_expanded() throws Exception {
// GIVEN
final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
- row.setPromotedOngoing(true);
+ NotificationEntry entry = mock(NotificationEntry.class);
+ when(entry.isPromotedOngoing()).thenReturn(true);
+ row.setEntry(entry);
row.setOnKeyguard(false);
// THEN
@@ -961,11 +966,13 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
}
@Test
- @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME)
+ @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME})
public void isExpanded_promotedNotificationAllowOnKeyguard_expanded() throws Exception {
// GIVEN
final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
- row.setPromotedOngoing(true);
+ NotificationEntry entry = mock(NotificationEntry.class);
+ when(entry.isPromotedOngoing()).thenReturn(true);
+ row.setEntry(entry);
row.setOnKeyguard(true);
// THEN
@@ -973,12 +980,14 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
}
@Test
- @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME)
+ @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME})
public void isExpanded_promotedNotificationIgnoreLockscreenConstraints_expanded()
throws Exception {
// GIVEN
final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
- row.setPromotedOngoing(true);
+ NotificationEntry entry = mock(NotificationEntry.class);
+ when(entry.isPromotedOngoing()).thenReturn(true);
+ row.setEntry(entry);
row.setOnKeyguard(true);
row.setIgnoreLockscreenConstraints(true);
@@ -987,12 +996,14 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
}
@Test
- @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME)
+ @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME})
public void isExpanded_promotedNotificationSaveSpaceOnLockScreen_notExpanded()
throws Exception {
// GIVEN
final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
- row.setPromotedOngoing(true);
+ NotificationEntry entry = mock(NotificationEntry.class);
+ when(entry.isPromotedOngoing()).thenReturn(true);
+ row.setEntry(entry);
row.setOnKeyguard(true);
row.setSaveSpaceOnLockscreen(true);
@@ -1001,12 +1012,14 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
}
@Test
- @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME)
+ @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME})
public void isExpanded_promotedNotificationNotSaveSpaceOnLockScreen_expanded()
throws Exception {
// GIVEN
final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
- row.setPromotedOngoing(true);
+ NotificationEntry entry = mock(NotificationEntry.class);
+ when(entry.isPromotedOngoing()).thenReturn(true);
+ row.setEntry(entry);
row.setOnKeyguard(true);
row.setSaveSpaceOnLockscreen(false);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index 89b7bee7f286..a3616d20e11f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -128,6 +128,7 @@ import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.kosmos.KosmosJavaAdapter;
+import com.android.systemui.media.NotificationMediaManager;
import com.android.systemui.navigationbar.NavigationBarController;
import com.android.systemui.notetask.NoteTaskController;
import com.android.systemui.plugins.ActivityStarter;
@@ -164,7 +165,6 @@ import com.android.systemui.statusbar.KeyguardIndicationController;
import com.android.systemui.statusbar.LightRevealScrim;
import com.android.systemui.statusbar.LockscreenShadeTransitionController;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
-import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.NotificationPresenter;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.NotificationShadeDepthController;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
index ffe7750dadfa..4a0445d5543a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
@@ -269,14 +269,14 @@ public class RemoteInputViewTest extends SysuiTestCase {
when(viewRoot.getOnBackInvokedDispatcher()).thenReturn(backInvokedDispatcher);
view.setViewRootImpl(viewRoot);
- /* verify that predictive back callback registered when RemoteInputView becomes visible */
- view.onVisibilityAggregated(true);
+ /* verify that predictive back callback registered when RemoteInputView gains focus */
+ view.focus();
verify(backInvokedDispatcher).registerOnBackInvokedCallback(
eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY),
onBackInvokedCallbackCaptor.capture());
- /* verify that same callback unregistered when RemoteInputView becomes invisible */
- view.onVisibilityAggregated(false);
+ /* verify that same callback unregistered when RemoteInputView loses focus */
+ view.onDefocus(false, false, null);
verify(backInvokedDispatcher).unregisterOnBackInvokedCallback(
eq(onBackInvokedCallbackCaptor.getValue()));
}
@@ -299,13 +299,12 @@ public class RemoteInputViewTest extends SysuiTestCase {
view.onVisibilityAggregated(true);
view.setEditTextReferenceToSelf();
+ view.focus();
/* capture the callback during registration */
verify(backInvokedDispatcher).registerOnBackInvokedCallback(
eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY),
onBackInvokedCallbackCaptor.capture());
- view.focus();
-
/* invoke the captured callback */
onBackInvokedCallbackCaptor.getValue().onBackInvoked();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 341bd3a38999..8de931a7af40 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -33,6 +33,8 @@ import static com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_BAR;
import static com.google.common.truth.Truth.assertThat;
+import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -55,8 +57,6 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
-import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
-
import android.app.ActivityManager;
import android.app.IActivityManager;
import android.app.INotificationManager;
@@ -136,7 +136,6 @@ import com.android.systemui.statusbar.RankingBuilder;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.notification.NotifPipelineFlags;
import com.android.systemui.statusbar.notification.collection.GroupEntry;
-import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
@@ -166,7 +165,6 @@ import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvision
import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
import com.android.systemui.util.FakeEventLog;
import com.android.systemui.util.settings.FakeGlobalSettings;
-import com.android.systemui.util.settings.FakeSettings;
import com.android.systemui.util.settings.SystemSettings;
import com.android.systemui.util.time.SystemClock;
import com.android.wm.shell.Flags;
@@ -206,8 +204,6 @@ import com.android.wm.shell.taskview.TaskViewRepository;
import com.android.wm.shell.taskview.TaskViewTransitions;
import com.android.wm.shell.transition.Transitions;
-import kotlin.Lazy;
-
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -218,9 +214,6 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.stubbing.Answer;
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
-import platform.test.runner.parameterized.Parameters;
-
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -229,6 +222,10 @@ import java.util.List;
import java.util.Optional;
import java.util.concurrent.Executor;
+import kotlin.Lazy;
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
@SmallTest
@RunWith(ParameterizedAndroidJunit4.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@@ -451,7 +448,6 @@ public class BubblesTest extends SysuiTestCase {
() -> mSelectedUserInteractor,
mUserTracker,
mNotificationShadeWindowModel,
- new FakeSettings(),
mKosmos::getCommunalInteractor,
mKosmos.getShadeLayoutParams()
);
@@ -605,14 +601,19 @@ public class BubblesTest extends SysuiTestCase {
// Get a reference to KeyguardStateController.Callback
verify(mKeyguardStateController, atLeastOnce())
.addCallback(mKeyguardStateControllerCallbackCaptor.capture());
+
+ // Make sure mocks are set up for current user
+ switchUser(ActivityManager.getCurrentUser());
}
@After
public void tearDown() throws Exception {
- ArrayList<Bubble> bubbles = new ArrayList<>(mBubbleData.getBubbles());
- for (int i = 0; i < bubbles.size(); i++) {
- mBubbleController.removeBubble(bubbles.get(i).getKey(),
- Bubbles.DISMISS_NO_LONGER_BUBBLE);
+ if (mBubbleData != null) {
+ ArrayList<Bubble> bubbles = new ArrayList<>(mBubbleData.getBubbles());
+ for (int i = 0; i < bubbles.size(); i++) {
+ mBubbleController.removeBubble(bubbles.get(i).getKey(),
+ Bubbles.DISMISS_NO_LONGER_BUBBLE);
+ }
}
mTestableLooper.processAllMessages();
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
index 0ba7c8574d85..a3d01355ac4c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
@@ -47,6 +47,7 @@ import com.android.systemui.log.dagger.BiometricLog
import com.android.systemui.log.dagger.BroadcastDispatcherLog
import com.android.systemui.log.dagger.FaceAuthLog
import com.android.systemui.log.dagger.SceneFrameworkLog
+import com.android.systemui.media.NotificationMediaManager
import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager
import com.android.systemui.model.SysUiState
import com.android.systemui.plugins.ActivityStarter
@@ -57,7 +58,6 @@ import com.android.systemui.shared.system.ActivityManagerWrapper
import com.android.systemui.statusbar.LockscreenShadeTransitionController
import com.android.systemui.statusbar.NotificationListener
import com.android.systemui.statusbar.NotificationLockscreenUserManager
-import com.android.systemui.statusbar.NotificationMediaManager
import com.android.systemui.statusbar.NotificationShadeDepthController
import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt
index 6f570a86b19e..cd4b09c5267a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt
@@ -21,7 +21,6 @@ import com.android.systemui.biometrics.data.repository.fingerprintPropertyReposi
import com.android.systemui.dump.dumpManager
import com.android.systemui.keyevent.domain.interactor.keyEventInteractor
import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
-import com.android.systemui.keyguard.domain.interactor.keyguardBypassInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.util.time.systemClock
@@ -31,8 +30,6 @@ val Kosmos.deviceEntryHapticsInteractor by
DeviceEntryHapticsInteractor(
biometricSettingsRepository = biometricSettingsRepository,
deviceEntryBiometricAuthInteractor = deviceEntryBiometricAuthInteractor,
- deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor,
- keyguardBypassInteractor = keyguardBypassInteractor,
deviceEntryFingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor,
deviceEntrySourceInteractor = deviceEntrySourceInteractor,
fingerprintPropertyRepository = fingerprintPropertyRepository,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyevent/data/repository/FakeKeyEventRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyevent/data/repository/FakeKeyEventRepository.kt
index 97dab4987f6c..c9e25c31faa0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyevent/data/repository/FakeKeyEventRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyevent/data/repository/FakeKeyEventRepository.kt
@@ -27,9 +27,16 @@ class FakeKeyEventRepository @Inject constructor() : KeyEventRepository {
private val _isPowerButtonDown = MutableStateFlow(false)
override val isPowerButtonDown: Flow<Boolean> = _isPowerButtonDown.asStateFlow()
+ private val _isPowerButtonLongPressed = MutableStateFlow(false)
+ override val isPowerButtonLongPressed = _isPowerButtonLongPressed.asStateFlow()
+
fun setPowerButtonDown(isDown: Boolean) {
_isPowerButtonDown.value = isDown
}
+
+ fun setPowerButtonLongPressed(isLongPressed: Boolean) {
+ _isPowerButtonLongPressed.value = isLongPressed
+ }
}
@Module
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt
index 255a780a84be..113059222aa2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt
@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.domain.interactor
import android.content.applicationContext
+import android.os.powerManager
import android.view.accessibility.accessibilityManagerWrapper
import com.android.internal.logging.uiEventLogger
import com.android.systemui.broadcast.broadcastDispatcher
@@ -26,6 +27,8 @@ import com.android.systemui.keyguard.data.repository.keyguardRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.shade.pulsingGestureListener
+import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository
+import com.android.systemui.util.time.fakeSystemClock
val Kosmos.keyguardTouchHandlingInteractor by
Kosmos.Fixture {
@@ -40,5 +43,8 @@ val Kosmos.keyguardTouchHandlingInteractor by
accessibilityManager = accessibilityManagerWrapper,
pulsingGestureListener = pulsingGestureListener,
faceAuthInteractor = deviceEntryFaceAuthInteractor,
+ secureSettingsRepository = userAwareSecureSettingsRepository,
+ powerManager = powerManager,
+ systemClock = fakeSystemClock,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModelKosmos.kt
index 2797b4409ff0..bf456978d983 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModelKosmos.kt
@@ -48,5 +48,6 @@ val Kosmos.deviceEntryBackgroundViewModel by Fixture {
primaryBouncerToLockscreenTransitionViewModel,
lockscreenToDozingTransitionViewModel = lockscreenToDozingTransitionViewModel,
glanceableHubToAodTransitionViewModel = glanceableHubToAodTransitionViewModel,
+ glanceableHubToLockscreenTransitionViewModel = glanceableHubToLockscreenTransitionViewModel,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
index 27ca0f867855..a9aa8cd5a7f9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
@@ -84,6 +84,7 @@ val Kosmos.keyguardRootViewModel by Fixture {
occludedToAodTransitionViewModel = occludedToAodTransitionViewModel,
occludedToDozingTransitionViewModel = occludedToDozingTransitionViewModel,
occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel,
+ occludedToPrimaryBouncerTransitionViewModel = occludedToPrimaryBouncerTransitionViewModel,
offToLockscreenTransitionViewModel = offToLockscreenTransitionViewModel,
primaryBouncerToAodTransitionViewModel = primaryBouncerToAodTransitionViewModel,
primaryBouncerToGoneTransitionViewModel = primaryBouncerToGoneTransitionViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/util/KeyguardTransitionRepositorySpySubject.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/util/KeyguardTransitionRepositorySpySubject.kt
index 11f0c19ffa67..7093a941485a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/util/KeyguardTransitionRepositorySpySubject.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/util/KeyguardTransitionRepositorySpySubject.kt
@@ -115,14 +115,14 @@ private constructor(
fun assertThat(
repository: KeyguardTransitionRepository
): KeyguardTransitionRepositorySpySubject =
- assertAbout { failureMetadata, repository: KeyguardTransitionRepository ->
+ assertAbout { failureMetadata, repository: KeyguardTransitionRepository? ->
if (!Mockito.mockingDetails(repository).isSpy) {
fail(
"Cannot assert on a non-spy KeyguardTransitionRepository. " +
"Use Mockito.spy(keyguardTransitionRepository)."
)
}
- KeyguardTransitionRepositorySpySubject(failureMetadata, repository)
+ KeyguardTransitionRepositorySpySubject(failureMetadata, repository!!)
}
.that(repository)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt
index 5e67182d7353..b41ceff5f581 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt
@@ -13,89 +13,103 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package com.android.systemui.log
import android.util.Log
import android.util.Log.TerribleFailureHandler
-import junit.framework.Assert
+import com.google.common.truth.Truth.assertWithMessage
+import java.util.concurrent.Callable
-/** Asserts that the given block does not make a call to Log.wtf */
-fun assertDoesNotLogWtf(
+/** Asserts that [notLoggingBlock] does not make a call to [Log.wtf] */
+fun <T> assertDoesNotLogWtf(
message: String = "Expected Log.wtf not to be called",
- notLoggingBlock: () -> Unit,
-) {
+ notLoggingBlock: () -> T,
+): T {
var caught: TerribleFailureLog? = null
val newHandler = TerribleFailureHandler { tag, failure, system ->
caught = TerribleFailureLog(tag, failure, system)
}
val oldHandler = Log.setWtfHandler(newHandler)
- try {
- notLoggingBlock()
- } finally {
- Log.setWtfHandler(oldHandler)
- }
+ val result =
+ try {
+ notLoggingBlock()
+ } finally {
+ Log.setWtfHandler(oldHandler)
+ }
caught?.let { throw AssertionError("$message: $it", it.failure) }
+ return result
}
-fun assertDoesNotLogWtf(
- message: String = "Expected Log.wtf not to be called",
- notLoggingRunnable: Runnable,
-) = assertDoesNotLogWtf(message = message) { notLoggingRunnable.run() }
-
-/**
- * Assert that the given block makes a call to Log.wtf
- *
- * @return the details of the log
- */
-fun assertLogsWtf(
+/** Assert that [loggingBlock] makes a call to [Log.wtf] */
+@JvmOverloads
+fun <T> assertLogsWtf(
message: String = "Expected Log.wtf to be called",
allowMultiple: Boolean = false,
- loggingBlock: () -> Unit,
-): TerribleFailureLog {
- var caught: TerribleFailureLog? = null
- var count = 0
+ loggingBlock: () -> T,
+): WtfBlockResult<T> {
+ val caught = mutableListOf<TerribleFailureLog>()
val newHandler = TerribleFailureHandler { tag, failure, system ->
- if (caught == null) {
- caught = TerribleFailureLog(tag, failure, system)
- }
- count++
+ caught.add(TerribleFailureLog(tag, failure, system))
}
val oldHandler = Log.setWtfHandler(newHandler)
- try {
- loggingBlock()
- } finally {
- Log.setWtfHandler(oldHandler)
- }
- Assert.assertNotNull(message, caught)
- if (!allowMultiple && count != 1) {
- Assert.fail("Unexpectedly caught Log.Wtf $count times; expected only 1. First: $caught")
+ val result =
+ try {
+ loggingBlock()
+ } finally {
+ Log.setWtfHandler(oldHandler)
+ }
+ assertWithMessage(message).that(caught).isNotEmpty()
+ if (!allowMultiple) {
+ assertWithMessage("Unexpectedly caught Log.Wtf multiple times").that(caught).hasSize(1)
}
- return caught!!
+ return WtfBlockResult(caught, result)
}
+/** Assert that [loggingCallable] makes a call to [Log.wtf] */
@JvmOverloads
-fun assertLogsWtf(
+fun <T> assertLogsWtf(
message: String = "Expected Log.wtf to be called",
allowMultiple: Boolean = false,
- loggingRunnable: Runnable,
-): TerribleFailureLog =
- assertLogsWtf(message = message, allowMultiple = allowMultiple) { loggingRunnable.run() }
+ loggingCallable: Callable<T>,
+): WtfBlockResult<T> =
+ assertLogsWtf(message = message, allowMultiple = allowMultiple, loggingCallable::call)
-fun assertLogsWtfs(
+/** Assert that [loggingBlock] makes at least one call to [Log.wtf] */
+@JvmOverloads
+fun <T> assertLogsWtfs(
message: String = "Expected Log.wtf to be called once or more",
- loggingBlock: () -> Unit,
-): TerribleFailureLog = assertLogsWtf(message, allowMultiple = true, loggingBlock)
+ loggingBlock: () -> T,
+): WtfBlockResult<T> = assertLogsWtf(message, allowMultiple = true, loggingBlock)
+/** Assert that [loggingCallable] makes at least one call to [Log.wtf] */
@JvmOverloads
-fun assertLogsWtfs(
+fun <T> assertLogsWtfs(
message: String = "Expected Log.wtf to be called once or more",
- loggingRunnable: Runnable,
-): TerribleFailureLog = assertLogsWtfs(message) { loggingRunnable.run() }
+ loggingCallable: Callable<T>,
+): WtfBlockResult<T> = assertLogsWtf(message, allowMultiple = true, loggingCallable)
/** The data passed to [TerribleFailureHandler.onTerribleFailure] */
data class TerribleFailureLog(
val tag: String,
val failure: Log.TerribleFailure,
- val system: Boolean
+ val system: Boolean,
)
+
+/** The [Log.wtf] logs and return value of the block */
+data class WtfBlockResult<T>(val logs: List<TerribleFailureLog>, val result: T)
+
+/** Assert that [loggingRunnable] makes a call to [Log.wtf] */
+@JvmOverloads
+fun assertRunnableLogsWtf(
+ message: String = "Expected Log.wtf to be called",
+ allowMultiple: Boolean = false,
+ loggingRunnable: Runnable,
+): WtfBlockResult<Unit> =
+ assertLogsWtf(message = message, allowMultiple = allowMultiple) { loggingRunnable.run() }
+
+/** Assert that [loggingRunnable] makes at least one call to [Log.wtf] */
+@JvmOverloads
+fun assertRunnableLogsWtfs(
+ message: String = "Expected Log.wtf to be called once or more",
+ loggingRunnable: Runnable,
+): WtfBlockResult<Unit> = assertRunnableLogsWtf(message, allowMultiple = true, loggingRunnable)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogWtfHandlerRule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogWtfHandlerRule.kt
index e639326bd7a1..0e348c88f058 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogWtfHandlerRule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogWtfHandlerRule.kt
@@ -24,21 +24,23 @@ import org.junit.runners.model.Statement
class LogWtfHandlerRule : TestRule {
- private var started = false
- private var handler = ThrowAndFailAtEnd
+ private var failureLogExemptions = mutableListOf<FailureLogExemption>()
override fun apply(base: Statement, description: Description): Statement {
return object : Statement() {
override fun evaluate() {
- started = true
+ val handler = TerribleFailureTestHandler()
val originalWtfHandler = Log.setWtfHandler(handler)
var failure: Throwable? = null
try {
base.evaluate()
} catch (ex: Throwable) {
- failure = ex.runAndAddSuppressed { handler.onTestFailure(ex) }
+ failure = ex
} finally {
- failure = failure.runAndAddSuppressed { handler.onTestFinished() }
+ failure =
+ runAndAddSuppressed(failure) {
+ handler.onTestFinished(failureLogExemptions)
+ }
Log.setWtfHandler(originalWtfHandler)
}
if (failure != null) {
@@ -48,74 +50,52 @@ class LogWtfHandlerRule : TestRule {
}
}
- fun Throwable?.runAndAddSuppressed(block: () -> Unit): Throwable? {
+ /** Adds a log failure exemption. Exemptions are evaluated at the end of the test. */
+ fun addFailureLogExemption(exemption: FailureLogExemption) {
+ failureLogExemptions.add(exemption)
+ }
+
+ /** Clears and sets exemptions. Exemptions are evaluated at the end of the test. */
+ fun resetFailureLogExemptions(vararg exemptions: FailureLogExemption) {
+ failureLogExemptions = exemptions.toMutableList()
+ }
+
+ private fun runAndAddSuppressed(currentError: Throwable?, block: () -> Unit): Throwable? {
try {
block()
} catch (t: Throwable) {
- if (this == null) {
+ if (currentError == null) {
return t
}
- addSuppressed(t)
+ currentError.addSuppressed(t)
}
- return this
+ return currentError
}
- fun setWtfHandler(handler: TerribleFailureTestHandler) {
- check(!started) { "Should only be called before the test starts" }
- this.handler = handler
- }
-
- fun interface TerribleFailureTestHandler : TerribleFailureHandler {
- fun onTestFailure(failure: Throwable) {}
- fun onTestFinished() {}
- }
-
- companion object Handlers {
- val ThrowAndFailAtEnd
- get() =
- object : TerribleFailureTestHandler {
- val failures = mutableListOf<Log.TerribleFailure>()
-
- override fun onTerribleFailure(
- tag: String,
- what: Log.TerribleFailure,
- system: Boolean
- ) {
- failures.add(what)
- throw what
- }
+ private class TerribleFailureTestHandler : TerribleFailureHandler {
+ private val failureLogs = mutableListOf<FailureLog>()
- override fun onTestFailure(failure: Throwable) {
- super.onTestFailure(failure)
- }
+ override fun onTerribleFailure(tag: String, what: Log.TerribleFailure, system: Boolean) {
+ failureLogs.add(FailureLog(tag = tag, failure = what, system = system))
+ }
- override fun onTestFinished() {
- if (failures.isNotEmpty()) {
- throw AssertionError("Unexpected Log.wtf calls: $failures", failures[0])
- }
- }
+ fun onTestFinished(exemptions: List<FailureLogExemption>) {
+ val failures =
+ failureLogs.filter { failureLog ->
+ !exemptions.any { it.isFailureLogExempt(failureLog) }
}
+ if (failures.isNotEmpty()) {
+ throw AssertionError("Unexpected Log.wtf calls: $failures", failures[0].failure)
+ }
+ }
+ }
- val JustThrow = TerribleFailureTestHandler { _, what, _ -> throw what }
-
- val JustFailAtEnd
- get() =
- object : TerribleFailureTestHandler {
- val failures = mutableListOf<Log.TerribleFailure>()
-
- override fun onTerribleFailure(
- tag: String,
- what: Log.TerribleFailure,
- system: Boolean
- ) {
- failures.add(what)
- }
+ /** All the information from a call to [Log.wtf] that was handed to [TerribleFailureHandler] */
+ data class FailureLog(val tag: String, val failure: Log.TerribleFailure, val system: Boolean)
- override fun onTestFinished() {
- if (failures.isNotEmpty()) {
- throw AssertionError("Unexpected Log.wtf calls: $failures", failures[0])
- }
- }
- }
+ /** An interface for exempting a [FailureLog] from causing a test failure. */
+ fun interface FailureLogExemption {
+ /** Determines whether a log should be except from failing the test. */
+ fun isFailureLogExempt(log: FailureLog): Boolean
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt
index 4f8d5a14e390..9457de18b3b9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt
@@ -18,18 +18,14 @@ package com.android.systemui.qs
import com.android.systemui.plugins.qs.TileDetailsViewModel
-class FakeTileDetailsViewModel(var tileSpec: String?) : TileDetailsViewModel() {
+class FakeTileDetailsViewModel(var tileSpec: String?) : TileDetailsViewModel {
private var _clickOnSettingsButton = 0
override fun clickOnSettingsButton() {
_clickOnSettingsButton++
}
- override fun getTitle(): String {
- return tileSpec ?: " Fake title"
- }
+ override val title = tileSpec ?: " Fake title"
- override fun getSubTitle(): String {
- return tileSpec ?: "Fake sub title"
- }
+ override val subTitle = tileSpec ?: "Fake sub title"
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModelKosmos.kt
index 75ca311689ce..4aa4a2b1a73a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModelKosmos.kt
@@ -18,6 +18,7 @@ package com.android.systemui.qs.panels.ui.viewmodel.toolbar
import android.content.applicationContext
import com.android.systemui.classifier.domain.interactor.falsingInteractor
+import com.android.systemui.development.ui.viewmodel.buildNumberViewModelFactory
import com.android.systemui.globalactions.globalActionsDialogLite
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.qs.footerActionsInteractor
@@ -29,6 +30,7 @@ val Kosmos.toolbarViewModelFactory by
override fun create(): ToolbarViewModel {
return ToolbarViewModel(
editModeButtonViewModelFactory,
+ buildNumberViewModelFactory,
footerActionsInteractor,
{ globalActionsDialogLite },
falsingInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandlerSubject.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandlerSubject.kt
index e09f2806ce5c..c1e689c0165c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandlerSubject.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandlerSubject.kt
@@ -68,8 +68,8 @@ private constructor(
): QSTileIntentUserInputHandlerSubject =
Truth.assertAbout {
failureMetadata: FailureMetadata,
- subject: FakeQSTileIntentUserInputHandler ->
- QSTileIntentUserInputHandlerSubject(failureMetadata, subject)
+ subject: FakeQSTileIntentUserInputHandler? ->
+ QSTileIntentUserInputHandlerSubject(failureMetadata, subject!!)
}
.that(handler)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/QSTileStateSubject.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/QSTileStateSubject.kt
index aa29808bd9ee..657a95a3261e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/QSTileStateSubject.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/QSTileStateSubject.kt
@@ -63,7 +63,7 @@ private constructor(failureMetadata: FailureMetadata, subject: QSTileState?) :
companion object {
/** Returns a factory to be used with [Truth.assertAbout]. */
- fun states(): Factory<QSTileStateSubject, QSTileState?> {
+ fun states(): Factory<QSTileStateSubject, QSTileState> {
return Factory { failureMetadata: FailureMetadata, subject: QSTileState? ->
QSTileStateSubject(failureMetadata, subject)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/TileSubject.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/TileSubject.kt
index d2351dc8ae18..cc8b44b0dade 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/TileSubject.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/TileSubject.kt
@@ -59,7 +59,7 @@ class TileSubject private constructor(failureMetadata: FailureMetadata, subject:
companion object {
/** Returns a factory to be used with [Truth.assertAbout]. */
- fun tiles(): Factory<TileSubject, Tile?> {
+ fun tiles(): Factory<TileSubject, Tile> {
return Factory { failureMetadata: FailureMetadata, subject: Tile? ->
TileSubject(failureMetadata, subject)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/EntryAdapterFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/EntryAdapterFactoryKosmos.kt
index e99f61e7cd13..067e420b89c3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/EntryAdapterFactoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/EntryAdapterFactoryKosmos.kt
@@ -25,12 +25,13 @@ import com.android.systemui.statusbar.notification.people.peopleNotificationIden
import com.android.systemui.statusbar.notification.row.icon.notificationIconStyleProvider
val Kosmos.entryAdapterFactory by
-Kosmos.Fixture {
- EntryAdapterFactoryImpl(
- mockNotificationActivityStarter,
- metricsLogger,
- peopleNotificationIdentifier,
- notificationIconStyleProvider,
- visualStabilityCoordinator,
- )
-} \ No newline at end of file
+ Kosmos.Fixture {
+ EntryAdapterFactoryImpl(
+ mockNotificationActivityStarter,
+ metricsLogger,
+ peopleNotificationIdentifier,
+ notificationIconStyleProvider,
+ visualStabilityCoordinator,
+ mockNotificationActionClickManager,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt
index ff4fbf9f0e94..6a674ca29ca4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt
@@ -37,6 +37,7 @@ import com.android.systemui.flags.FakeFeatureFlagsClassic
import com.android.systemui.flags.FeatureFlagsClassic
import com.android.systemui.flags.Flags
import com.android.systemui.log.logcatLogBuffer
+import com.android.systemui.media.NotificationMediaManager
import com.android.systemui.media.controls.util.MediaFeatureFlag
import com.android.systemui.media.dialog.MediaOutputDialogManager
import com.android.systemui.plugins.ActivityStarter
@@ -45,7 +46,6 @@ import com.android.systemui.settings.UserTracker
import com.android.systemui.shared.system.ActivityManagerWrapper
import com.android.systemui.shared.system.DevicePolicyManagerWrapper
import com.android.systemui.shared.system.PackageManagerWrapper
-import com.android.systemui.statusbar.NotificationMediaManager
import com.android.systemui.statusbar.NotificationRemoteInputManager
import com.android.systemui.statusbar.NotificationShadeWindowController
import com.android.systemui.statusbar.RankingBuilder
@@ -375,6 +375,7 @@ class ExpandableNotificationRowBuilder(
Mockito.mock(PeopleNotificationIdentifier::class.java),
Mockito.mock(NotificationIconStyleProvider::class.java),
Mockito.mock(VisualStabilityCoordinator::class.java),
+ Mockito.mock(NotificationActionClickManager::class.java),
)
.create(entry)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManagerKosmos.kt
new file mode 100644
index 000000000000..8e62ae8825f3
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManagerKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row
+
+import com.android.systemui.kosmos.Kosmos
+import org.mockito.kotlin.mock
+
+var Kosmos.mockNotificationActionClickManager: NotificationActionClickManager by
+ Kosmos.Fixture { mock<NotificationActionClickManager>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt
index 8ff7c7d01fb3..3e96fd7c729f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt
@@ -17,6 +17,8 @@
package com.android.systemui.statusbar.phone.ongoingcall.shared.model
import android.app.PendingIntent
+import com.android.systemui.activity.data.repository.activityManagerRepository
+import com.android.systemui.activity.data.repository.fake
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
@@ -38,6 +40,7 @@ fun inCallModel(
notificationKey: String = "test",
appName: String = "",
promotedContent: PromotedNotificationContentModel? = null,
+ isAppVisible: Boolean = false,
) =
OngoingCallModel.InCall(
startTimeMs,
@@ -46,6 +49,7 @@ fun inCallModel(
notificationKey,
appName,
promotedContent,
+ isAppVisible,
)
object OngoingCallTestHelper {
@@ -77,8 +81,10 @@ object OngoingCallTestHelper {
contentIntent: PendingIntent? = null,
uid: Int = DEFAULT_UID,
appName: String = "Fake name",
+ isAppVisible: Boolean = false,
) {
if (StatusBarChipsModernization.isEnabled) {
+ activityManagerRepository.fake.startingIsAppVisibleValue = isAppVisible
activeNotificationListRepository.addNotif(
activeNotificationModel(
key = key,
@@ -100,6 +106,7 @@ object OngoingCallTestHelper {
notificationKey = key,
appName = appName,
promotedContent = promotedContent,
+ isAppVisible = isAppVisible,
)
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileDomainInteractorKairosKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileDomainInteractorKairosKosmos.kt
new file mode 100644
index 000000000000..d9a327b99c7f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileDomainInteractorKairosKosmos.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.statusbar.pipeline.mobile.domain.interactor
+
+import android.content.applicationContext
+import com.android.systemui.flags.featureFlagsClassic
+import com.android.systemui.kairos.ActivatedKairosFixture
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.log.table.logcatTableLogBuffer
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepositoryKairos
+import com.android.systemui.statusbar.pipeline.shared.data.repository.connectivityRepository
+import com.android.systemui.statusbar.policy.data.repository.userSetupRepository
+import com.android.systemui.util.carrierConfigTracker
+
+@ExperimentalKairosApi
+val Kosmos.mobileIconsInteractorKairos by ActivatedKairosFixture {
+ MobileIconsInteractorKairosImpl(
+ mobileConnectionsRepositoryKairos,
+ carrierConfigTracker,
+ logcatTableLogBuffer(this),
+ connectivityRepository,
+ userSetupRepository,
+ applicationContext,
+ featureFlagsClassic,
+ )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosKosmos.kt
new file mode 100644
index 000000000000..3ee33802e9d5
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.stackedMobileIconViewModelKairos by
+ Kosmos.Fixture { StackedMobileIconViewModelKairos(mobileIconsViewModel) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/FakeSqueezeEffectRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/FakeSqueezeEffectRepository.kt
new file mode 100644
index 000000000000..2d2a81586515
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/FakeSqueezeEffectRepository.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects.data.repository
+
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeSqueezeEffectRepository : SqueezeEffectRepository {
+ override val isSqueezeEffectEnabled = MutableStateFlow(false)
+} \ No newline at end of file
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryKosmos.kt
new file mode 100644
index 000000000000..aa8bb6b1e104
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryKosmos.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.fakeSqueezeEffectRepository by Kosmos.Fixture { FakeSqueezeEffectRepository() } \ No newline at end of file
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/truth/correspondence/FakeUiEvent.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/truth/correspondence/FakeUiEvent.kt
index 48cd345b1f68..6c10526a17fe 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/truth/correspondence/FakeUiEvent.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/truth/correspondence/FakeUiEvent.kt
@@ -24,7 +24,7 @@ import com.google.common.truth.Correspondence
object FakeUiEvent {
val EVENT_ID =
Correspondence.transforming<FakeUiEvent, Int>(
- { it?.eventId },
+ { it.eventId },
"has a eventId of",
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/truth/correspondence/LogMaker.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/truth/correspondence/LogMaker.kt
index 3f0a95248d9c..9664bf3d6cc9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/truth/correspondence/LogMaker.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/truth/correspondence/LogMaker.kt
@@ -23,7 +23,7 @@ import com.google.common.truth.Correspondence
object LogMaker {
val CATEGORY =
Correspondence.transforming<LogMaker, Int>(
- { it?.category },
+ { it.category },
"has a category of",
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/CarrierConfigTrackerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/CarrierConfigTrackerKosmos.kt
new file mode 100644
index 000000000000..58473693954c
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/CarrierConfigTrackerKosmos.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.util
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mockFixture
+
+var Kosmos.carrierConfigTracker: CarrierConfigTracker by mockFixture()
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index ccbc46fdb03b..5424ac3bd897 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -16,7 +16,7 @@ filegroup {
srcs: [
"texts/ravenwood-common-policies.txt",
],
- visibility: ["//visibility:private"],
+ visibility: [":__subpackages__"],
}
filegroup {
@@ -44,6 +44,22 @@ filegroup {
}
filegroup {
+ name: "ravenwood-standard-annotations",
+ srcs: [
+ "texts/ravenwood-standard-annotations.txt",
+ ],
+ visibility: [":__subpackages__"],
+}
+
+filegroup {
+ name: "ravenizer-standard-options",
+ srcs: [
+ "texts/ravenizer-standard-options.txt",
+ ],
+ visibility: [":__subpackages__"],
+}
+
+filegroup {
name: "ravenwood-annotation-allowed-classes",
srcs: [
"texts/ravenwood-annotation-allowed-classes.txt",
diff --git a/ravenwood/Framework.bp b/ravenwood/Framework.bp
index e36677189e02..f5b075b17fd4 100644
--- a/ravenwood/Framework.bp
+++ b/ravenwood/Framework.bp
@@ -33,6 +33,7 @@ genrule_defaults {
":ravenwood-common-policies",
":ravenwood-framework-policies",
":ravenwood-standard-options",
+ ":ravenwood-standard-annotations",
":ravenwood-annotation-allowed-classes",
],
out: [
@@ -44,6 +45,7 @@ genrule_defaults {
framework_minus_apex_cmd = "$(location hoststubgen) " +
"@$(location :ravenwood-standard-options) " +
+ "@$(location :ravenwood-standard-annotations) " +
"--debug-log $(location hoststubgen_framework-minus-apex.log) " +
"--out-jar $(location ravenwood.jar) " +
"--in-jar $(location :framework-minus-apex-for-host) " +
@@ -178,6 +180,7 @@ java_genrule {
tools: ["hoststubgen"],
cmd: "$(location hoststubgen) " +
"@$(location :ravenwood-standard-options) " +
+ "@$(location :ravenwood-standard-annotations) " +
"--debug-log $(location hoststubgen_services.core.log) " +
"--stats-file $(location hoststubgen_services.core_stats.csv) " +
@@ -196,6 +199,7 @@ java_genrule {
":ravenwood-common-policies",
":ravenwood-services-policies",
":ravenwood-standard-options",
+ ":ravenwood-standard-annotations",
":ravenwood-annotation-allowed-classes",
],
out: [
@@ -247,6 +251,7 @@ java_genrule {
tools: ["hoststubgen"],
cmd: "$(location hoststubgen) " +
"@$(location :ravenwood-standard-options) " +
+ "@$(location :ravenwood-standard-annotations) " +
"--debug-log $(location hoststubgen_core-icu4j-for-host.log) " +
"--stats-file $(location hoststubgen_core-icu4j-for-host_stats.csv) " +
@@ -265,6 +270,7 @@ java_genrule {
":ravenwood-common-policies",
":icu-ravenwood-policies",
":ravenwood-standard-options",
+ ":ravenwood-standard-annotations",
],
out: [
"ravenwood.jar",
@@ -301,6 +307,7 @@ java_genrule {
tools: ["hoststubgen"],
cmd: "$(location hoststubgen) " +
"@$(location :ravenwood-standard-options) " +
+ "@$(location :ravenwood-standard-annotations) " +
"--debug-log $(location framework-configinfrastructure.log) " +
"--stats-file $(location framework-configinfrastructure_stats.csv) " +
@@ -319,6 +326,7 @@ java_genrule {
":ravenwood-common-policies",
":framework-configinfrastructure-ravenwood-policies",
":ravenwood-standard-options",
+ ":ravenwood-standard-annotations",
],
out: [
"ravenwood.jar",
@@ -355,6 +363,7 @@ java_genrule {
tools: ["hoststubgen"],
cmd: "$(location hoststubgen) " +
"@$(location :ravenwood-standard-options) " +
+ "@$(location :ravenwood-standard-annotations) " +
"--debug-log $(location framework-statsd.log) " +
"--stats-file $(location framework-statsd_stats.csv) " +
@@ -373,6 +382,7 @@ java_genrule {
":ravenwood-common-policies",
":framework-statsd-ravenwood-policies",
":ravenwood-standard-options",
+ ":ravenwood-standard-annotations",
],
out: [
"ravenwood.jar",
@@ -409,6 +419,7 @@ java_genrule {
tools: ["hoststubgen"],
cmd: "$(location hoststubgen) " +
"@$(location :ravenwood-standard-options) " +
+ "@$(location :ravenwood-standard-annotations) " +
"--debug-log $(location framework-graphics.log) " +
"--stats-file $(location framework-graphics_stats.csv) " +
@@ -427,6 +438,7 @@ java_genrule {
":ravenwood-common-policies",
":framework-graphics-ravenwood-policies",
":ravenwood-standard-options",
+ ":ravenwood-standard-annotations",
],
out: [
"ravenwood.jar",
diff --git a/ravenwood/TEST_MAPPING b/ravenwood/TEST_MAPPING
index df63cb9dfc50..1148539187ac 100644
--- a/ravenwood/TEST_MAPPING
+++ b/ravenwood/TEST_MAPPING
@@ -150,6 +150,10 @@
"host": true
},
{
+ "name": "RavenwoodCoreTest-light",
+ "host": true
+ },
+ {
"name": "RavenwoodMinimumTest",
"host": true
},
@@ -168,6 +172,10 @@
{
"name": "RavenwoodServicesTest",
"host": true
+ },
+ {
+ "name": "UinputTestsRavenwood",
+ "host": true
}
// AUTO-GENERATED-END
],
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java
index 3e2c4051b792..f25ae6a34242 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java
@@ -128,28 +128,6 @@ public class RavenwoodUtils {
runOnHandlerSync(getMainHandler(), r);
}
- public static class MockitoHelper {
- private MockitoHelper() {
- }
-
- /**
- * Allow verifyZeroInteractions to work on ravenwood. It was replaced with a different
- * method on. (Maybe we should do it in Ravenizer.)
- */
- public static void verifyZeroInteractions(Object... mocks) {
- if (RavenwoodRule.isOnRavenwood()) {
- // Mockito 4 or later
- reflectMethod("org.mockito.Mockito", "verifyNoInteractions", Object[].class)
- .callStatic(new Object[]{mocks});
- } else {
- // Mockito 2
- reflectMethod("org.mockito.Mockito", "verifyZeroInteractions", Object[].class)
- .callStatic(new Object[]{mocks});
- }
- }
- }
-
-
/**
* Wrap the given {@link Supplier} to become memoized.
*
diff --git a/ravenwood/scripts/extract-last-soong-commands.py b/ravenwood/scripts/extract-last-soong-commands.py
index 0629b77029e0..b8d6f2042389 100755
--- a/ravenwood/scripts/extract-last-soong-commands.py
+++ b/ravenwood/scripts/extract-last-soong-commands.py
@@ -55,7 +55,7 @@ def main(args):
if s.startswith("verbose"):
continue
- if re.match('^\[.*bootstrap blueprint', s):
+ if re.match('^\\[.*bootstrap blueprint', s):
continue
s = s.rstrip()
diff --git a/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodJdkPatchTest.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodJdkPatchTest.java
new file mode 100644
index 000000000000..cdfd4a877f43
--- /dev/null
+++ b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodJdkPatchTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ravenwoodtest.bivalenttest.ravenizer;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+
+import org.junit.Test;
+
+import java.io.FileDescriptor;
+import java.util.LinkedHashMap;
+import java.util.regex.Pattern;
+
+public class RavenwoodJdkPatchTest {
+
+ @Test
+ public void testUnicodeRegex() {
+ var pattern = Pattern.compile("\\w+");
+ assertTrue(pattern.matcher("über").matches());
+ }
+
+ @Test
+ public void testLinkedHashMapEldest() {
+ var map = new LinkedHashMap<String, String>();
+ map.put("a", "b");
+ map.put("x", "y");
+ assertEquals(map.entrySet().iterator().next(), map.eldest());
+ }
+
+ @Test
+ public void testFileDescriptorGetSetInt() throws ErrnoException {
+ FileDescriptor fd = Os.open("/dev/zero", OsConstants.O_RDONLY, 0);
+ try {
+ int fdRaw = fd.getInt$();
+ assertNotEquals(-1, fdRaw);
+ fd.setInt$(-1);
+ assertEquals(-1, fd.getInt$());
+ fd.setInt$(fdRaw);
+ Os.close(fd);
+ assertEquals(-1, fd.getInt$());
+ } finally {
+ Os.close(fd);
+ }
+ }
+}
diff --git a/ravenwood/texts/ravenizer-standard-options.txt b/ravenwood/texts/ravenizer-standard-options.txt
new file mode 100644
index 000000000000..cef736f87e72
--- /dev/null
+++ b/ravenwood/texts/ravenizer-standard-options.txt
@@ -0,0 +1,13 @@
+# File containing standard options to Ravenizer for Ravenwood
+
+# Keep all classes / methods / fields in tests and its target
+--default-keep
+
+--delete-finals
+
+# Include standard annotations
+@jar:texts/ravenwood-standard-annotations.txt
+
+# Apply common policies
+--policy-override-file
+ jar:texts/ravenwood-common-policies.txt
diff --git a/ravenwood/texts/ravenwood-standard-annotations.txt b/ravenwood/texts/ravenwood-standard-annotations.txt
new file mode 100644
index 000000000000..75ec5cadb6fc
--- /dev/null
+++ b/ravenwood/texts/ravenwood-standard-annotations.txt
@@ -0,0 +1,37 @@
+# Standard annotations.
+# Note, each line is a single argument, so we need newlines after each `--xxx-annotation`.
+--keep-annotation
+ android.ravenwood.annotation.RavenwoodKeep
+
+--keep-annotation
+ android.ravenwood.annotation.RavenwoodKeepPartialClass
+
+--keep-class-annotation
+ android.ravenwood.annotation.RavenwoodKeepWholeClass
+
+--throw-annotation
+ android.ravenwood.annotation.RavenwoodThrow
+
+--remove-annotation
+ android.ravenwood.annotation.RavenwoodRemove
+
+--ignore-annotation
+ android.ravenwood.annotation.RavenwoodIgnore
+
+--partially-allowed-annotation
+ android.ravenwood.annotation.RavenwoodPartiallyAllowlisted
+
+--substitute-annotation
+ android.ravenwood.annotation.RavenwoodReplace
+
+--redirect-annotation
+ android.ravenwood.annotation.RavenwoodRedirect
+
+--redirection-class-annotation
+ android.ravenwood.annotation.RavenwoodRedirectionClass
+
+--class-load-hook-annotation
+ android.ravenwood.annotation.RavenwoodClassLoadHook
+
+--keep-static-initializer-annotation
+ android.ravenwood.annotation.RavenwoodKeepStaticInitializer
diff --git a/ravenwood/texts/ravenwood-standard-options.txt b/ravenwood/texts/ravenwood-standard-options.txt
index 233657557747..0a650254a71f 100644
--- a/ravenwood/texts/ravenwood-standard-options.txt
+++ b/ravenwood/texts/ravenwood-standard-options.txt
@@ -15,41 +15,3 @@
#--default-class-load-hook
# com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
-
-# Standard annotations.
-# Note, each line is a single argument, so we need newlines after each `--xxx-annotation`.
---keep-annotation
- android.ravenwood.annotation.RavenwoodKeep
-
---keep-annotation
- android.ravenwood.annotation.RavenwoodKeepPartialClass
-
---keep-class-annotation
- android.ravenwood.annotation.RavenwoodKeepWholeClass
-
---throw-annotation
- android.ravenwood.annotation.RavenwoodThrow
-
---remove-annotation
- android.ravenwood.annotation.RavenwoodRemove
-
---ignore-annotation
- android.ravenwood.annotation.RavenwoodIgnore
-
---partially-allowed-annotation
- android.ravenwood.annotation.RavenwoodPartiallyAllowlisted
-
---substitute-annotation
- android.ravenwood.annotation.RavenwoodReplace
-
---redirect-annotation
- android.ravenwood.annotation.RavenwoodRedirect
-
---redirection-class-annotation
- android.ravenwood.annotation.RavenwoodRedirectionClass
-
---class-load-hook-annotation
- android.ravenwood.annotation.RavenwoodClassLoadHook
-
---keep-static-initializer-annotation
- android.ravenwood.annotation.RavenwoodKeepStaticInitializer
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/Exceptions.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/Exceptions.kt
index f59e143c1e4e..ae0a00855650 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/Exceptions.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/Exceptions.kt
@@ -15,8 +15,6 @@
*/
package com.android.hoststubgen
-import java.io.File
-
/**
* We will not print the stack trace for exceptions implementing it.
*/
@@ -64,9 +62,6 @@ class DuplicateAnnotationException(annotationName: String?) :
class InputFileNotFoundException(filename: String) :
ArgumentsException("File '$filename' not found")
-fun String.ensureFileExists(): String {
- if (!File(this).exists()) {
- throw InputFileNotFoundException(this)
- }
- return this
-}
+/** Thrown when a JAR resource does not exist. */
+class JarResourceNotFoundException(path: String) :
+ ArgumentsException("JAR resource '$path' not found")
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessor.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessor.kt
index 4fe21eac6972..98f96a89d889 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessor.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessor.kt
@@ -16,6 +16,7 @@
package com.android.hoststubgen
import com.android.hoststubgen.asm.ClassNodes
+import com.android.hoststubgen.asm.findAnyAnnotation
import com.android.hoststubgen.filters.AnnotationBasedFilter
import com.android.hoststubgen.filters.ClassWidePolicyPropagatingFilter
import com.android.hoststubgen.filters.ConstantFilter
@@ -26,21 +27,25 @@ import com.android.hoststubgen.filters.KeepNativeFilter
import com.android.hoststubgen.filters.OutputFilter
import com.android.hoststubgen.filters.SanitizationFilter
import com.android.hoststubgen.filters.TextFileFilterPolicyBuilder
+import com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
import com.android.hoststubgen.utils.ClassPredicate
import com.android.hoststubgen.visitors.BaseAdapter
+import com.android.hoststubgen.visitors.ImplGeneratingAdapter
import com.android.hoststubgen.visitors.PackageRedirectRemapper
+import java.io.PrintWriter
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.commons.ClassRemapper
import org.objectweb.asm.util.CheckClassAdapter
+import org.objectweb.asm.util.TraceClassVisitor
/**
* This class implements bytecode transformation of HostStubGen.
*/
class HostStubGenClassProcessor(
private val options: HostStubGenClassProcessorOptions,
- private val allClasses: ClassNodes,
+ val allClasses: ClassNodes,
private val errors: HostStubGenErrors = HostStubGenErrors(),
private val stats: HostStubGenStats? = null,
) {
@@ -48,6 +53,7 @@ class HostStubGenClassProcessor(
val remapper = FilterRemapper(filter)
private val packageRedirector = PackageRedirectRemapper(options.packageRedirects)
+ private val processedAnnotation = setOf(HostStubGenProcessedAsKeep.CLASS_DESCRIPTOR)
/**
* Build the filter, which decides what classes/methods/fields should be put in stub or impl
@@ -130,15 +136,10 @@ class HostStubGenClassProcessor(
return filter
}
- fun processClassBytecode(bytecode: ByteArray): ByteArray {
- val cr = ClassReader(bytecode)
-
- // COMPUTE_FRAMES wouldn't be happy if code uses
- val flags = ClassWriter.COMPUTE_MAXS // or ClassWriter.COMPUTE_FRAMES
- val cw = ClassWriter(flags)
+ private fun buildVisitor(base: ClassVisitor, className: String): ClassVisitor {
+ // Connect to the base visitor
+ var outVisitor: ClassVisitor = base
- // Connect to the class writer
- var outVisitor: ClassVisitor = cw
if (options.enableClassChecker.get) {
outVisitor = CheckClassAdapter(outVisitor)
}
@@ -149,15 +150,59 @@ class HostStubGenClassProcessor(
val visitorOptions = BaseAdapter.Options(
errors = errors,
stats = stats,
- enablePreTrace = options.enablePreTrace.get,
- enablePostTrace = options.enablePostTrace.get,
deleteClassFinals = options.deleteFinals.get,
deleteMethodFinals = options.deleteFinals.get,
)
- outVisitor = BaseAdapter.getVisitor(
- cr.className, allClasses, outVisitor, filter,
- packageRedirector, visitorOptions
- )
+
+ val verbosePrinter = PrintWriter(log.getWriter(LogLevel.Verbose))
+
+ // Inject TraceClassVisitor for debugging.
+ if (options.enablePostTrace.get) {
+ outVisitor = TraceClassVisitor(outVisitor, verbosePrinter)
+ }
+
+ // Handle --package-redirect
+ if (!packageRedirector.isEmpty) {
+ // Don't apply the remapper on redirect-from classes.
+ // Otherwise, if the target jar actually contains the "from" classes (which
+ // may or may not be the case) they'd be renamed.
+ // But we update all references in other places, so, a method call to a "from" class
+ // would be replaced with the "to" class. All type references (e.g. variable types)
+ // will be updated too.
+ if (!packageRedirector.isTarget(className)) {
+ outVisitor = ClassRemapper(outVisitor, packageRedirector)
+ } else {
+ log.v(
+ "Class $className is a redirect-from class, not applying" +
+ " --package-redirect"
+ )
+ }
+ }
+
+ outVisitor = ImplGeneratingAdapter(allClasses, outVisitor, filter, visitorOptions)
+
+ // Inject TraceClassVisitor for debugging.
+ if (options.enablePreTrace.get) {
+ outVisitor = TraceClassVisitor(outVisitor, verbosePrinter)
+ }
+
+ return outVisitor
+ }
+
+ fun processClassBytecode(bytecode: ByteArray): ByteArray {
+ val cr = ClassReader(bytecode)
+
+ // If the class was already processed previously, skip
+ val clz = allClasses.getClass(cr.className)
+ if (clz.findAnyAnnotation(processedAnnotation) != null) {
+ return bytecode
+ }
+
+ // COMPUTE_FRAMES wouldn't be happy if code uses
+ val flags = ClassWriter.COMPUTE_MAXS // or ClassWriter.COMPUTE_FRAMES
+ val cw = ClassWriter(flags)
+
+ val outVisitor = buildVisitor(cw, cr.className)
cr.accept(outVisitor, ClassReader.EXPAND_FRAMES)
return cw.toByteArray()
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessorOptions.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessorOptions.kt
index c7c45e65a8b6..e7166f11f597 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessorOptions.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessorOptions.kt
@@ -18,6 +18,7 @@ package com.android.hoststubgen
import com.android.hoststubgen.filters.FilterPolicy
import com.android.hoststubgen.utils.ArgIterator
import com.android.hoststubgen.utils.BaseOptions
+import com.android.hoststubgen.utils.FileOrResource
import com.android.hoststubgen.utils.SetOnce
private fun parsePackageRedirect(fromColonTo: String): Pair<String, String> {
@@ -53,7 +54,7 @@ open class HostStubGenClassProcessorOptions(
var defaultClassLoadHook: SetOnce<String?> = SetOnce(null),
var defaultMethodCallHook: SetOnce<String?> = SetOnce(null),
- var policyOverrideFiles: MutableList<String> = mutableListOf(),
+ var policyOverrideFiles: MutableList<FileOrResource> = mutableListOf(),
var defaultPolicy: SetOnce<FilterPolicy> = SetOnce(FilterPolicy.Remove),
@@ -73,15 +74,14 @@ open class HostStubGenClassProcessorOptions(
return name
}
- override fun parseOption(option: String, ai: ArgIterator): Boolean {
+ override fun parseOption(option: String, args: ArgIterator): Boolean {
// Define some shorthands...
- fun nextArg(): String = ai.nextArgRequired(option)
+ fun nextArg(): String = args.nextArgRequired(option)
fun MutableSet<String>.addUniqueAnnotationArg(): String =
nextArg().also { this += ensureUniqueAnnotation(it) }
when (option) {
- "--policy-override-file" ->
- policyOverrideFiles.add(nextArg().ensureFileExists())
+ "--policy-override-file" -> policyOverrideFiles.add(FileOrResource(nextArg()))
"--default-remove" -> defaultPolicy.set(FilterPolicy.Remove)
"--default-throw" -> defaultPolicy.set(FilterPolicy.Throw)
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt
index b41ce0f65017..112ef01e20cb 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt
@@ -217,7 +217,7 @@ private val numericalInnerClassName = """.*\$\d+$""".toRegex()
fun isAnonymousInnerClass(cn: ClassNode): Boolean {
// TODO: Is there a better way?
- return cn.name.matches(numericalInnerClassName)
+ return cn.outerClass != null && cn.name.matches(numericalInnerClassName)
}
/**
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/DelegatingFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/DelegatingFilter.kt
index b8b0d8a31268..7f36aca33eee 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/DelegatingFilter.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/DelegatingFilter.kt
@@ -19,9 +19,9 @@ package com.android.hoststubgen.filters
* Base class for an [OutputFilter] that uses another filter as a fallback.
*/
abstract class DelegatingFilter(
- // fallback shouldn't be used by subclasses directly, so make it private.
- // They should instead be calling into `super` or `outermostFilter`.
- private val fallback: OutputFilter
+ // fallback shouldn't be used by subclasses directly, so make it private.
+ // They should instead be calling into `super` or `outermostFilter`.
+ private val fallback: OutputFilter
) : OutputFilter() {
init {
fallback.outermostFilter = this
@@ -50,24 +50,24 @@ abstract class DelegatingFilter(
}
override fun getPolicyForField(
- className: String,
- fieldName: String
+ className: String,
+ fieldName: String
): FilterPolicyWithReason {
return fallback.getPolicyForField(className, fieldName)
}
override fun getPolicyForMethod(
- className: String,
- methodName: String,
- descriptor: String
+ className: String,
+ methodName: String,
+ descriptor: String
): FilterPolicyWithReason {
return fallback.getPolicyForMethod(className, methodName, descriptor)
}
override fun getRenameTo(
- className: String,
- methodName: String,
- descriptor: String
+ className: String,
+ methodName: String,
+ descriptor: String
): String? {
return fallback.getRenameTo(className, methodName, descriptor)
}
@@ -97,13 +97,12 @@ abstract class DelegatingFilter(
}
override fun getMethodCallReplaceTo(
- callerClassName: String,
- callerMethodName: String,
className: String,
methodName: String,
descriptor: String,
): MethodReplaceTarget? {
return fallback.getMethodCallReplaceTo(
- callerClassName, callerMethodName, className, methodName, descriptor)
+ className, methodName, descriptor
+ )
}
}
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/ImplicitOutputFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/ImplicitOutputFilter.kt
index 474da6dfa1b9..d44d016f7c5b 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/ImplicitOutputFilter.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/ImplicitOutputFilter.kt
@@ -16,7 +16,6 @@
package com.android.hoststubgen.filters
import com.android.hoststubgen.HostStubGenErrors
-import com.android.hoststubgen.HostStubGenInternalException
import com.android.hoststubgen.asm.CLASS_INITIALIZER_DESC
import com.android.hoststubgen.asm.CLASS_INITIALIZER_NAME
import com.android.hoststubgen.asm.ClassNodes
@@ -37,19 +36,15 @@ import org.objectweb.asm.tree.ClassNode
* TODO: Do we need a way to make anonymous class methods and lambdas "throw"?
*/
class ImplicitOutputFilter(
- private val errors: HostStubGenErrors,
- private val classes: ClassNodes,
- fallback: OutputFilter
+ private val errors: HostStubGenErrors,
+ private val classes: ClassNodes,
+ fallback: OutputFilter
) : DelegatingFilter(fallback) {
- private fun getClassImplicitPolicy(className: String, cn: ClassNode): FilterPolicyWithReason? {
+ private fun getClassImplicitPolicy(cn: ClassNode): FilterPolicyWithReason? {
if (isAnonymousInnerClass(cn)) {
log.forDebug {
// log.d(" anon-inner class: ${className} outer: ${cn.outerClass} ")
}
- if (cn.outerClass == null) {
- throw HostStubGenInternalException(
- "outerClass is null for anonymous inner class")
- }
// If the outer class needs to be in impl, it should be in impl too.
val outerPolicy = outermostFilter.getPolicyForClass(cn.outerClass)
if (outerPolicy.policy.needsInOutput) {
@@ -65,15 +60,15 @@ class ImplicitOutputFilter(
val cn = classes.getClass(className)
// Use the implicit policy, if any.
- getClassImplicitPolicy(className, cn)?.let { return it }
+ getClassImplicitPolicy(cn)?.let { return it }
return fallback
}
override fun getPolicyForMethod(
- className: String,
- methodName: String,
- descriptor: String
+ className: String,
+ methodName: String,
+ descriptor: String
): FilterPolicyWithReason {
val fallback = super.getPolicyForMethod(className, methodName, descriptor)
val classPolicy = outermostFilter.getPolicyForClass(className)
@@ -84,12 +79,14 @@ class ImplicitOutputFilter(
// "keep" instead.
// Unless it's an enum -- in that case, the below code would handle it.
if (!cn.isEnum() &&
- fallback.policy == FilterPolicy.Throw &&
- methodName == CLASS_INITIALIZER_NAME && descriptor == CLASS_INITIALIZER_DESC) {
+ fallback.policy == FilterPolicy.Throw &&
+ methodName == CLASS_INITIALIZER_NAME && descriptor == CLASS_INITIALIZER_DESC
+ ) {
// TODO Maybe show a warning?? But that'd be too noisy with --default-throw.
return FilterPolicy.Ignore.withReason(
"'throw' on static initializer is handled as 'ignore'" +
- " [original throw reason: ${fallback.reason}]")
+ " [original throw reason: ${fallback.reason}]"
+ )
}
log.d("Class ${cn.name} Class policy: $classPolicy")
@@ -120,7 +117,8 @@ class ImplicitOutputFilter(
// For synthetic methods (such as lambdas), let's just inherit the class's
// policy.
return memberPolicy.withReason(classPolicy.reason).wrapReason(
- "is-synthetic-method")
+ "is-synthetic-method"
+ )
}
}
}
@@ -129,8 +127,8 @@ class ImplicitOutputFilter(
}
override fun getPolicyForField(
- className: String,
- fieldName: String
+ className: String,
+ fieldName: String
): FilterPolicyWithReason {
val fallback = super.getPolicyForField(className, fieldName)
@@ -161,4 +159,4 @@ class ImplicitOutputFilter(
return fallback
}
-} \ No newline at end of file
+}
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/InMemoryOutputFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/InMemoryOutputFilter.kt
index fc885d6f463b..59da3da99ea5 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/InMemoryOutputFilter.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/InMemoryOutputFilter.kt
@@ -28,10 +28,12 @@ class InMemoryOutputFilter(
private val classes: ClassNodes,
fallback: OutputFilter,
) : DelegatingFilter(fallback) {
- private val mPolicies: MutableMap<String, FilterPolicyWithReason> = mutableMapOf()
- private val mRenames: MutableMap<String, String> = mutableMapOf()
- private val mRedirectionClasses: MutableMap<String, String> = mutableMapOf()
- private val mClassLoadHooks: MutableMap<String, String> = mutableMapOf()
+ private val mPolicies = mutableMapOf<String, FilterPolicyWithReason>()
+ private val mRenames = mutableMapOf<String, String>()
+ private val mRedirectionClasses = mutableMapOf<String, String>()
+ private val mClassLoadHooks = mutableMapOf<String, String>()
+ private val mMethodCallReplaceSpecs = mutableListOf<MethodCallReplaceSpec>()
+ private val mTypeRenameSpecs = mutableListOf<TypeRenameSpec>()
private fun getClassKey(className: String): String {
return className.toHumanReadableClassName()
@@ -45,10 +47,6 @@ class InMemoryOutputFilter(
return getClassKey(className) + "." + methodName + ";" + signature
}
- override fun getPolicyForClass(className: String): FilterPolicyWithReason {
- return mPolicies[getClassKey(className)] ?: super.getPolicyForClass(className)
- }
-
private fun checkClass(className: String) {
if (classes.findClass(className) == null) {
log.w("Unknown class $className")
@@ -74,6 +72,10 @@ class InMemoryOutputFilter(
}
}
+ override fun getPolicyForClass(className: String): FilterPolicyWithReason {
+ return mPolicies[getClassKey(className)] ?: super.getPolicyForClass(className)
+ }
+
fun setPolicyForClass(className: String, policy: FilterPolicyWithReason) {
checkClass(className)
mPolicies[getClassKey(className)] = policy
@@ -81,7 +83,7 @@ class InMemoryOutputFilter(
override fun getPolicyForField(className: String, fieldName: String): FilterPolicyWithReason {
return mPolicies[getFieldKey(className, fieldName)]
- ?: super.getPolicyForField(className, fieldName)
+ ?: super.getPolicyForField(className, fieldName)
}
fun setPolicyForField(className: String, fieldName: String, policy: FilterPolicyWithReason) {
@@ -90,21 +92,21 @@ class InMemoryOutputFilter(
}
override fun getPolicyForMethod(
- className: String,
- methodName: String,
- descriptor: String,
- ): FilterPolicyWithReason {
+ className: String,
+ methodName: String,
+ descriptor: String,
+ ): FilterPolicyWithReason {
return mPolicies[getMethodKey(className, methodName, descriptor)]
?: mPolicies[getMethodKey(className, methodName, "*")]
?: super.getPolicyForMethod(className, methodName, descriptor)
}
fun setPolicyForMethod(
- className: String,
- methodName: String,
- descriptor: String,
- policy: FilterPolicyWithReason,
- ) {
+ className: String,
+ methodName: String,
+ descriptor: String,
+ policy: FilterPolicyWithReason,
+ ) {
checkMethod(className, methodName, descriptor)
mPolicies[getMethodKey(className, methodName, descriptor)] = policy
}
@@ -123,7 +125,7 @@ class InMemoryOutputFilter(
override fun getRedirectionClass(className: String): String? {
return mRedirectionClasses[getClassKey(className)]
- ?: super.getRedirectionClass(className)
+ ?: super.getRedirectionClass(className)
}
fun setRedirectionClass(from: String, to: String) {
@@ -135,11 +137,52 @@ class InMemoryOutputFilter(
}
override fun getClassLoadHooks(className: String): List<String> {
- return addNonNullElement(super.getClassLoadHooks(className),
- mClassLoadHooks[getClassKey(className)])
+ return addNonNullElement(
+ super.getClassLoadHooks(className),
+ mClassLoadHooks[getClassKey(className)]
+ )
}
fun setClassLoadHook(className: String, methodName: String) {
mClassLoadHooks[getClassKey(className)] = methodName.toHumanReadableMethodName()
}
+
+ override fun hasAnyMethodCallReplace(): Boolean {
+ return mMethodCallReplaceSpecs.isNotEmpty() || super.hasAnyMethodCallReplace()
+ }
+
+ override fun getMethodCallReplaceTo(
+ className: String,
+ methodName: String,
+ descriptor: String,
+ ): MethodReplaceTarget? {
+ // Maybe use 'Tri' if we end up having too many replacements.
+ mMethodCallReplaceSpecs.forEach {
+ if (className == it.fromClass &&
+ methodName == it.fromMethod
+ ) {
+ if (it.fromDescriptor == "*" || descriptor == it.fromDescriptor) {
+ return MethodReplaceTarget(it.toClass, it.toMethod)
+ }
+ }
+ }
+ return super.getMethodCallReplaceTo(className, methodName, descriptor)
+ }
+
+ fun setMethodCallReplaceSpec(spec: MethodCallReplaceSpec) {
+ mMethodCallReplaceSpecs.add(spec)
+ }
+
+ override fun remapType(className: String): String? {
+ mTypeRenameSpecs.forEach {
+ if (it.typeInternalNamePattern.matcher(className).matches()) {
+ return it.typeInternalNamePrefix + className
+ }
+ }
+ return super.remapType(className)
+ }
+
+ fun setRemapTypeSpec(spec: TypeRenameSpec) {
+ mTypeRenameSpecs.add(spec)
+ }
}
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/OutputFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/OutputFilter.kt
index f99ce906240a..c47bb302920f 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/OutputFilter.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/OutputFilter.kt
@@ -41,10 +41,10 @@ abstract class OutputFilter {
abstract fun getPolicyForField(className: String, fieldName: String): FilterPolicyWithReason
abstract fun getPolicyForMethod(
- className: String,
- methodName: String,
- descriptor: String,
- ): FilterPolicyWithReason
+ className: String,
+ methodName: String,
+ descriptor: String,
+ ): FilterPolicyWithReason
/**
* If a given method is a substitute-from method, return the substitute-to method name.
@@ -108,8 +108,6 @@ abstract class OutputFilter {
* If a method call should be forwarded to another method, return the target's class / method.
*/
open fun getMethodCallReplaceTo(
- callerClassName: String,
- callerMethodName: String,
className: String,
methodName: String,
descriptor: String,
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
index dd353e9caeff..97fc35302528 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
@@ -22,13 +22,13 @@ import com.android.hoststubgen.asm.toHumanReadableClassName
import com.android.hoststubgen.asm.toJvmClassName
import com.android.hoststubgen.log
import com.android.hoststubgen.normalizeTextLine
+import com.android.hoststubgen.utils.FileOrResource
import com.android.hoststubgen.whitespaceRegex
-import org.objectweb.asm.tree.ClassNode
import java.io.BufferedReader
-import java.io.FileReader
import java.io.PrintWriter
import java.io.Reader
import java.util.regex.Pattern
+import org.objectweb.asm.tree.ClassNode
/**
* Print a class node as a "keep" policy.
@@ -58,6 +58,23 @@ enum class SpecialClass {
RFile,
}
+data class MethodCallReplaceSpec(
+ val fromClass: String,
+ val fromMethod: String,
+ val fromDescriptor: String,
+ val toClass: String,
+ val toMethod: String,
+)
+
+/**
+ * When a package name matches [typeInternalNamePattern], we prepend [typeInternalNamePrefix]
+ * to it.
+ */
+data class TypeRenameSpec(
+ val typeInternalNamePattern: Pattern,
+ val typeInternalNamePrefix: String,
+)
+
/**
* This receives [TextFileFilterPolicyBuilder] parsing result.
*/
@@ -99,7 +116,7 @@ interface PolicyFileProcessor {
className: String,
methodName: String,
methodDesc: String,
- replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec,
+ replaceSpec: MethodCallReplaceSpec,
)
}
@@ -116,9 +133,6 @@ class TextFileFilterPolicyBuilder(
private var featureFlagsPolicy: FilterPolicyWithReason? = null
private var syspropsPolicy: FilterPolicyWithReason? = null
private var rFilePolicy: FilterPolicyWithReason? = null
- private val typeRenameSpec = mutableListOf<TextFilePolicyRemapperFilter.TypeRenameSpec>()
- private val methodReplaceSpec =
- mutableListOf<TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec>()
/**
* Fields for a filter chain used for "partial allowlisting", which are used by
@@ -126,47 +140,34 @@ class TextFileFilterPolicyBuilder(
*/
private val annotationAllowedInMemoryFilter: InMemoryOutputFilter
val annotationAllowedMembersFilter: OutputFilter
+ get() = annotationAllowedInMemoryFilter
private val annotationAllowedPolicy = FilterPolicy.AnnotationAllowed.withReason(FILTER_REASON)
init {
// Create a filter that checks "partial allowlisting".
- var aaf: OutputFilter = ConstantFilter(FilterPolicy.Remove, "default disallowed")
-
- aaf = InMemoryOutputFilter(classes, aaf)
- annotationAllowedInMemoryFilter = aaf
-
- annotationAllowedMembersFilter = annotationAllowedInMemoryFilter
+ val filter = ConstantFilter(FilterPolicy.Remove, "default disallowed")
+ annotationAllowedInMemoryFilter = InMemoryOutputFilter(classes, filter)
}
/**
* Parse a given policy file. This method can be called multiple times to read from
* multiple files. To get the resulting filter, use [createOutputFilter]
*/
- fun parse(file: String) {
+ fun parse(file: FileOrResource) {
// We may parse multiple files, but we reuse the same parser, because the parser
// will make sure there'll be no dupplicating "special class" policies.
- parser.parse(FileReader(file), file, Processor())
+ parser.parse(file.open(), file.path, Processor())
}
/**
* Generate the resulting [OutputFilter].
*/
fun createOutputFilter(): OutputFilter {
- var ret: OutputFilter = imf
- if (typeRenameSpec.isNotEmpty()) {
- ret = TextFilePolicyRemapperFilter(typeRenameSpec, ret)
- }
- if (methodReplaceSpec.isNotEmpty()) {
- ret = TextFilePolicyMethodReplaceFilter(methodReplaceSpec, classes, ret)
- }
-
// Wrap the in-memory-filter with AHF.
- ret = AndroidHeuristicsFilter(
- classes, aidlPolicy, featureFlagsPolicy, syspropsPolicy, rFilePolicy, ret
+ return AndroidHeuristicsFilter(
+ classes, aidlPolicy, featureFlagsPolicy, syspropsPolicy, rFilePolicy, imf
)
-
- return ret
}
private inner class Processor : PolicyFileProcessor {
@@ -180,9 +181,7 @@ class TextFileFilterPolicyBuilder(
}
override fun onRename(pattern: Pattern, prefix: String) {
- typeRenameSpec += TextFilePolicyRemapperFilter.TypeRenameSpec(
- pattern, prefix
- )
+ imf.setRemapTypeSpec(TypeRenameSpec(pattern, prefix))
}
override fun onClassStart(className: String) {
@@ -284,12 +283,12 @@ class TextFileFilterPolicyBuilder(
className: String,
methodName: String,
methodDesc: String,
- replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec,
+ replaceSpec: MethodCallReplaceSpec,
) {
// Keep the source method, because the target method may call it.
imf.setPolicyForMethod(className, methodName, methodDesc,
FilterPolicy.Keep.withReason(FILTER_REASON))
- methodReplaceSpec.add(replaceSpec)
+ imf.setMethodCallReplaceSpec(replaceSpec)
}
}
}
@@ -630,13 +629,13 @@ class TextFileFilterPolicyParser {
if (classAndMethod != null) {
// If the substitution target contains a ".", then
// it's a method call redirect.
- val spec = TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec(
- currentClassName!!.toJvmClassName(),
- methodName,
- signature,
- classAndMethod.first.toJvmClassName(),
- classAndMethod.second,
- )
+ val spec = MethodCallReplaceSpec(
+ className.toJvmClassName(),
+ methodName,
+ signature,
+ classAndMethod.first.toJvmClassName(),
+ classAndMethod.second,
+ )
processor.onMethodOutClassReplace(
className,
methodName,
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyMethodReplaceFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyMethodReplaceFilter.kt
deleted file mode 100644
index a3f934cacc2c..000000000000
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyMethodReplaceFilter.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.hoststubgen.filters
-
-import com.android.hoststubgen.asm.ClassNodes
-
-/**
- * Filter used by TextFileFilterPolicyParser for "method call relacement".
- */
-class TextFilePolicyMethodReplaceFilter(
- val spec: List<MethodCallReplaceSpec>,
- val classes: ClassNodes,
- val fallback: OutputFilter,
-) : DelegatingFilter(fallback) {
-
- data class MethodCallReplaceSpec(
- val fromClass: String,
- val fromMethod: String,
- val fromDescriptor: String,
- val toClass: String,
- val toMethod: String,
- )
-
- override fun hasAnyMethodCallReplace(): Boolean {
- return true
- }
-
- override fun getMethodCallReplaceTo(
- callerClassName: String,
- callerMethodName: String,
- className: String,
- methodName: String,
- descriptor: String,
- ): MethodReplaceTarget? {
- // Maybe use 'Tri' if we end up having too many replacements.
- spec.forEach {
- if (className == it.fromClass &&
- methodName == it.fromMethod
- ) {
- if (it.fromDescriptor == "*" || descriptor == it.fromDescriptor) {
- return MethodReplaceTarget(it.toClass, it.toMethod)
- }
- }
- }
- return null
- }
-}
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyRemapperFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyRemapperFilter.kt
deleted file mode 100644
index bc90d1248322..000000000000
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyRemapperFilter.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.hoststubgen.filters
-
-import java.util.regex.Pattern
-
-/**
- * A filter that provides a simple "jarjar" functionality via [mapType]
- */
-class TextFilePolicyRemapperFilter(
- val typeRenameSpecs: List<TypeRenameSpec>,
- fallback: OutputFilter,
-) : DelegatingFilter(fallback) {
- /**
- * When a package name matches [typeInternalNamePattern], we prepend [typeInternalNamePrefix]
- * to it.
- */
- data class TypeRenameSpec(
- val typeInternalNamePattern: Pattern,
- val typeInternalNamePrefix: String,
- )
-
- override fun remapType(className: String): String? {
- typeRenameSpecs.forEach {
- if (it.typeInternalNamePattern.matcher(className).matches()) {
- return it.typeInternalNamePrefix + className
- }
- }
- return null
- }
-}
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/utils/OptionUtils.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/utils/OptionUtils.kt
index 0b17879b862c..d0869929edfb 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/utils/OptionUtils.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/utils/OptionUtils.kt
@@ -16,11 +16,16 @@
package com.android.hoststubgen.utils
import com.android.hoststubgen.ArgumentsException
-import com.android.hoststubgen.ensureFileExists
+import com.android.hoststubgen.InputFileNotFoundException
+import com.android.hoststubgen.JarResourceNotFoundException
import com.android.hoststubgen.log
import com.android.hoststubgen.normalizeTextLine
-import java.io.BufferedReader
+import java.io.File
import java.io.FileReader
+import java.io.InputStreamReader
+import java.io.Reader
+
+const val JAR_RESOURCE_PREFIX = "jar:"
/**
* Base class for parsing arguments from commandline.
@@ -73,7 +78,7 @@ abstract class BaseOptions {
*
* Subclasses override/extend this method to support more options.
*/
- abstract fun parseOption(option: String, ai: ArgIterator): Boolean
+ abstract fun parseOption(option: String, args: ArgIterator): Boolean
abstract fun dumpFields(): String
}
@@ -112,7 +117,9 @@ class ArgIterator(
companion object {
fun withAtFiles(args: List<String>): ArgIterator {
- return ArgIterator(expandAtFiles(args))
+ val expanded = mutableListOf<String>()
+ expandAtFiles(args.asSequence(), expanded)
+ return ArgIterator(expanded)
}
/**
@@ -125,34 +132,30 @@ class ArgIterator(
*
* The file can contain '#' as comments.
*/
- private fun expandAtFiles(args: List<String>): List<String> {
- val ret = mutableListOf<String>()
-
+ private fun expandAtFiles(args: Sequence<String>, out: MutableList<String>) {
args.forEach { arg ->
if (arg.startsWith("@@")) {
- ret += arg.substring(1)
+ out.add(arg.substring(1))
return@forEach
} else if (!arg.startsWith('@')) {
- ret += arg
+ out.add(arg)
return@forEach
}
+
// Read from the file, and add each line to the result.
- val filename = arg.substring(1).ensureFileExists()
+ val file = FileOrResource(arg.substring(1))
- log.v("Expanding options file $filename")
+ log.v("Expanding options file ${file.path}")
- BufferedReader(FileReader(filename)).use { reader ->
- while (true) {
- var line = reader.readLine() ?: break // EOF
+ val fileArgs = file
+ .open()
+ .buffered()
+ .lineSequence()
+ .map(::normalizeTextLine)
+ .filter(CharSequence::isNotEmpty)
- line = normalizeTextLine(line)
- if (line.isNotEmpty()) {
- ret += line
- }
- }
- }
+ expandAtFiles(fileArgs, out)
}
- return ret
}
}
}
@@ -204,3 +207,37 @@ class IntSetOnce(value: Int) : SetOnce<Int>(value) {
}
}
}
+
+/**
+ * A path either points to a file in filesystem, or an entry in the JAR.
+ */
+class FileOrResource(val path: String) {
+ init {
+ path.ensureFileExists()
+ }
+
+ /**
+ * Either read from filesystem, or read from JAR resources.
+ */
+ fun open(): Reader {
+ return if (path.startsWith(JAR_RESOURCE_PREFIX)) {
+ val path = path.removePrefix(JAR_RESOURCE_PREFIX)
+ InputStreamReader(this::class.java.classLoader.getResourceAsStream(path)!!)
+ } else {
+ FileReader(path)
+ }
+ }
+}
+
+fun String.ensureFileExists(): String {
+ if (this.startsWith(JAR_RESOURCE_PREFIX)) {
+ val cl = FileOrResource::class.java.classLoader
+ val path = this.removePrefix(JAR_RESOURCE_PREFIX)
+ if (cl.getResource(path) == null) {
+ throw JarResourceNotFoundException(path)
+ }
+ } else if (!File(this).exists()) {
+ throw InputFileNotFoundException(this)
+ }
+ return this
+}
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/BaseAdapter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/BaseAdapter.kt
index a08d1d605949..769b769d7a20 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/BaseAdapter.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/BaseAdapter.kt
@@ -17,7 +17,6 @@ package com.android.hoststubgen.visitors
import com.android.hoststubgen.HostStubGenErrors
import com.android.hoststubgen.HostStubGenStats
-import com.android.hoststubgen.LogLevel
import com.android.hoststubgen.asm.ClassNodes
import com.android.hoststubgen.asm.UnifiedVisitor
import com.android.hoststubgen.asm.getPackageNameFromFullClassName
@@ -26,13 +25,10 @@ import com.android.hoststubgen.filters.FilterPolicyWithReason
import com.android.hoststubgen.filters.OutputFilter
import com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
import com.android.hoststubgen.log
-import java.io.PrintWriter
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.FieldVisitor
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes
-import org.objectweb.asm.commons.ClassRemapper
-import org.objectweb.asm.util.TraceClassVisitor
const val OPCODE_VERSION = Opcodes.ASM9
@@ -49,8 +45,6 @@ abstract class BaseAdapter(
data class Options(
val errors: HostStubGenErrors,
val stats: HostStubGenStats?,
- val enablePreTrace: Boolean,
- val enablePostTrace: Boolean,
val deleteClassFinals: Boolean,
val deleteMethodFinals: Boolean,
// We don't remove finals from fields, because final fields have a stronger memory
@@ -253,50 +247,4 @@ abstract class BaseAdapter(
substituted: Boolean,
superVisitor: MethodVisitor?,
): MethodVisitor?
-
- companion object {
- fun getVisitor(
- classInternalName: String,
- classes: ClassNodes,
- nextVisitor: ClassVisitor,
- filter: OutputFilter,
- packageRedirector: PackageRedirectRemapper,
- options: Options,
- ): ClassVisitor {
- var next = nextVisitor
-
- val verbosePrinter = PrintWriter(log.getWriter(LogLevel.Verbose))
-
- // Inject TraceClassVisitor for debugging.
- if (options.enablePostTrace) {
- next = TraceClassVisitor(next, verbosePrinter)
- }
-
- // Handle --package-redirect
- if (!packageRedirector.isEmpty) {
- // Don't apply the remapper on redirect-from classes.
- // Otherwise, if the target jar actually contains the "from" classes (which
- // may or may not be the case) they'd be renamed.
- // But we update all references in other places, so, a method call to a "from" class
- // would be replaced with the "to" class. All type references (e.g. variable types)
- // will be updated too.
- if (!packageRedirector.isTarget(classInternalName)) {
- next = ClassRemapper(next, packageRedirector)
- } else {
- log.v(
- "Class $classInternalName is a redirect-from class, not applying" +
- " --package-redirect"
- )
- }
- }
-
- next = ImplGeneratingAdapter(classes, next, filter, options)
-
- // Inject TraceClassVisitor for debugging.
- if (options.enablePreTrace) {
- next = TraceClassVisitor(next, verbosePrinter)
- }
- return next
- }
- }
}
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
index b8a357668c2b..617385ad438e 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
@@ -396,7 +396,7 @@ class ImplGeneratingAdapter(
}
val to = filter.getMethodCallReplaceTo(
- currentClassName, callerMethodName, owner, name, descriptor
+ owner, name, descriptor
)
if (to == null
diff --git a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
index 8bb454fa12e7..d9cc54aebf51 100644
--- a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
+++ b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
@@ -18,6 +18,7 @@ package com.android.hoststubgen
import com.android.hoststubgen.utils.ArgIterator
import com.android.hoststubgen.utils.IntSetOnce
import com.android.hoststubgen.utils.SetOnce
+import com.android.hoststubgen.utils.ensureFileExists
/**
* Options that can be set from command line arguments.
@@ -61,9 +62,9 @@ class HostStubGenOptions(
}
}
- override fun parseOption(option: String, ai: ArgIterator): Boolean {
+ override fun parseOption(option: String, args: ArgIterator): Boolean {
// Define some shorthands...
- fun nextArg(): String = ai.nextArgRequired(option)
+ fun nextArg(): String = args.nextArgRequired(option)
when (option) {
// TODO: Write help
@@ -94,7 +95,7 @@ class HostStubGenOptions(
}
}
- else -> return super.parseOption(option, ai)
+ else -> return super.parseOption(option, args)
}
return true
diff --git a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaOptions.kt b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaOptions.kt
index f7fd0804c151..58bd9e987fd1 100644
--- a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaOptions.kt
+++ b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaOptions.kt
@@ -16,10 +16,10 @@
package com.android.platform.test.ravenwood.ravenhelper.policytoannot
import com.android.hoststubgen.ArgumentsException
-import com.android.hoststubgen.ensureFileExists
import com.android.hoststubgen.utils.ArgIterator
import com.android.hoststubgen.utils.BaseOptions
import com.android.hoststubgen.utils.SetOnce
+import com.android.hoststubgen.utils.ensureFileExists
/**
* Options for the "ravenhelper pta" subcommand.
@@ -41,8 +41,8 @@ class PtaOptions(
var dumpOperations: SetOnce<Boolean> = SetOnce(false),
) : BaseOptions() {
- override fun parseOption(option: String, ai: ArgIterator): Boolean {
- fun nextArg(): String = ai.nextArgRequired(option)
+ override fun parseOption(option: String, args: ArgIterator): Boolean {
+ fun nextArg(): String = args.nextArgRequired(option)
when (option) {
// TODO: Write help
diff --git a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaProcessor.kt b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaProcessor.kt
index fd6f732a06ce..5ce9a23e6e05 100644
--- a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaProcessor.kt
+++ b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaProcessor.kt
@@ -19,10 +19,10 @@ import com.android.hoststubgen.LogLevel
import com.android.hoststubgen.asm.CLASS_INITIALIZER_NAME
import com.android.hoststubgen.asm.toJvmClassName
import com.android.hoststubgen.filters.FilterPolicyWithReason
+import com.android.hoststubgen.filters.MethodCallReplaceSpec
import com.android.hoststubgen.filters.PolicyFileProcessor
import com.android.hoststubgen.filters.SpecialClass
import com.android.hoststubgen.filters.TextFileFilterPolicyParser
-import com.android.hoststubgen.filters.TextFilePolicyMethodReplaceFilter
import com.android.hoststubgen.log
import com.android.hoststubgen.utils.ClassPredicate
import com.android.platform.test.ravenwood.ravenhelper.SubcommandHandler
@@ -448,7 +448,7 @@ private class TextPolicyToAnnotationConverter(
className: String,
methodName: String,
methodDesc: String,
- replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec,
+ replaceSpec: MethodCallReplaceSpec,
) {
// This can't be converted to an annotation.
classHasMember = true
diff --git a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/sourcemap/MapOptions.kt b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/sourcemap/MapOptions.kt
index 8b95843f08a6..6e0b7b89cf13 100644
--- a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/sourcemap/MapOptions.kt
+++ b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/sourcemap/MapOptions.kt
@@ -16,10 +16,10 @@
package com.android.platform.test.ravenwood.ravenhelper.sourcemap
import com.android.hoststubgen.ArgumentsException
-import com.android.hoststubgen.ensureFileExists
import com.android.hoststubgen.utils.ArgIterator
import com.android.hoststubgen.utils.BaseOptions
import com.android.hoststubgen.utils.SetOnce
+import com.android.hoststubgen.utils.ensureFileExists
/**
* Options for the "ravenhelper map" subcommand.
@@ -38,8 +38,8 @@ class MapOptions(
var text: SetOnce<String?> = SetOnce(null),
) : BaseOptions() {
- override fun parseOption(option: String, ai: ArgIterator): Boolean {
- fun nextArg(): String = ai.nextArgRequired(option)
+ override fun parseOption(option: String, args: ArgIterator): Boolean {
+ fun nextArg(): String = args.nextArgRequired(option)
when (option) {
// TODO: Write help
diff --git a/ravenwood/tools/ravenizer/Android.bp b/ravenwood/tools/ravenizer/Android.bp
index 957e20647d44..93cda4e3c4c9 100644
--- a/ravenwood/tools/ravenizer/Android.bp
+++ b/ravenwood/tools/ravenizer/Android.bp
@@ -15,5 +15,10 @@ java_binary_host {
"hoststubgen-lib",
"ravenwood-junit-for-ravenizer",
],
+ java_resources: [
+ ":ravenizer-standard-options",
+ ":ravenwood-standard-annotations",
+ ":ravenwood-common-policies",
+ ],
visibility: ["//visibility:public"],
}
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt
index e67c730df069..04e3bda2ba27 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt
@@ -16,23 +16,22 @@
package com.android.platform.test.ravenwood.ravenizer
import com.android.hoststubgen.GeneralUserErrorException
+import com.android.hoststubgen.HostStubGenClassProcessor
import com.android.hoststubgen.asm.ClassNodes
import com.android.hoststubgen.asm.zipEntryNameToClassName
import com.android.hoststubgen.executableName
import com.android.hoststubgen.log
import com.android.platform.test.ravenwood.ravenizer.adapter.RunnerRewritingAdapter
-import org.objectweb.asm.ClassReader
-import org.objectweb.asm.ClassVisitor
-import org.objectweb.asm.ClassWriter
-import org.objectweb.asm.util.CheckClassAdapter
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.FileOutputStream
-import java.io.InputStream
-import java.io.OutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipOutputStream
+import org.objectweb.asm.ClassReader
+import org.objectweb.asm.ClassVisitor
+import org.objectweb.asm.ClassWriter
+import org.objectweb.asm.util.CheckClassAdapter
/**
* Various stats on Ravenizer.
@@ -41,7 +40,7 @@ data class RavenizerStats(
/** Total end-to-end time. */
var totalTime: Double = .0,
- /** Time took to build [ClasNodes] */
+ /** Time took to build [ClassNodes] */
var loadStructureTime: Double = .0,
/** Time took to validate the classes */
@@ -50,14 +49,17 @@ data class RavenizerStats(
/** Total real time spent for converting the jar file */
var totalProcessTime: Double = .0,
- /** Total real time spent for converting class files (except for I/O time). */
- var totalConversionTime: Double = .0,
+ /** Total real time spent for ravenizing class files (excluding I/O time). */
+ var totalRavenizeTime: Double = .0,
+
+ /** Total real time spent for processing class files HSG style (excluding I/O time). */
+ var totalHostStubGenTime: Double = .0,
/** Total real time spent for copying class files without modification. */
var totalCopyTime: Double = .0,
/** # of entries in the input jar file */
- var totalEntiries: Int = 0,
+ var totalEntries: Int = 0,
/** # of *.class files in the input jar file */
var totalClasses: Int = 0,
@@ -67,14 +69,15 @@ data class RavenizerStats(
) {
override fun toString(): String {
return """
- RavenizerStats{
+ RavenizerStats {
totalTime=$totalTime,
loadStructureTime=$loadStructureTime,
validationTime=$validationTime,
totalProcessTime=$totalProcessTime,
- totalConversionTime=$totalConversionTime,
+ totalRavenizeTime=$totalRavenizeTime,
+ totalHostStubGenTime=$totalHostStubGenTime,
totalCopyTime=$totalCopyTime,
- totalEntiries=$totalEntiries,
+ totalEntries=$totalEntries,
totalClasses=$totalClasses,
processedClasses=$processedClasses,
}
@@ -90,12 +93,18 @@ class Ravenizer {
val stats = RavenizerStats()
stats.totalTime = log.nTime {
+ val allClasses = ClassNodes.loadClassStructures(options.inJar.get) {
+ stats.loadStructureTime = it
+ }
+ val processor = HostStubGenClassProcessor(options, allClasses)
+
process(
options.inJar.get,
options.outJar.get,
options.enableValidation.get,
options.fatalValidation.get,
options.stripMockito.get,
+ processor,
stats,
)
}
@@ -108,15 +117,13 @@ class Ravenizer {
enableValidation: Boolean,
fatalValidation: Boolean,
stripMockito: Boolean,
+ processor: HostStubGenClassProcessor,
stats: RavenizerStats,
) {
- var allClasses = ClassNodes.loadClassStructures(inJar) {
- time -> stats.loadStructureTime = time
- }
if (enableValidation) {
stats.validationTime = log.iTime("Validating classes") {
- if (!validateClasses(allClasses)) {
- var message = "Invalid test class(es) detected." +
+ if (!validateClasses(processor.allClasses)) {
+ val message = "Invalid test class(es) detected." +
" See error log for details."
if (fatalValidation) {
throw RavenizerInvalidTestException(message)
@@ -126,7 +133,7 @@ class Ravenizer {
}
}
}
- if (includeUnsupportedMockito(allClasses)) {
+ if (includeUnsupportedMockito(processor.allClasses)) {
log.w("Unsupported Mockito detected in $inJar!")
}
@@ -134,7 +141,7 @@ class Ravenizer {
ZipFile(inJar).use { inZip ->
val inEntries = inZip.entries()
- stats.totalEntiries = inZip.size()
+ stats.totalEntries = inZip.size()
ZipOutputStream(BufferedOutputStream(FileOutputStream(outJar))).use { outZip ->
while (inEntries.hasMoreElements()) {
@@ -159,9 +166,9 @@ class Ravenizer {
stats.totalClasses += 1
}
- if (className != null && shouldProcessClass(allClasses, className)) {
- stats.processedClasses += 1
- processSingleClass(inZip, entry, outZip, allClasses, stats)
+ if (className != null &&
+ shouldProcessClass(processor.allClasses, className)) {
+ processSingleClass(inZip, entry, outZip, processor, stats)
} else {
// Too slow, let's use merge_zips to bring back the original classes.
copyZipEntry(inZip, entry, outZip, stats)
@@ -201,14 +208,22 @@ class Ravenizer {
inZip: ZipFile,
entry: ZipEntry,
outZip: ZipOutputStream,
- allClasses: ClassNodes,
+ processor: HostStubGenClassProcessor,
stats: RavenizerStats,
) {
+ stats.processedClasses += 1
val newEntry = ZipEntry(entry.name)
outZip.putNextEntry(newEntry)
BufferedInputStream(inZip.getInputStream(entry)).use { bis ->
- processSingleClass(entry, bis, outZip, allClasses, stats)
+ var classBytes = bis.readBytes()
+ stats.totalRavenizeTime += log.vTime("Ravenize ${entry.name}") {
+ classBytes = ravenizeSingleClass(entry, classBytes, processor.allClasses)
+ }
+ stats.totalHostStubGenTime += log.vTime("HostStubGen ${entry.name}") {
+ classBytes = processor.processClassBytecode(classBytes)
+ }
+ outZip.write(classBytes)
}
outZip.closeEntry()
}
@@ -217,41 +232,34 @@ class Ravenizer {
* Whether a class needs to be processed. This must be kept in sync with [processSingleClass].
*/
private fun shouldProcessClass(classes: ClassNodes, classInternalName: String): Boolean {
- return !classInternalName.shouldByBypassed()
+ return !classInternalName.shouldBypass()
&& RunnerRewritingAdapter.shouldProcess(classes, classInternalName)
}
- private fun processSingleClass(
+ private fun ravenizeSingleClass(
entry: ZipEntry,
- input: InputStream,
- output: OutputStream,
+ input: ByteArray,
allClasses: ClassNodes,
- stats: RavenizerStats,
- ) {
- val cr = ClassReader(input)
-
- lateinit var data: ByteArray
- stats.totalConversionTime += log.vTime("Modify ${entry.name}") {
+ ): ByteArray {
+ val classInternalName = zipEntryNameToClassName(entry.name)
+ ?: throw RavenizerInternalException("Unexpected zip entry name: ${entry.name}")
- val classInternalName = zipEntryNameToClassName(entry.name)
- ?: throw RavenizerInternalException("Unexpected zip entry name: ${entry.name}")
- val flags = ClassWriter.COMPUTE_MAXS
- val cw = ClassWriter(flags)
- var outVisitor: ClassVisitor = cw
+ val flags = ClassWriter.COMPUTE_MAXS
+ val cw = ClassWriter(flags)
+ var outVisitor: ClassVisitor = cw
- val enableChecker = false
- if (enableChecker) {
- outVisitor = CheckClassAdapter(outVisitor)
- }
+ val enableChecker = false
+ if (enableChecker) {
+ outVisitor = CheckClassAdapter(outVisitor)
+ }
- // This must be kept in sync with shouldProcessClass.
- outVisitor = RunnerRewritingAdapter.maybeApply(
- classInternalName, allClasses, outVisitor)
+ // This must be kept in sync with shouldProcessClass.
+ outVisitor = RunnerRewritingAdapter.maybeApply(
+ classInternalName, allClasses, outVisitor)
- cr.accept(outVisitor, ClassReader.EXPAND_FRAMES)
+ val cr = ClassReader(input)
+ cr.accept(outVisitor, ClassReader.EXPAND_FRAMES)
- data = cw.toByteArray()
- }
- output.write(data)
+ return cw.toByteArray()
}
}
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt
index 7f4829ec6127..8a09e6d533b8 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt
@@ -21,6 +21,7 @@ import com.android.hoststubgen.LogLevel
import com.android.hoststubgen.executableName
import com.android.hoststubgen.log
import com.android.hoststubgen.runMainWithBoilerplate
+import com.android.hoststubgen.utils.JAR_RESOURCE_PREFIX
import java.nio.file.Paths
import kotlin.io.path.exists
@@ -36,6 +37,10 @@ import kotlin.io.path.exists
*/
private val RAVENIZER_DOTFILE = System.getenv("HOME") + "/.ravenizer-unsafe"
+/**
+ * This is the name of the standard option text file embedded inside ravenizer.jar.
+ */
+private const val RAVENIZER_STANDARD_OPTIONS = "texts/ravenizer-standard-options.txt"
/**
* Entry point.
@@ -45,12 +50,12 @@ fun main(args: Array<String>) {
log.setConsoleLogLevel(LogLevel.Info)
runMainWithBoilerplate {
- var newArgs = args.asList()
+ val newArgs = args.toMutableList()
+ newArgs.add(0, "@$JAR_RESOURCE_PREFIX$RAVENIZER_STANDARD_OPTIONS")
+
if (Paths.get(RAVENIZER_DOTFILE).exists()) {
log.i("Reading options from $RAVENIZER_DOTFILE")
- newArgs = args.toMutableList().apply {
- add(0, "@$RAVENIZER_DOTFILE")
- }
+ newArgs.add(0, "@$RAVENIZER_DOTFILE")
}
val options = RavenizerOptions().apply { parseArgs(newArgs) }
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt
index 2c0365404ab6..5d278bb046ae 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt
@@ -16,10 +16,10 @@
package com.android.platform.test.ravenwood.ravenizer
import com.android.hoststubgen.ArgumentsException
-import com.android.hoststubgen.ensureFileExists
+import com.android.hoststubgen.HostStubGenClassProcessorOptions
import com.android.hoststubgen.utils.ArgIterator
-import com.android.hoststubgen.utils.BaseOptions
import com.android.hoststubgen.utils.SetOnce
+import com.android.hoststubgen.utils.ensureFileExists
class RavenizerOptions(
/** Input jar file*/
@@ -36,10 +36,10 @@ class RavenizerOptions(
/** Whether to remove mockito and dexmaker classes. */
var stripMockito: SetOnce<Boolean> = SetOnce(false),
-) : BaseOptions() {
+) : HostStubGenClassProcessorOptions() {
- override fun parseOption(option: String, ai: ArgIterator): Boolean {
- fun nextArg(): String = ai.nextArgRequired(option)
+ override fun parseOption(option: String, args: ArgIterator): Boolean {
+ fun nextArg(): String = args.nextArgRequired(option)
when (option) {
// TODO: Write help
@@ -57,7 +57,7 @@ class RavenizerOptions(
"--strip-mockito" -> stripMockito.set(true)
"--no-strip-mockito" -> stripMockito.set(false)
- else -> return false
+ else -> return super.parseOption(option, args)
}
return true
@@ -79,6 +79,6 @@ class RavenizerOptions(
enableValidation=$enableValidation,
fatalValidation=$fatalValidation,
stripMockito=$stripMockito,
- """.trimIndent()
+ """.trimIndent() + '\n' + super.dumpFields()
}
}
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt
index 6092fcc9402d..b394a761c7ae 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt
@@ -15,8 +15,8 @@
*/
package com.android.platform.test.ravenwood.ravenizer
-import android.platform.test.annotations.internal.InnerRunner
import android.platform.test.annotations.NoRavenizer
+import android.platform.test.annotations.internal.InnerRunner
import android.platform.test.ravenwood.RavenwoodAwareTestRunner
import com.android.hoststubgen.asm.ClassNodes
import com.android.hoststubgen.asm.findAnyAnnotation
@@ -85,7 +85,7 @@ fun String.isRavenwoodClass(): Boolean {
/**
* Classes that should never be modified.
*/
-fun String.shouldByBypassed(): Boolean {
+fun String.shouldBypass(): Boolean {
if (this.isRavenwoodClass()) {
return true
}
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt
index 61e254b225c3..d252b4dc8ab6 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt
@@ -20,8 +20,8 @@ import com.android.hoststubgen.asm.isAbstract
import com.android.hoststubgen.asm.startsWithAny
import com.android.hoststubgen.asm.toHumanReadableClassName
import com.android.hoststubgen.log
-import org.objectweb.asm.tree.ClassNode
import java.util.regex.Pattern
+import org.objectweb.asm.tree.ClassNode
fun validateClasses(classes: ClassNodes): Boolean {
var allOk = true
@@ -37,7 +37,7 @@ fun validateClasses(classes: ClassNodes): Boolean {
*
*/
fun checkClass(cn: ClassNode, classes: ClassNodes): Boolean {
- if (cn.name.shouldByBypassed()) {
+ if (cn.name.shouldBypass()) {
// Class doesn't need to be checked.
return true
}
@@ -145,4 +145,4 @@ com.android.server.power.stats.BatteryStatsTimerTest
private fun isAllowListedLegacyTest(targetClass: ClassNode): Boolean {
return allowListedLegacyTests.contains(targetClass.name)
-} \ No newline at end of file
+}
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index b52b3dabd47d..64a4a4ae3c0d 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -260,6 +260,16 @@ flag {
}
flag {
+ name: "pointer_up_motion_event_in_touch_exploration"
+ namespace: "accessibility"
+ description: "Allows POINTER_UP motionEvents to trigger during touch exploration."
+ bug: "374930391"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "proxy_use_apps_on_virtual_device_listener"
namespace: "accessibility"
description: "Fixes race condition described in b/286587811"
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index c49151dd5e30..573c591cb504 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -521,15 +521,6 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
@Nullable IBinder focusedToken) {
return AccessibilityManagerService.this.handleKeyGestureEvent(event);
}
-
- @Override
- public boolean isKeyGestureSupported(int gestureType) {
- return switch (gestureType) {
- case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION,
- KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK -> true;
- default -> false;
- };
- }
};
@VisibleForTesting
diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
index 0f6f86b39458..cc93d0887d89 100644
--- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
+++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
@@ -25,6 +25,7 @@ import static android.view.accessibility.AccessibilityManager.AUTOCLICK_IGNORE_M
import static com.android.server.accessibility.autoclick.AutoclickIndicatorView.SHOW_INDICATOR_DELAY_TIME;
import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_LEFT_CLICK;
import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_RIGHT_CLICK;
+import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_SCROLL;
import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AutoclickType;
import static com.android.server.accessibility.autoclick.AutoclickTypePanel.ClickPanelControllerInterface;
@@ -87,6 +88,7 @@ public class AutoclickController extends BaseEventStreamTransformation {
@VisibleForTesting AutoclickIndicatorScheduler mAutoclickIndicatorScheduler;
@VisibleForTesting AutoclickIndicatorView mAutoclickIndicatorView;
@VisibleForTesting AutoclickTypePanel mAutoclickTypePanel;
+ @VisibleForTesting AutoclickScrollPanel mAutoclickScrollPanel;
private WindowManager mWindowManager;
// Default click type is left-click.
@@ -98,6 +100,11 @@ public class AutoclickController extends BaseEventStreamTransformation {
@Override
public void handleAutoclickTypeChange(@AutoclickType int clickType) {
mActiveClickType = clickType;
+
+ // Hide scroll panel when type is not scroll.
+ if (clickType != AUTOCLICK_TYPE_SCROLL && mAutoclickScrollPanel != null) {
+ mAutoclickScrollPanel.hide();
+ }
}
@Override
@@ -161,6 +168,7 @@ public class AutoclickController extends BaseEventStreamTransformation {
mWindowManager = mContext.getSystemService(WindowManager.class);
mAutoclickTypePanel =
new AutoclickTypePanel(mContext, mWindowManager, mUserId, clickPanelController);
+ mAutoclickScrollPanel = new AutoclickScrollPanel(mContext, mWindowManager);
mAutoclickTypePanel.show();
mWindowManager.addView(mAutoclickIndicatorView, mAutoclickIndicatorView.getLayoutParams());
@@ -189,6 +197,10 @@ public class AutoclickController extends BaseEventStreamTransformation {
mClickScheduler.cancel();
}
+ if (mAutoclickScrollPanel != null) {
+ mAutoclickScrollPanel.hide();
+ }
+
super.clearEvents(inputSource);
}
@@ -209,6 +221,11 @@ public class AutoclickController extends BaseEventStreamTransformation {
mWindowManager.removeView(mAutoclickIndicatorView);
mAutoclickTypePanel.hide();
}
+
+ if (mAutoclickScrollPanel != null) {
+ mAutoclickScrollPanel.hide();
+ mAutoclickScrollPanel = null;
+ }
}
private void handleMouseMotion(MotionEvent event, int policyFlags) {
@@ -231,7 +248,11 @@ public class AutoclickController extends BaseEventStreamTransformation {
private boolean isPaused() {
return Flags.enableAutoclickIndicator() && mAutoclickTypePanel.isPaused()
- && !mAutoclickTypePanel.isHovered();
+ && !isHovered();
+ }
+
+ private boolean isHovered() {
+ return Flags.enableAutoclickIndicator() && mAutoclickTypePanel.isHovered();
}
private void cancelPendingClick() {
@@ -478,6 +499,8 @@ public class AutoclickController extends BaseEventStreamTransformation {
private int mEventPolicyFlags;
/** Current meta state. This value will be used as meta state for click event sequence. */
private int mMetaState;
+ /** Last observed panel hovered state when click was scheduled. */
+ private boolean mHoveredState;
/**
* The current anchor's coordinates. Should be ignored if #mLastMotionEvent is null.
@@ -631,6 +654,7 @@ public class AutoclickController extends BaseEventStreamTransformation {
}
mLastMotionEvent = MotionEvent.obtain(event);
mEventPolicyFlags = policyFlags;
+ mHoveredState = isHovered();
if (useAsAnchor) {
final int pointerIndex = mLastMotionEvent.getActionIndex();
@@ -687,6 +711,14 @@ public class AutoclickController extends BaseEventStreamTransformation {
return;
}
+ // Handle scroll type specially, show scroll panel instead of sending click events.
+ if (mActiveClickType == AutoclickTypePanel.AUTOCLICK_TYPE_SCROLL) {
+ if (mAutoclickScrollPanel != null) {
+ mAutoclickScrollPanel.show();
+ }
+ return;
+ }
+
final int pointerIndex = mLastMotionEvent.getActionIndex();
if (mTempPointerProperties == null) {
@@ -704,14 +736,18 @@ public class AutoclickController extends BaseEventStreamTransformation {
final long now = SystemClock.uptimeMillis();
- // TODO(b/395094903): always triggers left-click when the cursor hovers over the
- // autoclick type panel, to always allow users to change a different click type.
- // Otherwise, if one chooses the right-click, this user won't be able to rely on
- // autoclick to select other click types.
- final int actionButton =
- mActiveClickType == AUTOCLICK_TYPE_RIGHT_CLICK
- ? BUTTON_SECONDARY
- : BUTTON_PRIMARY;
+ int actionButton;
+ if (mHoveredState) {
+ // Always triggers left-click when the cursor hovers over the autoclick type
+ // panel, to always allow users to change a different click type. Otherwise, if
+ // one chooses the right-click, this user won't be able to rely on autoclick to
+ // select other click types.
+ actionButton = BUTTON_PRIMARY;
+ } else {
+ actionButton = mActiveClickType == AUTOCLICK_TYPE_RIGHT_CLICK
+ ? BUTTON_SECONDARY
+ : BUTTON_PRIMARY;
+ }
MotionEvent downEvent =
MotionEvent.obtain(
diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java
new file mode 100644
index 000000000000..86f79a83ea28
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.accessibility.autoclick;
+
+import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+
+import android.content.Context;
+import android.graphics.PixelFormat;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.internal.R;
+
+public class AutoclickScrollPanel {
+ private final Context mContext;
+ private final View mContentView;
+ private final WindowManager mWindowManager;
+ private boolean mInScrollMode = false;
+
+ public AutoclickScrollPanel(Context context, WindowManager windowManager) {
+ mContext = context;
+ mWindowManager = windowManager;
+ mContentView = LayoutInflater.from(context).inflate(
+ R.layout.accessibility_autoclick_scroll_panel, null);
+ }
+
+ /**
+ * Shows the autoclick scroll panel.
+ */
+ public void show() {
+ if (mInScrollMode) {
+ return;
+ }
+ mWindowManager.addView(mContentView, getLayoutParams());
+ mInScrollMode = true;
+ }
+
+ /**
+ * Hides the autoclick scroll panel.
+ */
+ public void hide() {
+ if (!mInScrollMode) {
+ return;
+ }
+ mWindowManager.removeView(mContentView);
+ mInScrollMode = false;
+ }
+
+ /**
+ * Retrieves the layout params for AutoclickScrollPanel, used when it's added to the Window
+ * Manager.
+ */
+ @NonNull
+ private WindowManager.LayoutParams getLayoutParams() {
+ final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
+ layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+ layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
+ layoutParams.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
+ layoutParams.setFitInsetsTypes(WindowInsets.Type.statusBars());
+ layoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+ layoutParams.format = PixelFormat.TRANSLUCENT;
+ layoutParams.setTitle(AutoclickScrollPanel.class.getSimpleName());
+ layoutParams.accessibilityTitle =
+ mContext.getString(R.string.accessibility_autoclick_scroll_panel_title);
+ layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
+ layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
+ layoutParams.gravity = Gravity.CENTER;
+ return layoutParams;
+ }
+
+ @VisibleForTesting
+ public boolean isVisible() {
+ return mInScrollMode;
+ }
+
+ @VisibleForTesting
+ public View getContentViewForTesting() {
+ return mContentView;
+ }
+
+ @VisibleForTesting
+ public WindowManager.LayoutParams getLayoutParamsForTesting() {
+ return getLayoutParams();
+ }
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
index fb329430acb2..b02fe2752a62 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
@@ -653,6 +653,14 @@ public class TouchExplorer extends BaseEventStreamTransformation
case ACTION_UP:
handleActionUp(event, rawEvent, policyFlags);
break;
+ case ACTION_POINTER_UP:
+ if (com.android.server.accessibility.Flags
+ .pointerUpMotionEventInTouchExploration()) {
+ if (mState.isServiceDetectingGestures()) {
+ mAms.sendMotionEventToListeningServices(rawEvent);
+ }
+ }
+ break;
default:
break;
}
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java
index cd46b38272c2..568abd196735 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java
@@ -26,6 +26,8 @@ import android.view.Display;
import android.view.MotionEvent;
import android.view.accessibility.AccessibilityEvent;
+import androidx.annotation.VisibleForTesting;
+
import com.android.server.accessibility.AccessibilityManagerService;
/**
@@ -73,7 +75,8 @@ public class TouchState {
private int mState = STATE_CLEAR;
// Helper class to track received pointers.
// Todo: collapse or hide this class so multiple classes don't modify it.
- private final ReceivedPointerTracker mReceivedPointerTracker;
+ @VisibleForTesting
+ public final ReceivedPointerTracker mReceivedPointerTracker;
// The most recently received motion event.
private MotionEvent mLastReceivedEvent;
// The accompanying raw event without any transformations.
@@ -219,8 +222,19 @@ public class TouchState {
startTouchInteracting();
break;
case AccessibilityEvent.TYPE_TOUCH_INTERACTION_END:
- setState(STATE_CLEAR);
- // We will clear when we actually handle the next ACTION_DOWN.
+ // When interaction ends, check if there are still down pointers.
+ // If there are any down pointers, go directly to TouchExploring instead.
+ if (com.android.server.accessibility.Flags
+ .pointerUpMotionEventInTouchExploration()) {
+ if (mReceivedPointerTracker.mReceivedPointersDown > 0) {
+ startTouchExploring();
+ } else {
+ setState(STATE_CLEAR);
+ // We will clear when we actually handle the next ACTION_DOWN.
+ }
+ } else {
+ setState(STATE_CLEAR);
+ }
break;
case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START:
startTouchExploring();
@@ -419,7 +433,8 @@ public class TouchState {
private final PointerDownInfo[] mReceivedPointers = new PointerDownInfo[MAX_POINTER_COUNT];
// Which pointers are down.
- private int mReceivedPointersDown;
+ @VisibleForTesting
+ public int mReceivedPointersDown;
// The edge flags of the last received down event.
private int mLastReceivedDownEdgeFlags;
diff --git a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
index 6ccf5e47ca6c..59566677b1fc 100644
--- a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
+++ b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
@@ -39,6 +39,14 @@ import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_
import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__DISPLAY_PRESENTATION_TYPE__INLINE;
import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__DISPLAY_PRESENTATION_TYPE__MENU;
import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__DISPLAY_PRESENTATION_TYPE__UNKNOWN_AUTOFILL_DISPLAY_PRESENTATION_TYPE;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_DELAY_AFTER_ANIMATION_END;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_FILL_DIALOG_DISABLED;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_LAST_TRIGGERED_ID_CHANGED;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_SCREEN_HAS_CREDMAN_FIELD;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_TIMEOUT_AFTER_DELAY;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_TIMEOUT_SINCE_IME_ANIMATED;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_UNKNOWN;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_WAIT_FOR_IME_ANIMATION;
import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__ANY_SHOWN;
import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_ACTIVITY_FINISHED;
import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_FILL_REQUEST_FAILED;
@@ -157,8 +165,24 @@ public final class PresentationStatsEventLogger {
DETECTION_PREFER_PCC
})
@Retention(RetentionPolicy.SOURCE)
- public @interface DetectionPreference {
- }
+ public @interface DetectionPreference {}
+
+ /**
+ * The fill dialog not shown reason. These are wrappers around
+ * {@link com.android.os.AtomsProto.AutofillPresentationEventReported.FillDialogNotShownReason}.
+ */
+ @IntDef(prefix = {"FILL_DIALOG_NOT_SHOWN_REASON"}, value = {
+ FILL_DIALOG_NOT_SHOWN_REASON_UNKNOWN,
+ FILL_DIALOG_NOT_SHOWN_REASON_FILL_DIALOG_DISABLED,
+ FILL_DIALOG_NOT_SHOWN_REASON_SCREEN_HAS_CREDMAN_FIELD,
+ FILL_DIALOG_NOT_SHOWN_REASON_LAST_TRIGGERED_ID_CHANGED,
+ FILL_DIALOG_NOT_SHOWN_REASON_WAIT_FOR_IME_ANIMATION,
+ FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_SINCE_IME_ANIMATED,
+ FILL_DIALOG_NOT_SHOWN_REASON_DELAY_AFTER_ANIMATION_END,
+ FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_AFTER_DELAY
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface FillDialogNotShownReason {}
public static final int NOT_SHOWN_REASON_ANY_SHOWN =
AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__ANY_SHOWN;
@@ -219,6 +243,25 @@ public final class PresentationStatsEventLogger {
public static final int DETECTION_PREFER_PCC =
AUTOFILL_FILL_RESPONSE_REPORTED__DETECTION_PREFERENCE__DETECTION_PREFER_PCC;
+ // Values for AutofillFillResponseReported.fill_dialog_not_shown_reason
+ public static final int FILL_DIALOG_NOT_SHOWN_REASON_UNKNOWN =
+ AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_UNKNOWN;
+ public static final int FILL_DIALOG_NOT_SHOWN_REASON_FILL_DIALOG_DISABLED =
+ AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_FILL_DIALOG_DISABLED;
+ public static final int FILL_DIALOG_NOT_SHOWN_REASON_SCREEN_HAS_CREDMAN_FIELD =
+ AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_SCREEN_HAS_CREDMAN_FIELD;
+ public static final int FILL_DIALOG_NOT_SHOWN_REASON_LAST_TRIGGERED_ID_CHANGED =
+ AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_LAST_TRIGGERED_ID_CHANGED;
+ public static final int FILL_DIALOG_NOT_SHOWN_REASON_WAIT_FOR_IME_ANIMATION =
+ AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_WAIT_FOR_IME_ANIMATION;
+ public static final int FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_SINCE_IME_ANIMATED =
+ AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_TIMEOUT_SINCE_IME_ANIMATED;
+ public static final int FILL_DIALOG_NOT_SHOWN_REASON_DELAY_AFTER_ANIMATION_END =
+ AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_DELAY_AFTER_ANIMATION_END;
+ public static final int FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_AFTER_DELAY =
+ AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_TIMEOUT_AFTER_DELAY;
+
+
private static final int DEFAULT_VALUE_INT = -1;
private final int mSessionId;
@@ -871,6 +914,43 @@ public final class PresentationStatsEventLogger {
}
/**
+ * Set fill_dialog_not_shown_reason
+ * @param reason
+ */
+ public void maybeSetFillDialogNotShownReason(@FillDialogNotShownReason int reason) {
+ mEventInternal.ifPresent(event -> {
+ if ((event.mFillDialogNotShownReason
+ == FILL_DIALOG_NOT_SHOWN_REASON_DELAY_AFTER_ANIMATION_END
+ || event.mFillDialogNotShownReason
+ == FILL_DIALOG_NOT_SHOWN_REASON_WAIT_FOR_IME_ANIMATION) && reason
+ == FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_SINCE_IME_ANIMATED) {
+ event.mFillDialogNotShownReason = FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_AFTER_DELAY;
+ } else {
+ event.mFillDialogNotShownReason = reason;
+ }
+ });
+ }
+
+ /**
+ * Set fill_dialog_ready_to_show_ms
+ * @param val
+ */
+ public void maybeSetFillDialogReadyToShowMs(long val) {
+ mEventInternal.ifPresent(event -> {
+ event.mFillDialogReadyToShowMs = (int) (val - mSessionStartTimestamp);
+ });
+ }
+
+ /**
+ * Set ime_animation_finish_ms
+ * @param val
+ */
+ public void maybeSetImeAnimationFinishMs(long val) {
+ mEventInternal.ifPresent(event -> {
+ event.mImeAnimationFinishMs = (int) (val - mSessionStartTimestamp);
+ });
+ }
+ /**
* Set the log contains relayout metrics.
* This is being added as a temporary measure to add logging.
* In future, when we map Session's old view states to the new autofill id's as part of fixing
@@ -959,7 +1039,13 @@ public final class PresentationStatsEventLogger {
+ " event.notExpiringResponseDuringAuthCount="
+ event.mFixExpireResponseDuringAuthCount
+ " event.notifyViewEnteredIgnoredDuringAuthCount="
- + event.mNotifyViewEnteredIgnoredDuringAuthCount);
+ + event.mNotifyViewEnteredIgnoredDuringAuthCount
+ + " event.fillDialogNotShownReason="
+ + event.mFillDialogNotShownReason
+ + " event.fillDialogReadyToShowMs="
+ + event.mFillDialogReadyToShowMs
+ + " event.imeAnimationFinishMs="
+ + event.mImeAnimationFinishMs);
}
// TODO(b/234185326): Distinguish empty responses from other no presentation reasons.
@@ -1020,7 +1106,10 @@ public final class PresentationStatsEventLogger {
event.mViewFilledSuccessfullyOnRefillCount,
event.mViewFailedOnRefillCount,
event.mFixExpireResponseDuringAuthCount,
- event.mNotifyViewEnteredIgnoredDuringAuthCount);
+ event.mNotifyViewEnteredIgnoredDuringAuthCount,
+ event.mFillDialogNotShownReason,
+ event.mFillDialogReadyToShowMs,
+ event.mImeAnimationFinishMs);
mEventInternal = Optional.empty();
}
@@ -1087,6 +1176,9 @@ public final class PresentationStatsEventLogger {
// Following are not logged and used only for internal logic
boolean shouldResetShownCount = false;
boolean mHasRelayoutLog = false;
+ @FillDialogNotShownReason int mFillDialogNotShownReason = FILL_DIALOG_NOT_SHOWN_REASON_UNKNOWN;
+ int mFillDialogReadyToShowMs = DEFAULT_VALUE_INT;
+ int mImeAnimationFinishMs = DEFAULT_VALUE_INT;
PresentationStatsEventInternal() {}
}
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 6fdb2b6b83f7..ff3bf2acb080 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -64,6 +64,7 @@ import static com.android.server.autofill.FillResponseEventLogger.DETECTION_PREF
import static com.android.server.autofill.FillResponseEventLogger.DETECTION_PREFER_PCC;
import static com.android.server.autofill.FillResponseEventLogger.DETECTION_PREFER_UNKNOWN;
import static com.android.server.autofill.FillResponseEventLogger.HAVE_SAVE_TRIGGER_ID;
+import static com.android.server.autofill.FillResponseEventLogger.RESPONSE_STATUS_CANCELLED;
import static com.android.server.autofill.FillResponseEventLogger.RESPONSE_STATUS_FAILURE;
import static com.android.server.autofill.FillResponseEventLogger.RESPONSE_STATUS_SESSION_DESTROYED;
import static com.android.server.autofill.FillResponseEventLogger.RESPONSE_STATUS_SUCCESS;
@@ -80,6 +81,13 @@ import static com.android.server.autofill.PresentationStatsEventLogger.AUTHENTIC
import static com.android.server.autofill.PresentationStatsEventLogger.AUTHENTICATION_RESULT_SUCCESS;
import static com.android.server.autofill.PresentationStatsEventLogger.AUTHENTICATION_TYPE_DATASET_AUTHENTICATION;
import static com.android.server.autofill.PresentationStatsEventLogger.AUTHENTICATION_TYPE_FULL_AUTHENTICATION;
+import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_DELAY_AFTER_ANIMATION_END;
+import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_FILL_DIALOG_DISABLED;
+import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_LAST_TRIGGERED_ID_CHANGED;
+import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_SCREEN_HAS_CREDMAN_FIELD;
+import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_SINCE_IME_ANIMATED;
+import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_UNKNOWN;
+import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_WAIT_FOR_IME_ANIMATION;
import static com.android.server.autofill.PresentationStatsEventLogger.NOT_SHOWN_REASON_ANY_SHOWN;
import static com.android.server.autofill.PresentationStatsEventLogger.NOT_SHOWN_REASON_NO_FOCUS;
import static com.android.server.autofill.PresentationStatsEventLogger.NOT_SHOWN_REASON_REQUEST_FAILED;
@@ -1416,6 +1424,15 @@ final class Session
// Remove the FillContext as there will never be a response for the service
if (canceledRequest != INVALID_REQUEST_ID && mContexts != null) {
+ // Start a new FillResponse logger for the cancellation case.
+ mFillResponseEventLogger.startLogForNewResponse();
+ mFillResponseEventLogger.maybeSetRequestId(canceledRequest);
+ mFillResponseEventLogger.maybeSetAppPackageUid(uid);
+ mFillResponseEventLogger.maybeSetResponseStatus(RESPONSE_STATUS_CANCELLED);
+ mFillResponseEventLogger.maybeSetLatencyFillResponseReceivedMillis(
+ (int) (SystemClock.elapsedRealtime() - mLatencyBaseTime));
+ mFillResponseEventLogger.logAndEndEvent();
+
final int numContexts = mContexts.size();
// It is most likely the last context, hence search backwards
@@ -5612,6 +5629,10 @@ final class Session
synchronized (mLock) {
final ViewState currentView = mViewStates.get(mCurrentViewId);
currentView.setState(ViewState.STATE_FILL_DIALOG_SHOWN);
+ // Set fill_dialog_not_shown_reason to unknown (a.k.a shown). It is needed due
+ // to possible SHOW_FILL_DIALOG_WAIT.
+ mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason(
+ FILL_DIALOG_NOT_SHOWN_REASON_UNKNOWN);
}
// Just show fill dialog once per fill request, so disabled after shown.
// Note: Cannot disable before requestShowFillDialog() because the method
@@ -5715,6 +5736,15 @@ final class Session
private boolean isFillDialogUiEnabled() {
synchronized (mLock) {
+ if (mSessionFlags.mFillDialogDisabled) {
+ mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason(
+ FILL_DIALOG_NOT_SHOWN_REASON_FILL_DIALOG_DISABLED);
+ }
+ if (mSessionFlags.mScreenHasCredmanField) {
+ // Prefer to log "HAS_CREDMAN_FIELD" over "FILL_DIALOG_DISABLED".
+ mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason(
+ FILL_DIALOG_NOT_SHOWN_REASON_SCREEN_HAS_CREDMAN_FIELD);
+ }
return !mSessionFlags.mFillDialogDisabled && !mSessionFlags.mScreenHasCredmanField;
}
}
@@ -5779,6 +5809,8 @@ final class Session
|| !ArrayUtils.contains(mLastFillDialogTriggerIds, filledId)) {
// Last fill dialog triggered ids are changed.
if (sDebug) Log.w(TAG, "Last fill dialog triggered ids are changed.");
+ mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason(
+ FILL_DIALOG_NOT_SHOWN_REASON_LAST_TRIGGERED_ID_CHANGED);
return SHOW_FILL_DIALOG_NO;
}
@@ -5805,6 +5837,8 @@ final class Session
// we need to wait for animation to happen. We can't return from here yet.
// This is the situation #2 described above.
Log.d(TAG, "Waiting for ime animation to complete before showing fill dialog");
+ mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason(
+ FILL_DIALOG_NOT_SHOWN_REASON_WAIT_FOR_IME_ANIMATION);
mFillDialogRunnable = createFillDialogEvalRunnable(
response, filledId, filterText, flags);
return SHOW_FILL_DIALOG_WAIT;
@@ -5814,9 +5848,15 @@ final class Session
// max of start input time or the ime finish time
long effectiveDuration = currentTimestampMs
- Math.max(mLastInputStartTime, mImeAnimationFinishTimeMs);
+ mPresentationStatsEventLogger.maybeSetFillDialogReadyToShowMs(
+ currentTimestampMs);
+ mPresentationStatsEventLogger.maybeSetImeAnimationFinishMs(
+ Math.max(mLastInputStartTime, mImeAnimationFinishTimeMs));
if (effectiveDuration >= mFillDialogTimeoutMs) {
Log.d(TAG, "Fill dialog not shown since IME has been up for more time than "
+ mFillDialogTimeoutMs + "ms");
+ mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason(
+ FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_SINCE_IME_ANIMATED);
return SHOW_FILL_DIALOG_NO;
} else if (effectiveDuration < mFillDialogMinWaitAfterImeAnimationMs) {
// we need to wait for some time after animation ends
@@ -5824,6 +5864,8 @@ final class Session
response, filledId, filterText, flags);
mHandler.postDelayed(runnable,
mFillDialogMinWaitAfterImeAnimationMs - effectiveDuration);
+ mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason(
+ FILL_DIALOG_NOT_SHOWN_REASON_DELAY_AFTER_ANIMATION_END);
return SHOW_FILL_DIALOG_WAIT;
}
}
diff --git a/services/backup/java/com/android/server/backup/BackupAgentConnectionManager.java b/services/backup/java/com/android/server/backup/BackupAgentConnectionManager.java
index 9a353fbc45bf..867cd51e1c2b 100644
--- a/services/backup/java/com/android/server/backup/BackupAgentConnectionManager.java
+++ b/services/backup/java/com/android/server/backup/BackupAgentConnectionManager.java
@@ -44,6 +44,7 @@ import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.LocalServices;
+import com.android.server.backup.BackupRestoreTask.CancellationReason;
import com.android.server.backup.internal.LifecycleOperationStorage;
import java.util.Set;
@@ -298,20 +299,22 @@ public class BackupAgentConnectionManager {
// Offload operation cancellation off the main thread as the cancellation callbacks
// might call out to BackupTransport. Other operations started on the same package
// before the cancellation callback has executed will also be cancelled by the callback.
- Runnable cancellationRunnable = () -> {
- // handleCancel() causes the PerformFullTransportBackupTask to go on to
- // tearDownAgentAndKill: that will unbindBackupAgent in the Activity Manager, so
- // that the package being backed up doesn't get stuck in restricted mode until the
- // backup time-out elapses.
- for (int token : mOperationStorage.operationTokensForPackage(packageName)) {
- if (DEBUG) {
- Slog.d(TAG,
- mUserIdMsg + "agentDisconnected: will handleCancel(all) for token:"
- + Integer.toHexString(token));
- }
- mUserBackupManagerService.handleCancel(token, true /* cancelAll */);
- }
- };
+ Runnable cancellationRunnable =
+ () -> {
+ // On handleCancel(), the operation will call unbindAgent() which will make
+ // sure the app doesn't get stuck in restricted mode.
+ for (int token : mOperationStorage.operationTokensForPackage(packageName)) {
+ if (DEBUG) {
+ Slog.d(
+ TAG,
+ mUserIdMsg
+ + "agentDisconnected: cancelling for token:"
+ + Integer.toHexString(token));
+ }
+ mUserBackupManagerService.handleCancel(
+ token, CancellationReason.AGENT_DISCONNECTED);
+ }
+ };
getThreadForCancellation(cancellationRunnable).start();
mAgentConnectLock.notifyAll();
diff --git a/services/backup/java/com/android/server/backup/BackupRestoreTask.java b/services/backup/java/com/android/server/backup/BackupRestoreTask.java
index acaab0c54191..7ec5f0d786ed 100644
--- a/services/backup/java/com/android/server/backup/BackupRestoreTask.java
+++ b/services/backup/java/com/android/server/backup/BackupRestoreTask.java
@@ -16,9 +16,12 @@
package com.android.server.backup;
-/**
- * Interface and methods used by the asynchronous-with-timeout backup/restore operations.
- */
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Interface and methods used by the asynchronous-with-timeout backup/restore operations. */
public interface BackupRestoreTask {
// Execute one tick of whatever state machine the task implements
@@ -27,6 +30,24 @@ public interface BackupRestoreTask {
// An operation that wanted a callback has completed
void operationComplete(long result);
- // An operation that wanted a callback has timed out
- void handleCancel(boolean cancelAll);
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ CancellationReason.TIMEOUT,
+ CancellationReason.AGENT_DISCONNECTED,
+ CancellationReason.EXTERNAL,
+ CancellationReason.SCHEDULED_JOB_STOPPED,
+ })
+ @interface CancellationReason {
+ // The task timed out.
+ int TIMEOUT = 0;
+ // The agent went away before the task was able to finish (e.g. due to an app crash).
+ int AGENT_DISCONNECTED = 1;
+ // An external caller cancelled the operation (e.g. via BackupManager#cancelBackups).
+ int EXTERNAL = 2;
+ // The job scheduler has stopped an ongoing scheduled backup pass.
+ int SCHEDULED_JOB_STOPPED = 3;
+ }
+
+ /** The task is cancelled for the given {@link CancellationReason}. */
+ void handleCancel(@CancellationReason int cancellationReason);
}
diff --git a/services/backup/java/com/android/server/backup/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
index 2143aaaa4cd6..b3af444ff9bd 100644
--- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
@@ -102,6 +102,7 @@ import com.android.internal.util.Preconditions;
import com.android.server.AppWidgetBackupBridge;
import com.android.server.EventLogTags;
import com.android.server.LocalServices;
+import com.android.server.backup.BackupRestoreTask.CancellationReason;
import com.android.server.backup.OperationStorage.OpState;
import com.android.server.backup.OperationStorage.OpType;
import com.android.server.backup.fullbackup.FullBackupEntry;
@@ -168,6 +169,7 @@ import java.util.Random;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.IntConsumer;
/** System service that performs backup/restore operations. */
public class UserBackupManagerService {
@@ -1816,11 +1818,9 @@ public class UserBackupManagerService {
for (Integer token : operationsToCancel) {
mOperationStorage.cancelOperation(
- token, /* cancelAll */
- true,
- operationType -> {
- /* no callback needed here */
- });
+ token,
+ operationType -> {}, // no callback needed here
+ CancellationReason.EXTERNAL);
}
// We don't want the backup jobs to kick in any time soon.
// Reschedules them to run in the distant future.
@@ -1897,19 +1897,17 @@ public class UserBackupManagerService {
}
/** Cancel the operation associated with {@code token}. */
- public void handleCancel(int token, boolean cancelAll) {
+ public void handleCancel(int token, @CancellationReason int cancellationReason) {
// Remove all pending timeout messages of types OpType.BACKUP_WAIT and
// OpType.RESTORE_WAIT. On the other hand, OP_TYPE_BACKUP cannot time out and
// doesn't require cancellation.
- mOperationStorage.cancelOperation(
- token,
- cancelAll,
- operationType -> {
- if (operationType == OpType.BACKUP_WAIT
- || operationType == OpType.RESTORE_WAIT) {
- mBackupHandler.removeMessages(getMessageIdForOperationType(operationType));
+ IntConsumer timeoutCallback =
+ opType -> {
+ if (opType == OpType.BACKUP_WAIT || opType == OpType.RESTORE_WAIT) {
+ mBackupHandler.removeMessages(getMessageIdForOperationType(opType));
}
- });
+ };
+ mOperationStorage.cancelOperation(token, timeoutCallback, cancellationReason);
}
/** Returns {@code true} if a backup is currently running, else returns {@code false}. */
@@ -2219,20 +2217,17 @@ public class UserBackupManagerService {
// offload the mRunningFullBackupTask.handleCancel() call to another thread,
// as we might have to wait for mCancelLock
Runnable endFullBackupRunnable =
- new Runnable() {
- @Override
- public void run() {
- PerformFullTransportBackupTask pftbt = null;
- synchronized (mQueueLock) {
- if (mRunningFullBackupTask != null) {
- pftbt = mRunningFullBackupTask;
- }
- }
- if (pftbt != null) {
- Slog.i(TAG, mLogIdMsg + "Telling running backup to stop");
- pftbt.handleCancel(true);
+ () -> {
+ PerformFullTransportBackupTask pftbt = null;
+ synchronized (mQueueLock) {
+ if (mRunningFullBackupTask != null) {
+ pftbt = mRunningFullBackupTask;
}
}
+ if (pftbt != null) {
+ Slog.i(TAG, mLogIdMsg + "Telling running backup to stop");
+ pftbt.handleCancel(CancellationReason.SCHEDULED_JOB_STOPPED);
+ }
};
new Thread(endFullBackupRunnable, "end-full-backup").start();
}
diff --git a/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java b/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java
index ebb1194c7c4a..b173f76e5f6f 100644
--- a/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java
+++ b/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java
@@ -25,6 +25,7 @@ import static com.android.server.backup.UserBackupManagerService.SHARED_BACKUP_A
import android.annotation.UserIdInt;
import android.app.ApplicationThreadConstants;
import android.app.IBackupAgent;
+import android.app.backup.BackupManagerMonitor;
import android.app.backup.BackupTransport;
import android.app.backup.FullBackupDataOutput;
import android.content.pm.ApplicationInfo;
@@ -268,6 +269,12 @@ public class FullBackupEngine {
mBackupManagerMonitorEventSender.monitorAgentLoggingResults(mPkg, mAgent);
} catch (IOException e) {
Slog.e(TAG, "Error backing up " + mPkg.packageName + ": " + e.getMessage());
+ // This is likely due to the app process dying.
+ mBackupManagerMonitorEventSender.monitorEvent(
+ BackupManagerMonitor.LOG_EVENT_ID_FULL_BACKUP_AGENT_PIPE_BROKEN,
+ mPkg,
+ BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT,
+ /* extras= */ null);
result = BackupTransport.AGENT_ERROR;
} finally {
try {
diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java
index 0d4364e14e03..7fc9ed3e0213 100644
--- a/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java
+++ b/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java
@@ -484,7 +484,7 @@ public class PerformAdbBackupTask extends FullBackupTask implements BackupRestor
}
@Override
- public void handleCancel(boolean cancelAll) {
+ public void handleCancel(@CancellationReason int cancellationReason) {
final PackageInfo target = mCurrentTarget;
Slog.w(TAG, "adb backup cancel of " + target);
if (target != null) {
diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
index c182c2618fdf..48d21c3a222f 100644
--- a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
+++ b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
@@ -162,7 +162,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba
// This is true when a backup operation for some package is in progress.
private volatile boolean mIsDoingBackup;
- private volatile boolean mCancelAll;
+ private volatile boolean mCancelled;
private final int mCurrentOpToken;
private final BackupAgentTimeoutParameters mAgentTimeoutParameters;
private final BackupEligibilityRules mBackupEligibilityRules;
@@ -199,7 +199,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba
if (backupManagerService.isBackupOperationInProgress()) {
Slog.d(TAG, "Skipping full backup. A backup is already in progress.");
- mCancelAll = true;
+ mCancelled = true;
return;
}
@@ -287,25 +287,31 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba
}
@Override
- public void handleCancel(boolean cancelAll) {
+ public void handleCancel(@CancellationReason int cancellationReason) {
synchronized (mCancelLock) {
- // We only support 'cancelAll = true' case for this task. Cancelling of a single package
-
- // due to timeout is handled by SinglePackageBackupRunner and
+ // This callback is only used for cancelling the entire backup operation. Cancelling of
+ // a single package due to timeout is handled by SinglePackageBackupRunner and
// SinglePackageBackupPreflight.
+ if (cancellationReason == CancellationReason.TIMEOUT) {
+ Slog.wtf(TAG, "This task cannot time out");
+ return;
+ }
- if (!cancelAll) {
- Slog.wtf(TAG, "Expected cancelAll to be true.");
+ // We don't cancel the entire operation if a single agent is disconnected unexpectedly.
+ // SinglePackageBackupRunner and SinglePackageBackupPreflight will receive the same
+ // callback and fail gracefully. The operation should then continue to the next package.
+ if (cancellationReason == CancellationReason.AGENT_DISCONNECTED) {
+ return;
}
- if (mCancelAll) {
+ if (mCancelled) {
Slog.d(TAG, "Ignoring duplicate cancel call.");
return;
}
- mCancelAll = true;
+ mCancelled = true;
if (mIsDoingBackup) {
- mUserBackupManagerService.handleCancel(mBackupRunnerOpToken, cancelAll);
+ mUserBackupManagerService.handleCancel(mBackupRunnerOpToken, cancellationReason);
try {
// If we're running a backup we should be connected to a transport
BackupTransportClient transport =
@@ -410,7 +416,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba
int backupPackageStatus;
long quota = Long.MAX_VALUE;
synchronized (mCancelLock) {
- if (mCancelAll) {
+ if (mCancelled) {
break;
}
backupPackageStatus = transport.performFullBackup(currentPackage,
@@ -478,7 +484,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba
if (nRead > 0) {
out.write(buffer, 0, nRead);
synchronized (mCancelLock) {
- if (!mCancelAll) {
+ if (!mCancelled) {
backupPackageStatus = transport.sendBackupData(nRead);
}
}
@@ -509,7 +515,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba
synchronized (mCancelLock) {
mIsDoingBackup = false;
// If mCancelCurrent is true, we have already called cancelFullBackup().
- if (!mCancelAll) {
+ if (!mCancelled) {
if (backupRunnerResult == BackupTransport.TRANSPORT_OK) {
// If we were otherwise in a good state, now interpret the final
// result based on what finishBackup() returns. If we're in a
@@ -607,7 +613,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba
.sendBackupOnPackageResult(mBackupObserver, packageName,
BackupManager.ERROR_BACKUP_CANCELLED);
Slog.w(TAG, "Backup cancelled. package=" + packageName +
- ", cancelAll=" + mCancelAll);
+ ", entire session cancelled=" + mCancelled);
EventLog.writeEvent(EventLogTags.FULL_BACKUP_CANCELLED, packageName);
mUserBackupManagerService.getBackupAgentConnectionManager().unbindAgent(
currentPackage.applicationInfo, /* allowKill= */ true);
@@ -654,7 +660,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba
} finally {
- if (mCancelAll) {
+ if (mCancelled) {
backupRunStatus = BackupManager.ERROR_BACKUP_CANCELLED;
}
@@ -820,7 +826,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba
}
@Override
- public void handleCancel(boolean cancelAll) {
+ public void handleCancel(@CancellationReason int cancellationReason) {
if (DEBUG) {
Slog.i(TAG, "Preflight cancelled; failing");
}
@@ -974,7 +980,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba
public void operationComplete(long result) { /* intentionally empty */ }
@Override
- public void handleCancel(boolean cancelAll) {
+ public void handleCancel(@CancellationReason int cancellationReason) {
Slog.w(TAG, "Full backup cancel of " + mTarget.packageName);
mBackupManagerMonitorEventSender.monitorEvent(
@@ -984,7 +990,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba
/* extras= */ null);
mIsCancelled = true;
// Cancel tasks spun off by this task.
- mUserBackupManagerService.handleCancel(mEphemeralToken, cancelAll);
+ mUserBackupManagerService.handleCancel(mEphemeralToken, cancellationReason);
mUserBackupManagerService.getBackupAgentConnectionManager().unbindAgent(
mTarget.applicationInfo, /* allowKill= */ true);
// Free up everyone waiting on this task and its children.
diff --git a/services/backup/java/com/android/server/backup/internal/BackupHandler.java b/services/backup/java/com/android/server/backup/internal/BackupHandler.java
index 87cf8a313651..464dc2dfe1ec 100644
--- a/services/backup/java/com/android/server/backup/internal/BackupHandler.java
+++ b/services/backup/java/com/android/server/backup/internal/BackupHandler.java
@@ -34,6 +34,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.server.EventLogTags;
import com.android.server.backup.BackupAgentTimeoutParameters;
import com.android.server.backup.BackupRestoreTask;
+import com.android.server.backup.BackupRestoreTask.CancellationReason;
import com.android.server.backup.DataChangedJournal;
import com.android.server.backup.OperationStorage;
import com.android.server.backup.TransportManager;
@@ -410,8 +411,8 @@ public class BackupHandler extends Handler {
case MSG_BACKUP_OPERATION_TIMEOUT:
case MSG_RESTORE_OPERATION_TIMEOUT: {
- Slog.d(TAG, "Timeout message received for token=" + Integer.toHexString(msg.arg1));
- backupManagerService.handleCancel(msg.arg1, false);
+ Slog.d(TAG, "Timeout for token=" + Integer.toHexString(msg.arg1));
+ backupManagerService.handleCancel(msg.arg1, CancellationReason.TIMEOUT);
break;
}
diff --git a/services/backup/java/com/android/server/backup/internal/LifecycleOperationStorage.java b/services/backup/java/com/android/server/backup/internal/LifecycleOperationStorage.java
index 0b974e2d0a8a..5aacb2f4f007 100644
--- a/services/backup/java/com/android/server/backup/internal/LifecycleOperationStorage.java
+++ b/services/backup/java/com/android/server/backup/internal/LifecycleOperationStorage.java
@@ -24,6 +24,7 @@ import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.server.backup.BackupRestoreTask;
+import com.android.server.backup.BackupRestoreTask.CancellationReason;
import com.android.server.backup.OperationStorage;
import com.google.android.collect.Sets;
@@ -296,20 +297,18 @@ public class LifecycleOperationStorage implements OperationStorage {
}
/**
- * Cancel the operation associated with {@code token}. Cancellation may be
- * propagated to the operation's callback (a {@link BackupRestoreTask}) if
- * the operation has one, and the cancellation is due to the operation
- * timing out.
+ * Cancel the operation associated with {@code token}. Cancellation may be propagated to the
+ * operation's callback (a {@link BackupRestoreTask}) if the operation has one, and the
+ * cancellation is due to the operation timing out.
*
* @param token the operation token specified when registering the operation
- * @param cancelAll this is passed on when propagating the cancellation
- * @param operationTimedOutCallback a lambda that is invoked with the
- * operation type where the operation is
- * cancelled due to timeout, allowing the
- * caller to do type-specific clean-ups.
+ * @param operationTimedOutCallback a lambda that is invoked with the operation type where the
+ * operation is cancelled due to timeout, allowing the caller to do type-specific clean-ups.
*/
public void cancelOperation(
- int token, boolean cancelAll, IntConsumer operationTimedOutCallback) {
+ int token,
+ IntConsumer operationTimedOutCallback,
+ @CancellationReason int cancellationReason) {
// Notify any synchronous waiters
Operation op = null;
synchronized (mOperationsLock) {
@@ -343,7 +342,7 @@ public class LifecycleOperationStorage implements OperationStorage {
if (DEBUG) {
Slog.v(TAG, "[UserID:" + mUserId + " Invoking cancel on " + op.callback);
}
- op.callback.handleCancel(cancelAll);
+ op.callback.handleCancel(cancellationReason);
}
}
}
diff --git a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java
index 494b9d59a238..8e7a23ccbb25 100644
--- a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java
+++ b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java
@@ -171,7 +171,7 @@ import java.util.concurrent.atomic.AtomicInteger;
* complete backup should be performed.
*
* <p>This task is designed to run on a dedicated thread, with the exception of the {@link
- * #handleCancel(boolean)} method, which can be called from any thread.
+ * BackupRestoreTask#handleCancel(int)} method, which can be called from any thread.
*/
// TODO: Stop poking into BMS state and doing things for it (e.g. synchronizing on public locks)
// TODO: Consider having the caller responsible for some clean-up (like resetting state)
@@ -1208,13 +1208,13 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable {
*
* <p>Note: This method is inherently racy since there are no guarantees about how much of the
* task will be executed after you made the call.
- *
- * @param cancelAll MUST be {@code true}. Will be removed.
*/
@Override
- public void handleCancel(boolean cancelAll) {
+ public void handleCancel(@CancellationReason int cancellationReason) {
// This is called in a thread different from the one that executes method run().
- Preconditions.checkArgument(cancelAll, "Can't partially cancel a key-value backup task");
+ Preconditions.checkArgument(
+ cancellationReason != CancellationReason.TIMEOUT,
+ "Key-value backup task cannot time out");
markCancel();
waitCancel();
}
diff --git a/services/backup/java/com/android/server/backup/restore/AdbRestoreFinishedLatch.java b/services/backup/java/com/android/server/backup/restore/AdbRestoreFinishedLatch.java
index cb491c6f384e..f1829b6966a8 100644
--- a/services/backup/java/com/android/server/backup/restore/AdbRestoreFinishedLatch.java
+++ b/services/backup/java/com/android/server/backup/restore/AdbRestoreFinishedLatch.java
@@ -79,7 +79,7 @@ public class AdbRestoreFinishedLatch implements BackupRestoreTask {
}
@Override
- public void handleCancel(boolean cancelAll) {
+ public void handleCancel(@CancellationReason int cancellationReason) {
Slog.w(TAG, "adb onRestoreFinished() timed out");
mLatch.countDown();
mOperationStorage.removeOperation(mCurrentOpToken);
diff --git a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
index 707ae03b3964..20f103cdfab4 100644
--- a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
+++ b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
@@ -589,6 +589,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
monitoringExtras);
Slog.e(TAG, "Failure getting next package name");
EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE);
+ mStatus = BackupTransport.TRANSPORT_ERROR;
nextState = UnifiedRestoreState.FINAL;
return;
} else if (mRestoreDescription == RestoreDescription.NO_MORE_PACKAGES) {
@@ -1307,7 +1308,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
// The app has timed out handling a restoring file
@Override
- public void handleCancel(boolean cancelAll) {
+ public void handleCancel(@CancellationReason int cancellationReason) {
mOperationStorage.removeOperation(mEphemeralOpToken);
Slog.w(TAG, "Full-data restore target timed out; shutting down");
Bundle monitoringExtras = addRestoreOperationTypeToEvent(/* extras= */ null);
@@ -1555,7 +1556,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
// A call to agent.doRestore() or agent.doRestoreFinished() has timed out
@Override
- public void handleCancel(boolean cancelAll) {
+ public void handleCancel(@CancellationReason int cancellationReason) {
mOperationStorage.removeOperation(mEphemeralOpToken);
Slog.e(TAG, "Timeout restoring application " + mCurrentPackage.packageName);
Bundle monitoringExtras = addRestoreOperationTypeToEvent(/* extras= */ null);
diff --git a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtils.java b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtils.java
index fad59d23a6dc..855c72acd7ca 100644
--- a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtils.java
+++ b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtils.java
@@ -389,6 +389,8 @@ public class BackupManagerMonitorDumpsysUtils {
"Agent failure during restore";
case BackupManagerMonitor.LOG_EVENT_ID_FAILED_TO_READ_DATA_FROM_TRANSPORT ->
"Failed to read data from Transport";
+ case BackupManagerMonitor.LOG_EVENT_ID_FULL_BACKUP_AGENT_PIPE_BROKEN ->
+ "LOG_EVENT_ID_FULL_BACKUP_AGENT_PIPE_BROKEN";
default -> "Unknown log event ID: " + code;
};
return id;
diff --git a/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java
index cd9285cdfe91..cbee8391458d 100644
--- a/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java
+++ b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java
@@ -58,13 +58,16 @@ import android.os.Handler;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.UserHandle;
+import android.util.ArraySet;
import android.util.Slog;
import com.android.internal.R;
import com.android.server.companion.CompanionDeviceManagerService;
import com.android.server.companion.utils.PackageUtils;
+import java.util.Arrays;
import java.util.List;
+import java.util.Set;
/**
* Class responsible for handling incoming {@link AssociationRequest}s.
@@ -130,6 +133,12 @@ public class AssociationRequestsProcessor {
private static final int ASSOCIATE_WITHOUT_PROMPT_MAX_PER_TIME_WINDOW = 5;
private static final long ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS = 60 * 60 * 1000; // 60 min;
+ // Set of profiles for which the association dialog cannot be skipped.
+ private static final Set<String> DEVICE_PROFILES_WITH_REQUIRED_CONFIRMATION = new ArraySet<>(
+ Arrays.asList(
+ AssociationRequest.DEVICE_PROFILE_APP_STREAMING,
+ AssociationRequest.DEVICE_PROFILE_NEARBY_DEVICE_STREAMING));
+
private final @NonNull Context mContext;
private final @NonNull PackageManagerInternal mPackageManagerInternal;
private final @NonNull AssociationStore mAssociationStore;
@@ -174,6 +183,7 @@ public class AssociationRequestsProcessor {
// 2a. Check if association can be created without launching UI (i.e. CDM needs NEITHER
// to perform discovery NOR to collect user consent).
if (request.isSelfManaged() && !request.isForceConfirmation()
+ && !DEVICE_PROFILES_WITH_REQUIRED_CONFIRMATION.contains(request.getDeviceProfile())
&& !willAddRoleHolder(request, packageName, userId)) {
// 2a.1. Create association right away.
createAssociationAndNotifyApplication(request, packageName, userId,
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index 93b4de856463..caf535ce7a40 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -142,14 +142,6 @@ public class VirtualDeviceManagerService extends SystemService {
@GuardedBy("mVirtualDeviceManagerLock")
private ArrayMap<String, AssociationInfo> mActiveAssociations = new ArrayMap<>();
- private final CompanionDeviceManager.OnAssociationsChangedListener mCdmAssociationListener =
- new CompanionDeviceManager.OnAssociationsChangedListener() {
- @Override
- public void onAssociationsChanged(@NonNull List<AssociationInfo> associations) {
- syncVirtualDevicesToCdmAssociations(associations);
- }
- };
-
private class StrongAuthTracker extends LockPatternUtils.StrongAuthTracker {
final Set<Integer> mUsersInLockdown = new ArraySet<>();
@@ -348,33 +340,6 @@ public class VirtualDeviceManagerService extends SystemService {
return true;
}
- private void syncVirtualDevicesToCdmAssociations(List<AssociationInfo> associations) {
- Set<VirtualDeviceImpl> virtualDevicesToRemove = new HashSet<>();
- synchronized (mVirtualDeviceManagerLock) {
- if (mVirtualDevices.size() == 0) {
- return;
- }
-
- Set<Integer> activeAssociationIds = new HashSet<>(associations.size());
- for (AssociationInfo association : associations) {
- activeAssociationIds.add(association.getId());
- }
-
- for (int i = 0; i < mVirtualDevices.size(); i++) {
- VirtualDeviceImpl virtualDevice = mVirtualDevices.valueAt(i);
- int deviceAssociationId = virtualDevice.getAssociationId();
- if (deviceAssociationId != CDM_ASSOCIATION_ID_NONE
- && !activeAssociationIds.contains(deviceAssociationId)) {
- virtualDevicesToRemove.add(virtualDevice);
- }
- }
- }
-
- for (VirtualDeviceImpl virtualDevice : virtualDevicesToRemove) {
- virtualDevice.close();
- }
- }
-
void onCdmAssociationsChanged(List<AssociationInfo> associations) {
ArrayMap<String, AssociationInfo> vdmAssociations = new ArrayMap<>();
for (int i = 0; i < associations.size(); ++i) {
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 14d9d3f0c0a1..decac40d20f8 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -122,10 +122,10 @@ genrule {
}
genrule {
- name: "statslog-mediarouter-java-gen",
- tools: ["stats-log-api-gen"],
- cmd: "$(location stats-log-api-gen) --java $(out) --module mediarouter --javaPackage com.android.server.media --javaClass MediaRouterStatsLog",
- out: ["com/android/server/media/MediaRouterStatsLog.java"],
+ name: "statslog-mediarouter-java-gen",
+ tools: ["stats-log-api-gen"],
+ cmd: "$(location stats-log-api-gen) --java $(out) --module mediarouter --javaPackage com.android.server.media --javaClass MediaRouterStatsLog",
+ out: ["com/android/server/media/MediaRouterStatsLog.java"],
}
java_library_static {
@@ -138,6 +138,7 @@ java_library_static {
"ondeviceintelligence_conditionally",
],
srcs: [
+ ":android.hardware.audio.effect-V1-java-source",
":android.hardware.tv.hdmi.connection-V1-java-source",
":android.hardware.tv.hdmi.earc-V1-java-source",
":android.hardware.tv.mediaquality-V1-java-source",
diff --git a/services/core/java/com/android/server/Watchdog.java b/services/core/java/com/android/server/Watchdog.java
index 96b30d4e1285..d60c6c534871 100644
--- a/services/core/java/com/android/server/Watchdog.java
+++ b/services/core/java/com/android/server/Watchdog.java
@@ -165,7 +165,6 @@ public class Watchdog implements Dumpable {
"android.hardware.sensors@1.0::ISensors",
"android.hardware.sensors@2.0::ISensors",
"android.hardware.sensors@2.1::ISensors",
- "android.hardware.vibrator@1.0::IVibrator",
"android.hardware.vr@1.0::IVr",
"android.system.suspend@1.0::ISystemSuspend"
);
diff --git a/services/core/java/com/android/server/adb/AdbDebuggingManager.java b/services/core/java/com/android/server/adb/AdbDebuggingManager.java
index a73a991bc6a6..658ea4c27e4c 100644
--- a/services/core/java/com/android/server/adb/AdbDebuggingManager.java
+++ b/services/core/java/com/android/server/adb/AdbDebuggingManager.java
@@ -130,8 +130,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
*/
public class AdbDebuggingManager {
private static final String TAG = AdbDebuggingManager.class.getSimpleName();
- private static final boolean DEBUG = false;
- private static final boolean MDNS_DEBUG = false;
private static final String ADBD_SOCKET = "adbd";
private static final String ADB_DIRECTORY = "misc/adb";
@@ -156,8 +154,6 @@ public class AdbDebuggingManager {
@Nullable private final File mUserKeyFile;
@Nullable private final File mTempKeysFile;
- private static final String WIFI_PERSISTENT_CONFIG_PROPERTY =
- "persist.adb.tls_server.enable";
private static final String WIFI_PERSISTENT_GUID =
"persist.adb.wifi.guid";
private static final int PAIRING_CODE_LENGTH = 6;
@@ -261,12 +257,10 @@ public class AdbDebuggingManager {
mHandler.sendMessage(msg);
boolean paired = native_pairing_wait();
- if (DEBUG) {
- if (mPublicKey != null) {
- Slog.i(TAG, "Pairing succeeded key=" + mPublicKey);
- } else {
- Slog.i(TAG, "Pairing failed");
- }
+ if (mPublicKey != null) {
+ Slog.i(TAG, "Pairing succeeded key=" + mPublicKey);
+ } else {
+ Slog.i(TAG, "Pairing failed");
}
mNsdManager.unregisterService(this);
@@ -307,7 +301,7 @@ public class AdbDebuggingManager {
@Override
public void onServiceRegistered(NsdServiceInfo serviceInfo) {
- if (MDNS_DEBUG) Slog.i(TAG, "Registered pairing service: " + serviceInfo);
+ Slog.i(TAG, "Registered pairing service: " + serviceInfo);
}
@Override
@@ -319,7 +313,7 @@ public class AdbDebuggingManager {
@Override
public void onServiceUnregistered(NsdServiceInfo serviceInfo) {
- if (MDNS_DEBUG) Slog.i(TAG, "Unregistered pairing service: " + serviceInfo);
+ Slog.i(TAG, "Unregistered pairing service: " + serviceInfo);
}
@Override
@@ -354,7 +348,7 @@ public class AdbDebuggingManager {
@Override
public void run() {
- if (DEBUG) Slog.d(TAG, "Starting adb port property poller");
+ Slog.d(TAG, "Starting adb port property poller");
// Once adbwifi is enabled, we poll the service.adb.tls.port
// system property until we get the port, or -1 on failure.
// Let's also limit the polling to 10 seconds, just in case
@@ -390,7 +384,7 @@ public class AdbDebuggingManager {
class PortListenerImpl implements AdbConnectionPortListener {
public void onPortReceived(int port) {
- if (DEBUG) Slog.d(TAG, "Received tls port=" + port);
+ Slog.d(TAG, "Received tls port=" + port);
Message msg = mHandler.obtainMessage(port > 0
? AdbDebuggingHandler.MSG_SERVER_CONNECTED
: AdbDebuggingHandler.MSG_SERVER_DISCONNECTED);
@@ -419,11 +413,11 @@ public class AdbDebuggingManager {
@Override
public void run() {
- if (DEBUG) Slog.d(TAG, "Entering thread");
+ Slog.d(TAG, "Entering thread");
while (true) {
synchronized (this) {
if (mStopped) {
- if (DEBUG) Slog.d(TAG, "Exiting thread");
+ Slog.d(TAG, "Exiting thread");
return;
}
try {
@@ -448,7 +442,7 @@ public class AdbDebuggingManager {
LocalSocketAddress.Namespace.RESERVED);
mInputStream = null;
- if (DEBUG) Slog.d(TAG, "Creating socket");
+ Slog.d(TAG, "Creating socket");
mSocket = new LocalSocket(LocalSocket.SOCKET_SEQPACKET);
mSocket.connect(address);
@@ -549,7 +543,7 @@ public class AdbDebuggingManager {
}
private void closeSocketLocked() {
- if (DEBUG) Slog.d(TAG, "Closing socket");
+ Slog.d(TAG, "Closing socket");
try {
if (mOutputStream != null) {
mOutputStream.close();
@@ -859,7 +853,7 @@ public class AdbDebuggingManager {
private void startAdbDebuggingThread() {
++mAdbEnabledRefCount;
- if (DEBUG) Slog.i(TAG, "startAdbDebuggingThread ref=" + mAdbEnabledRefCount);
+ Slog.i(TAG, "startAdbDebuggingThread ref=" + mAdbEnabledRefCount);
if (mAdbEnabledRefCount > 1) {
return;
}
@@ -875,7 +869,7 @@ public class AdbDebuggingManager {
private void stopAdbDebuggingThread() {
--mAdbEnabledRefCount;
- if (DEBUG) Slog.i(TAG, "stopAdbDebuggingThread ref=" + mAdbEnabledRefCount);
+ Slog.i(TAG, "stopAdbDebuggingThread ref=" + mAdbEnabledRefCount);
if (mAdbEnabledRefCount > 0) {
return;
}
@@ -1093,7 +1087,7 @@ public class AdbDebuggingManager {
intentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
mContext.registerReceiver(mBroadcastReceiver, intentFilter);
- SystemProperties.set(WIFI_PERSISTENT_CONFIG_PROPERTY, "1");
+ SystemProperties.set(AdbService.WIFI_PERSISTENT_CONFIG_PROPERTY, "1");
mConnectionPortPoller =
new AdbDebuggingManager.AdbConnectionPortPoller(mPortListener);
mConnectionPortPoller.start();
@@ -1101,7 +1095,7 @@ public class AdbDebuggingManager {
startAdbDebuggingThread();
mAdbWifiEnabled = true;
- if (DEBUG) Slog.i(TAG, "adb start wireless adb");
+ Slog.i(TAG, "adb start wireless adb");
break;
}
case MSG_ADBDWIFI_DISABLE:
@@ -1143,7 +1137,7 @@ public class AdbDebuggingManager {
intentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
mContext.registerReceiver(mBroadcastReceiver, intentFilter);
- SystemProperties.set(WIFI_PERSISTENT_CONFIG_PROPERTY, "1");
+ SystemProperties.set(AdbService.WIFI_PERSISTENT_CONFIG_PROPERTY, "1");
mConnectionPortPoller =
new AdbDebuggingManager.AdbConnectionPortPoller(mPortListener);
mConnectionPortPoller.start();
@@ -1151,7 +1145,7 @@ public class AdbDebuggingManager {
startAdbDebuggingThread();
mAdbWifiEnabled = true;
- if (DEBUG) Slog.i(TAG, "adb start wireless adb");
+ Slog.i(TAG, "adb start wireless adb");
break;
case MSG_ADBWIFI_DENY:
Settings.Global.putInt(mContentResolver,
@@ -1259,7 +1253,7 @@ public class AdbDebuggingManager {
break;
}
case MSG_ADBD_SOCKET_CONNECTED: {
- if (DEBUG) Slog.d(TAG, "adbd socket connected");
+ Slog.d(TAG, "adbd socket connected");
if (mAdbWifiEnabled) {
// In scenarios where adbd is restarted, the tls port may change.
mConnectionPortPoller =
@@ -1269,7 +1263,7 @@ public class AdbDebuggingManager {
break;
}
case MSG_ADBD_SOCKET_DISCONNECTED: {
- if (DEBUG) Slog.d(TAG, "adbd socket disconnected");
+ Slog.d(TAG, "adbd socket disconnected");
if (mConnectionPortPoller != null) {
mConnectionPortPoller.cancelAndWait();
mConnectionPortPoller = null;
@@ -1477,7 +1471,7 @@ public class AdbDebuggingManager {
}
private void updateUIPairCode(String code) {
- if (DEBUG) Slog.i(TAG, "updateUIPairCode: " + code);
+ Slog.i(TAG, "updateUIPairCode: " + code);
Intent intent = new Intent(AdbManager.WIRELESS_DEBUG_PAIRING_RESULT_ACTION);
intent.putExtra(AdbManager.WIRELESS_PAIRING_CODE_EXTRA, code);
diff --git a/services/core/java/com/android/server/adb/AdbService.java b/services/core/java/com/android/server/adb/AdbService.java
index 55d8dba69626..aae48daa5dde 100644
--- a/services/core/java/com/android/server/adb/AdbService.java
+++ b/services/core/java/com/android/server/adb/AdbService.java
@@ -222,15 +222,14 @@ public class AdbService extends IAdbManager.Stub {
}
}
- private static final String TAG = "AdbService";
- private static final boolean DEBUG = false;
+ private static final String TAG = AdbService.class.getSimpleName();
/**
* The persistent property which stores whether adb is enabled or not.
* May also contain vendor-specific default functions for testing purposes.
*/
private static final String USB_PERSISTENT_CONFIG_PROPERTY = "persist.sys.usb.config";
- private static final String WIFI_PERSISTENT_CONFIG_PROPERTY = "persist.adb.tls_server.enable";
+ static final String WIFI_PERSISTENT_CONFIG_PROPERTY = "persist.adb.tls_server.enable";
private final Context mContext;
private final ContentResolver mContentResolver;
@@ -256,7 +255,7 @@ public class AdbService extends IAdbManager.Stub {
* SystemServer}.
*/
public void systemReady() {
- if (DEBUG) Slog.d(TAG, "systemReady");
+ Slog.d(TAG, "systemReady");
/*
* Use the normal bootmode persistent prop to maintain state of adb across
@@ -287,7 +286,7 @@ public class AdbService extends IAdbManager.Stub {
* Called in response to {@code SystemService.PHASE_BOOT_COMPLETED} from {@code SystemServer}.
*/
public void bootCompleted() {
- if (DEBUG) Slog.d(TAG, "boot completed");
+ Slog.d(TAG, "boot completed");
if (mDebuggingManager != null) {
mDebuggingManager.setAdbEnabled(mIsAdbUsbEnabled, AdbTransportType.USB);
mDebuggingManager.setAdbEnabled(mIsAdbWifiEnabled, AdbTransportType.WIFI);
@@ -429,17 +428,13 @@ public class AdbService extends IAdbManager.Stub {
@Override
public void registerCallback(IAdbCallback callback) throws RemoteException {
- if (DEBUG) {
- Slog.d(TAG, "Registering callback " + callback);
- }
+ Slog.d(TAG, "Registering callback " + callback);
mCallbacks.register(callback);
}
@Override
public void unregisterCallback(IAdbCallback callback) throws RemoteException {
- if (DEBUG) {
- Slog.d(TAG, "Unregistering callback " + callback);
- }
+ Slog.d(TAG, "Unregistering callback " + callback);
mCallbacks.unregister(callback);
}
/**
@@ -500,11 +495,8 @@ public class AdbService extends IAdbManager.Stub {
}
private void setAdbEnabled(boolean enable, byte transportType) {
- if (DEBUG) {
- Slog.d(TAG, "setAdbEnabled(" + enable + "), mIsAdbUsbEnabled=" + mIsAdbUsbEnabled
- + ", mIsAdbWifiEnabled=" + mIsAdbWifiEnabled + ", transportType="
- + transportType);
- }
+ Slog.d(TAG, "setAdbEnabled(" + enable + "), mIsAdbUsbEnabled=" + mIsAdbUsbEnabled
+ + ", mIsAdbWifiEnabled=" + mIsAdbWifiEnabled + ", transportType=" + transportType);
if (transportType == AdbTransportType.USB && enable != mIsAdbUsbEnabled) {
mIsAdbUsbEnabled = enable;
@@ -549,20 +541,14 @@ public class AdbService extends IAdbManager.Stub {
mDebuggingManager.setAdbEnabled(enable, transportType);
}
- if (DEBUG) {
- Slog.d(TAG, "Broadcasting enable = " + enable + ", type = " + transportType);
- }
+ Slog.d(TAG, "Broadcasting enable = " + enable + ", type = " + transportType);
mCallbacks.broadcast((callback) -> {
- if (DEBUG) {
- Slog.d(TAG, "Sending enable = " + enable + ", type = " + transportType
- + " to " + callback);
- }
+ Slog.d(TAG, "Sending enable = " + enable + ", type = " + transportType + " to "
+ + callback);
try {
callback.onDebuggingChanged(enable, transportType);
} catch (RemoteException ex) {
- if (DEBUG) {
- Slog.d(TAG, "Unable to send onDebuggingChanged:", ex);
- }
+ Slog.w(TAG, "Unable to send onDebuggingChanged:", ex);
}
});
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index b0b34d0ab9c4..76ba0054583b 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -16190,14 +16190,16 @@ public class ActivityManagerService extends IActivityManager.Stub
return mUserController.switchUser(targetUserId);
}
+ @Nullable
@Override
- public String getSwitchingFromUserMessage() {
- return mUserController.getSwitchingFromSystemUserMessage();
+ public String getSwitchingFromUserMessage(@UserIdInt int userId) {
+ return mUserController.getSwitchingFromUserMessage(userId);
}
+ @Nullable
@Override
- public String getSwitchingToUserMessage() {
- return mUserController.getSwitchingToSystemUserMessage();
+ public String getSwitchingToUserMessage(@UserIdInt int userId) {
+ return mUserController.getSwitchingToUserMessage(userId);
}
@Override
@@ -16938,13 +16940,13 @@ public class ActivityManagerService extends IActivityManager.Stub
}
@Override
- public void setSwitchingFromSystemUserMessage(String switchingFromSystemUserMessage) {
- mUserController.setSwitchingFromSystemUserMessage(switchingFromSystemUserMessage);
+ public void setSwitchingFromUserMessage(@UserIdInt int userId, @Nullable String message) {
+ mUserController.setSwitchingFromUserMessage(userId, message);
}
@Override
- public void setSwitchingToSystemUserMessage(String switchingToSystemUserMessage) {
- mUserController.setSwitchingToSystemUserMessage(switchingToSystemUserMessage);
+ public void setSwitchingToUserMessage(@UserIdInt int userId, @Nullable String message) {
+ mUserController.setSwitchingToUserMessage(userId, message);
}
@Override
diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
index db0562f5750a..508c01802156 100644
--- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
@@ -810,7 +810,7 @@ class BroadcastProcessQueue {
* Return the broadcast being actively dispatched in this process.
*/
public @NonNull BroadcastRecord getActive() {
- return Objects.requireNonNull(mActive);
+ return Objects.requireNonNull(mActive, toString());
}
/**
@@ -818,7 +818,7 @@ class BroadcastProcessQueue {
* being actively dispatched in this process.
*/
public int getActiveIndex() {
- Objects.requireNonNull(mActive);
+ Objects.requireNonNull(mActive, toString());
return mActiveIndex;
}
diff --git a/services/core/java/com/android/server/am/BroadcastQueueImpl.java b/services/core/java/com/android/server/am/BroadcastQueueImpl.java
index c76a0d0ac59a..d276b9a94791 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueImpl.java
@@ -606,8 +606,9 @@ class BroadcastQueueImpl extends BroadcastQueue {
} else {
mRunningColdStart.reEnqueueActiveBroadcast();
}
- demoteFromRunningLocked(mRunningColdStart);
+ final BroadcastProcessQueue queue = mRunningColdStart;
clearRunningColdStart();
+ demoteFromRunningLocked(queue);
enqueueUpdateRunningList();
}
@@ -1527,6 +1528,15 @@ class BroadcastQueueImpl extends BroadcastQueue {
final int cookie = traceBegin("demoteFromRunning");
// We've drained running broadcasts; maybe move back to runnable
+ if (mRunningColdStart == queue) {
+ // TODO: b/399020479 - Remove wtf log once we identify the case where mRunningColdStart
+ // is not getting cleared.
+ // If this queue is mRunningColdStart, then it should have been cleared before
+ // it is demoted. Log a wtf if this isn't the case.
+ Slog.wtf(TAG, "mRunningColdStart has not been cleared; mRunningColdStart.app: "
+ + mRunningColdStart.app + " , queue.app: " + queue.app,
+ new IllegalStateException());
+ }
queue.makeActiveIdle();
queue.traceProcessEnd();
@@ -2332,12 +2342,6 @@ class BroadcastQueueImpl extends BroadcastQueue {
@VisibleForTesting
@GuardedBy("mService")
- @Nullable BroadcastProcessQueue removeProcessQueue(@NonNull ProcessRecord app) {
- return removeProcessQueue(app.processName, app.info.uid);
- }
-
- @VisibleForTesting
- @GuardedBy("mService")
@Nullable BroadcastProcessQueue removeProcessQueue(@NonNull String processName,
int uid) {
BroadcastProcessQueue prev = null;
diff --git a/services/core/java/com/android/server/am/ConnectionRecord.java b/services/core/java/com/android/server/am/ConnectionRecord.java
index 31704c442290..4e1d77c26129 100644
--- a/services/core/java/com/android/server/am/ConnectionRecord.java
+++ b/services/core/java/com/android/server/am/ConnectionRecord.java
@@ -142,6 +142,10 @@ final class ConnectionRecord implements OomAdjusterModernImpl.Connection{
| Context.BIND_BYPASS_USER_NETWORK_RESTRICTIONS);
}
+ @Override
+ public boolean transmitsCpuTime() {
+ return !hasFlag(Context.BIND_ALLOW_FREEZE);
+ }
public long getFlags() {
return flags;
@@ -273,6 +277,9 @@ final class ConnectionRecord implements OomAdjusterModernImpl.Connection{
if (hasFlag(Context.BIND_INCLUDE_CAPABILITIES)) {
sb.append("CAPS ");
}
+ if (hasFlag(Context.BIND_ALLOW_FREEZE)) {
+ sb.append("!CPU ");
+ }
if (serviceDead) {
sb.append("DEAD ");
}
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index 336a35e7a7e3..fa35da30bf4b 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -2802,7 +2802,7 @@ public class OomAdjuster {
// we check the final procstate, and remove it if the procsate is below BFGS.
capability |= getBfslCapabilityFromClient(client);
- capability |= getCpuCapabilityFromClient(client);
+ capability |= getCpuCapabilityFromClient(cr, client);
if (cr.notHasFlag(Context.BIND_WAIVE_PRIORITY)) {
if (cr.hasFlag(Context.BIND_INCLUDE_CAPABILITIES)) {
@@ -3259,7 +3259,7 @@ public class OomAdjuster {
// we check the final procstate, and remove it if the procsate is below BFGS.
capability |= getBfslCapabilityFromClient(client);
- capability |= getCpuCapabilityFromClient(client);
+ capability |= getCpuCapabilityFromClient(conn, client);
if (clientProcState >= PROCESS_STATE_CACHED_ACTIVITY) {
// If the other app is cached for any reason, for purposes here
@@ -3502,10 +3502,13 @@ public class OomAdjuster {
/**
* @return the CPU capability from a client (of a service binding or provider).
*/
- private static int getCpuCapabilityFromClient(ProcessRecord client) {
- // Just grant CPU capability every time
- // TODO(b/370817323): Populate with reasons to not propagate cpu capability across bindings.
- return client.mState.getCurCapability() & PROCESS_CAPABILITY_CPU_TIME;
+ private static int getCpuCapabilityFromClient(OomAdjusterModernImpl.Connection conn,
+ ProcessRecord client) {
+ if (conn == null || conn.transmitsCpuTime()) {
+ return client.mState.getCurCapability() & PROCESS_CAPABILITY_CPU_TIME;
+ } else {
+ return 0;
+ }
}
/**
diff --git a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java
index 1b7e8f0bd244..7e7b5685cf13 100644
--- a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java
+++ b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java
@@ -635,6 +635,15 @@ public class OomAdjusterModernImpl extends OomAdjuster {
* Returns true if this connection can propagate capabilities.
*/
boolean canAffectCapabilities();
+
+ /**
+ * Returns whether this connection transmits PROCESS_CAPABILITY_CPU_TIME to the host, if the
+ * client possesses it.
+ */
+ default boolean transmitsCpuTime() {
+ // Always lend this capability by default.
+ return true;
+ }
}
/**
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 18f3500b2d56..40a9bbec3598 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -340,16 +340,16 @@ class UserController implements Handler.Callback {
private volatile ArraySet<String> mCurWaitingUserSwitchCallbacks;
/**
- * Messages for switching from {@link android.os.UserHandle#SYSTEM}.
+ * Message shown when switching from a user.
*/
@GuardedBy("mLock")
- private String mSwitchingFromSystemUserMessage;
+ private final SparseArray<String> mSwitchingFromUserMessage = new SparseArray<>();
/**
- * Messages for switching to {@link android.os.UserHandle#SYSTEM}.
+ * Message shown when switching to a user.
*/
@GuardedBy("mLock")
- private String mSwitchingToSystemUserMessage;
+ private final SparseArray<String> mSwitchingToUserMessage = new SparseArray<>();
/**
* Callbacks that are still active after {@link #getUserSwitchTimeoutMs}
@@ -2271,8 +2271,8 @@ class UserController implements Handler.Callback {
private void showUserSwitchDialog(Pair<UserInfo, UserInfo> fromToUserPair) {
// The dialog will show and then initiate the user switch by calling startUserInForeground
mInjector.showUserSwitchingDialog(fromToUserPair.first, fromToUserPair.second,
- getSwitchingFromSystemUserMessageUnchecked(),
- getSwitchingToSystemUserMessageUnchecked(),
+ getSwitchingFromUserMessageUnchecked(fromToUserPair.first.id),
+ getSwitchingToUserMessageUnchecked(fromToUserPair.second.id),
/* onShown= */ () -> sendStartUserSwitchFgMessage(fromToUserPair.second.id));
}
@@ -3388,41 +3388,45 @@ class UserController implements Handler.Callback {
return mLockPatternUtils.isLockScreenDisabled(userId);
}
- void setSwitchingFromSystemUserMessage(String switchingFromSystemUserMessage) {
+ void setSwitchingFromUserMessage(@UserIdInt int user, @Nullable String message) {
synchronized (mLock) {
- mSwitchingFromSystemUserMessage = switchingFromSystemUserMessage;
+ mSwitchingFromUserMessage.put(user, message);
}
}
- void setSwitchingToSystemUserMessage(String switchingToSystemUserMessage) {
+ void setSwitchingToUserMessage(@UserIdInt int user, @Nullable String message) {
synchronized (mLock) {
- mSwitchingToSystemUserMessage = switchingToSystemUserMessage;
+ mSwitchingToUserMessage.put(user, message);
}
}
// Called by AMS, must check permission
- String getSwitchingFromSystemUserMessage() {
- checkHasManageUsersPermission("getSwitchingFromSystemUserMessage()");
+ @Nullable
+ String getSwitchingFromUserMessage(@UserIdInt int userId) {
+ checkHasManageUsersPermission("getSwitchingFromUserMessage()");
- return getSwitchingFromSystemUserMessageUnchecked();
+ return getSwitchingFromUserMessageUnchecked(userId);
}
// Called by AMS, must check permission
- String getSwitchingToSystemUserMessage() {
- checkHasManageUsersPermission("getSwitchingToSystemUserMessage()");
+ @Nullable
+ String getSwitchingToUserMessage(@UserIdInt int userId) {
+ checkHasManageUsersPermission("getSwitchingToUserMessage()");
- return getSwitchingToSystemUserMessageUnchecked();
+ return getSwitchingToUserMessageUnchecked(userId);
}
- private String getSwitchingFromSystemUserMessageUnchecked() {
+ @Nullable
+ private String getSwitchingFromUserMessageUnchecked(@UserIdInt int userId) {
synchronized (mLock) {
- return mSwitchingFromSystemUserMessage;
+ return mSwitchingFromUserMessage.get(userId);
}
}
- private String getSwitchingToSystemUserMessageUnchecked() {
+ @Nullable
+ private String getSwitchingToUserMessageUnchecked(@UserIdInt int userId) {
synchronized (mLock) {
- return mSwitchingToSystemUserMessage;
+ return mSwitchingToUserMessage.get(userId);
}
}
@@ -3518,12 +3522,8 @@ class UserController implements Handler.Callback {
+ mIsBroadcastSentForSystemUserStarted);
pw.println(" mIsBroadcastSentForSystemUserStarting:"
+ mIsBroadcastSentForSystemUserStarting);
- if (mSwitchingFromSystemUserMessage != null) {
- pw.println(" mSwitchingFromSystemUserMessage: " + mSwitchingFromSystemUserMessage);
- }
- if (mSwitchingToSystemUserMessage != null) {
- pw.println(" mSwitchingToSystemUserMessage: " + mSwitchingToSystemUserMessage);
- }
+ pw.println(" mSwitchingFromUserMessage:" + mSwitchingFromUserMessage);
+ pw.println(" mSwitchingToUserMessage:" + mSwitchingToUserMessage);
pw.println(" mLastUserUnlockingUptime: " + mLastUserUnlockingUptime);
}
}
@@ -4046,7 +4046,7 @@ class UserController implements Handler.Callback {
}
void showUserSwitchingDialog(UserInfo fromUser, UserInfo toUser,
- String switchingFromSystemUserMessage, String switchingToSystemUserMessage,
+ @Nullable String switchingFromUserMessage, @Nullable String switchingToUserMessage,
@NonNull Runnable onShown) {
if (mService.mContext.getPackageManager()
.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
@@ -4059,7 +4059,7 @@ class UserController implements Handler.Callback {
synchronized (mUserSwitchingDialogLock) {
dismissUserSwitchingDialog(null);
mUserSwitchingDialog = new UserSwitchingDialog(mService.mContext, fromUser, toUser,
- mHandler, switchingFromSystemUserMessage, switchingToSystemUserMessage);
+ mHandler, switchingFromUserMessage, switchingToUserMessage);
mUserSwitchingDialog.show(onShown);
}
}
diff --git a/services/core/java/com/android/server/am/UserSwitchingDialog.java b/services/core/java/com/android/server/am/UserSwitchingDialog.java
index 223e0b79ec0b..f4e733a0c99f 100644
--- a/services/core/java/com/android/server/am/UserSwitchingDialog.java
+++ b/services/core/java/com/android/server/am/UserSwitchingDialog.java
@@ -52,6 +52,7 @@ import com.android.internal.R;
import com.android.internal.util.ObjectUtils;
import com.android.internal.util.UserIcons;
+import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
/**
@@ -75,21 +76,23 @@ class UserSwitchingDialog extends Dialog {
protected final UserInfo mOldUser;
protected final UserInfo mNewUser;
- private final String mSwitchingFromSystemUserMessage;
- private final String mSwitchingToSystemUserMessage;
+ @Nullable
+ private final String mSwitchingFromUserMessage;
+ @Nullable
+ private final String mSwitchingToUserMessage;
protected final Context mContext;
private final int mTraceCookie;
UserSwitchingDialog(Context context, UserInfo oldUser, UserInfo newUser, Handler handler,
- String switchingFromSystemUserMessage, String switchingToSystemUserMessage) {
+ @Nullable String switchingFromUserMessage, @Nullable String switchingToUserMessage) {
super(context, R.style.Theme_Material_NoActionBar_Fullscreen);
mContext = context;
mOldUser = oldUser;
mNewUser = newUser;
mHandler = handler;
- mSwitchingFromSystemUserMessage = switchingFromSystemUserMessage;
- mSwitchingToSystemUserMessage = switchingToSystemUserMessage;
+ mSwitchingFromUserMessage = switchingFromUserMessage;
+ mSwitchingToUserMessage = switchingToUserMessage;
mDisableAnimations = SystemProperties.getBoolean(
"debug.usercontroller.disable_user_switching_dialog_animations", false);
mTraceCookie = UserHandle.MAX_SECONDARY_USER_ID * oldUser.id + newUser.id;
@@ -166,14 +169,14 @@ class UserSwitchingDialog extends Dialog {
: R.string.demo_starting_message);
}
- final String message =
- mOldUser.id == UserHandle.USER_SYSTEM ? mSwitchingFromSystemUserMessage
- : mNewUser.id == UserHandle.USER_SYSTEM ? mSwitchingToSystemUserMessage : null;
+ if (mSwitchingFromUserMessage != null || mSwitchingToUserMessage != null) {
+ if (mSwitchingFromUserMessage != null && mSwitchingToUserMessage != null) {
+ return mSwitchingFromUserMessage + " " + mSwitchingToUserMessage;
+ }
+ return Objects.requireNonNullElse(mSwitchingFromUserMessage, mSwitchingToUserMessage);
+ }
- return message != null ? message
- // If switchingFromSystemUserMessage or switchingToSystemUserMessage is null,
- // fallback to system message.
- : res.getString(R.string.user_switching_message, mNewUser.name);
+ return res.getString(R.string.user_switching_message, mNewUser.name);
}
@Override
diff --git a/services/core/java/com/android/server/appop/AttributedOp.java b/services/core/java/com/android/server/appop/AttributedOp.java
index 9dd09cef88f9..40085ed89294 100644
--- a/services/core/java/com/android/server/appop/AttributedOp.java
+++ b/services/core/java/com/android/server/appop/AttributedOp.java
@@ -113,7 +113,7 @@ final class AttributedOp {
mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid,
parent.packageName, persistentDeviceId, tag, uidState, flags, accessTime,
AppOpsManager.ATTRIBUTION_FLAGS_NONE, AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE,
- DiscreteOpsRegistry.ACCESS_TYPE_NOTE_OP, accessCount);
+ accessCount);
}
/**
@@ -257,8 +257,7 @@ final class AttributedOp {
if (isStarted) {
mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid,
parent.packageName, persistentDeviceId, tag, uidState, flags, startTime,
- attributionFlags, attributionChainId,
- DiscreteOpsRegistry.ACCESS_TYPE_START_OP, 1);
+ attributionFlags, attributionChainId, 1);
}
}
@@ -344,9 +343,7 @@ final class AttributedOp {
mAppOpsService.mHistoricalRegistry.increaseOpAccessDuration(parent.op, parent.uid,
parent.packageName, persistentDeviceId, tag, event.getUidState(),
event.getFlags(), finishedEvent.getNoteTime(), finishedEvent.getDuration(),
- event.getAttributionFlags(), event.getAttributionChainId(),
- isPausing ? DiscreteOpsRegistry.ACCESS_TYPE_PAUSE_OP
- : DiscreteOpsRegistry.ACCESS_TYPE_FINISH_OP);
+ event.getAttributionFlags(), event.getAttributionChainId());
if (!isPausing) {
mAppOpsService.mInProgressStartOpEventPool.release(event);
@@ -454,7 +451,7 @@ final class AttributedOp {
mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid,
parent.packageName, persistentDeviceId, tag, event.getUidState(),
event.getFlags(), startTime, event.getAttributionFlags(),
- event.getAttributionChainId(), DiscreteOpsRegistry.ACCESS_TYPE_RESUME_OP, 1);
+ event.getAttributionChainId(), 1);
if (shouldSendActive) {
mAppOpsService.scheduleOpActiveChangedIfNeededLocked(parent.op, parent.uid,
parent.packageName, tag, event.getVirtualDeviceId(), true,
diff --git a/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java b/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java
index 86f5d9bd637f..c53e4bdc2205 100644
--- a/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java
+++ b/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java
@@ -189,11 +189,11 @@ class DiscreteOpsDbHelper extends SQLiteOpenHelper {
@AppOpsManager.HistoricalOpsRequestFilter int requestFilters,
int uidFilter, @Nullable String packageNameFilter,
@Nullable String attributionTagFilter, IntArray opCodesFilter, int opFlagsFilter,
- long beginTime, long endTime, int limit, String orderByColumn) {
+ long beginTime, long endTime, int limit, String orderByColumn, boolean ascending) {
List<SQLCondition> conditions = prepareConditions(beginTime, endTime, requestFilters,
uidFilter, packageNameFilter,
attributionTagFilter, opCodesFilter, opFlagsFilter);
- String sql = buildSql(conditions, orderByColumn, limit);
+ String sql = buildSql(conditions, orderByColumn, ascending, limit);
long startTime = 0;
if (Flags.sqliteDiscreteOpEventLoggingEnabled()) {
startTime = SystemClock.elapsedRealtime();
@@ -249,7 +249,8 @@ class DiscreteOpsDbHelper extends SQLiteOpenHelper {
return results;
}
- private String buildSql(List<SQLCondition> conditions, String orderByColumn, int limit) {
+ private String buildSql(List<SQLCondition> conditions, String orderByColumn, boolean ascending,
+ int limit) {
StringBuilder sql = new StringBuilder(DiscreteOpsTable.SELECT_TABLE_DATA);
if (!conditions.isEmpty()) {
sql.append(" WHERE ");
@@ -264,6 +265,7 @@ class DiscreteOpsDbHelper extends SQLiteOpenHelper {
if (orderByColumn != null) {
sql.append(" ORDER BY ").append(orderByColumn);
+ sql.append(ascending ? " ASC " : " DESC ");
}
if (limit > 0) {
sql.append(" LIMIT ").append(limit);
diff --git a/services/core/java/com/android/server/appop/DiscreteOpsMigrationHelper.java b/services/core/java/com/android/server/appop/DiscreteOpsMigrationHelper.java
index c38ee55b4f42..29e78be93da9 100644
--- a/services/core/java/com/android/server/appop/DiscreteOpsMigrationHelper.java
+++ b/services/core/java/com/android/server/appop/DiscreteOpsMigrationHelper.java
@@ -40,7 +40,16 @@ public class DiscreteOpsMigrationHelper {
static void migrateDiscreteOpsToXml(DiscreteOpsSqlRegistry sqlRegistry,
DiscreteOpsXmlRegistry xmlRegistry) {
List<DiscreteOpsSqlRegistry.DiscreteOp> sqlOps = sqlRegistry.getAllDiscreteOps();
- DiscreteOpsXmlRegistry.DiscreteOps xmlOps = getXmlDiscreteOps(sqlOps);
+
+ // Only migrate configured discrete ops. Sqlite may contain all runtime ops, and more.
+ List<DiscreteOpsSqlRegistry.DiscreteOp> filteredList = new ArrayList<>();
+ for (DiscreteOpsSqlRegistry.DiscreteOp opEvent: sqlOps) {
+ if (DiscreteOpsRegistry.isDiscreteOp(opEvent.getOpCode(), opEvent.getOpFlags())) {
+ filteredList.add(opEvent);
+ }
+ }
+
+ DiscreteOpsXmlRegistry.DiscreteOps xmlOps = getXmlDiscreteOps(filteredList);
xmlRegistry.migrateSqliteData(xmlOps);
sqlRegistry.deleteDatabase();
}
diff --git a/services/core/java/com/android/server/appop/DiscreteOpsRegistry.java b/services/core/java/com/android/server/appop/DiscreteOpsRegistry.java
index 12c35ae92cbe..70b7016fbb90 100644
--- a/services/core/java/com/android/server/appop/DiscreteOpsRegistry.java
+++ b/services/core/java/com/android/server/appop/DiscreteOpsRegistry.java
@@ -16,6 +16,9 @@
package com.android.server.appop;
+import static android.app.AppOpsManager.OP_ACCESS_ACCESSIBILITY;
+import static android.app.AppOpsManager.OP_ACCESS_NOTIFICATIONS;
+import static android.app.AppOpsManager.OP_BIND_ACCESSIBILITY_SERVICE;
import static android.app.AppOpsManager.OP_CAMERA;
import static android.app.AppOpsManager.OP_COARSE_LOCATION;
import static android.app.AppOpsManager.OP_EMERGENCY_LOCATION;
@@ -23,30 +26,24 @@ import static android.app.AppOpsManager.OP_FINE_LOCATION;
import static android.app.AppOpsManager.OP_FLAG_SELF;
import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXIED;
import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXY;
+import static android.app.AppOpsManager.OP_GPS;
import static android.app.AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION;
import static android.app.AppOpsManager.OP_MONITOR_LOCATION;
import static android.app.AppOpsManager.OP_PHONE_CALL_CAMERA;
import static android.app.AppOpsManager.OP_PHONE_CALL_MICROPHONE;
-import static android.app.AppOpsManager.OP_PROCESS_OUTGOING_CALLS;
+import static android.app.AppOpsManager.OP_READ_DEVICE_IDENTIFIERS;
import static android.app.AppOpsManager.OP_READ_HEART_RATE;
-import static android.app.AppOpsManager.OP_READ_ICC_SMS;
import static android.app.AppOpsManager.OP_READ_OXYGEN_SATURATION;
import static android.app.AppOpsManager.OP_READ_SKIN_TEMPERATURE;
-import static android.app.AppOpsManager.OP_READ_SMS;
import static android.app.AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO;
import static android.app.AppOpsManager.OP_RECEIVE_SANDBOX_TRIGGER_AUDIO;
import static android.app.AppOpsManager.OP_RECORD_AUDIO;
import static android.app.AppOpsManager.OP_RESERVED_FOR_TESTING;
-import static android.app.AppOpsManager.OP_SEND_SMS;
-import static android.app.AppOpsManager.OP_SMS_FINANCIAL_TRANSACTIONS;
-import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW;
-import static android.app.AppOpsManager.OP_WRITE_ICC_SMS;
-import static android.app.AppOpsManager.OP_WRITE_SMS;
+import static android.app.AppOpsManager.OP_RUN_IN_BACKGROUND;
import static java.lang.Long.min;
import static java.lang.Math.max;
-import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.AppOpsManager;
@@ -54,16 +51,13 @@ import android.os.AsyncTask;
import android.os.Build;
import android.permission.flags.Flags;
import android.provider.DeviceConfig;
+import android.util.IntArray;
import android.util.Slog;
-import com.android.internal.util.ArrayUtils;
-import com.android.internal.util.FrameworkStatsLog;
-
import java.io.PrintWriter;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
import java.text.SimpleDateFormat;
import java.time.Duration;
+import java.util.Arrays;
import java.util.Date;
import java.util.Set;
@@ -100,21 +94,37 @@ abstract class DiscreteOpsRegistry {
static final String PROPERTY_DISCRETE_HISTORY_QUANTIZATION =
"discrete_history_quantization_millis";
static final String PROPERTY_DISCRETE_FLAGS = "discrete_history_op_flags";
+ // Comma separated app ops list config for testing i.e. "1,2,3,4"
static final String PROPERTY_DISCRETE_OPS_LIST = "discrete_history_ops_cslist";
- static final String DEFAULT_DISCRETE_OPS = OP_FINE_LOCATION + "," + OP_COARSE_LOCATION
+ // These ops are deemed important for detecting a malicious app, and are recorded.
+ static final int[] IMPORTANT_OPS_FOR_SECURITY = new int[] {
+ OP_GPS,
+ OP_ACCESS_NOTIFICATIONS,
+ OP_RUN_IN_BACKGROUND,
+ OP_BIND_ACCESSIBILITY_SERVICE,
+ OP_ACCESS_ACCESSIBILITY,
+ OP_READ_DEVICE_IDENTIFIERS,
+ OP_MONITOR_HIGH_POWER_LOCATION,
+ OP_MONITOR_LOCATION
+ };
+
+ // These are additional ops, which are not backed by runtime permissions, but are recorded.
+ static final int[] ADDITIONAL_DISCRETE_OPS = new int[] {
+ OP_PHONE_CALL_MICROPHONE,
+ OP_RECEIVE_AMBIENT_TRIGGER_AUDIO,
+ OP_RECEIVE_SANDBOX_TRIGGER_AUDIO,
+ OP_PHONE_CALL_CAMERA,
+ OP_EMERGENCY_LOCATION,
+ OP_RESERVED_FOR_TESTING
+ };
+
+ // Legacy ops captured in discrete database.
+ private static final String LEGACY_OPS = OP_FINE_LOCATION + "," + OP_COARSE_LOCATION
+ "," + OP_EMERGENCY_LOCATION + "," + OP_CAMERA + "," + OP_RECORD_AUDIO + ","
+ OP_PHONE_CALL_MICROPHONE + "," + OP_PHONE_CALL_CAMERA + ","
+ OP_RECEIVE_AMBIENT_TRIGGER_AUDIO + "," + OP_RECEIVE_SANDBOX_TRIGGER_AUDIO
+ "," + OP_READ_HEART_RATE + "," + OP_READ_OXYGEN_SATURATION + ","
+ OP_READ_SKIN_TEMPERATURE + "," + OP_RESERVED_FOR_TESTING;
- static final int[] sDiscreteOpsToLog =
- new int[]{OP_FINE_LOCATION, OP_COARSE_LOCATION, OP_EMERGENCY_LOCATION, OP_CAMERA,
- OP_RECORD_AUDIO, OP_PHONE_CALL_MICROPHONE, OP_PHONE_CALL_CAMERA,
- OP_RECEIVE_AMBIENT_TRIGGER_AUDIO, OP_RECEIVE_SANDBOX_TRIGGER_AUDIO, OP_READ_SMS,
- OP_WRITE_SMS, OP_SEND_SMS, OP_READ_ICC_SMS, OP_WRITE_ICC_SMS,
- OP_SMS_FINANCIAL_TRANSACTIONS, OP_SYSTEM_ALERT_WINDOW, OP_MONITOR_LOCATION,
- OP_MONITOR_HIGH_POWER_LOCATION, OP_PROCESS_OUTGOING_CALLS,
- };
static final long DEFAULT_DISCRETE_HISTORY_CUTOFF = Duration.ofDays(7).toMillis();
static final long MAXIMUM_DISCRETE_HISTORY_CUTOFF = Duration.ofDays(30).toMillis();
@@ -126,7 +136,7 @@ abstract class DiscreteOpsRegistry {
// in case of duplicate op events.
static long sDiscreteHistoryQuantization;
- static int[] sDiscreteOps;
+ static int[] sDiscreteOps = new int[0];
static int sDiscreteFlags;
static final int OP_FLAGS_DISCRETE = OP_FLAG_SELF | OP_FLAG_TRUSTED_PROXIED
@@ -134,27 +144,6 @@ abstract class DiscreteOpsRegistry {
boolean mDebugMode = false;
- static final int ACCESS_TYPE_NOTE_OP =
- FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__NOTE_OP;
- static final int ACCESS_TYPE_START_OP =
- FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__START_OP;
- static final int ACCESS_TYPE_FINISH_OP =
- FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__FINISH_OP;
- static final int ACCESS_TYPE_PAUSE_OP =
- FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__PAUSE_OP;
- static final int ACCESS_TYPE_RESUME_OP =
- FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__RESUME_OP;
-
- @Retention(RetentionPolicy.SOURCE)
- @IntDef(prefix = {"ACCESS_TYPE_"}, value = {
- ACCESS_TYPE_NOTE_OP,
- ACCESS_TYPE_START_OP,
- ACCESS_TYPE_FINISH_OP,
- ACCESS_TYPE_PAUSE_OP,
- ACCESS_TYPE_RESUME_OP
- })
- @interface AccessType {}
-
void systemReady() {
DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_PRIVACY,
AsyncTask.THREAD_POOL_EXECUTOR, (DeviceConfig.Properties p) -> {
@@ -166,8 +155,7 @@ abstract class DiscreteOpsRegistry {
abstract void recordDiscreteAccess(int uid, String packageName, @NonNull String deviceId,
int op, @Nullable String attributionTag, @AppOpsManager.OpFlags int flags,
@AppOpsManager.UidState int uidState, long accessTime, long accessDuration,
- @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId,
- @DiscreteOpsRegistry.AccessType int accessType);
+ @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId);
/**
* A periodic callback from {@link AppOpsService} to flush the in memory events to disk.
@@ -218,7 +206,7 @@ abstract class DiscreteOpsRegistry {
}
static boolean isDiscreteOp(int op, @AppOpsManager.OpFlags int flags) {
- if (!ArrayUtils.contains(sDiscreteOps, op)) {
+ if (Arrays.binarySearch(sDiscreteOps, op) < 0) {
return false;
}
if ((flags & (sDiscreteFlags)) == 0) {
@@ -227,9 +215,6 @@ abstract class DiscreteOpsRegistry {
return true;
}
- // could this be impl detail of discrete registry, just one test is using the method
- // abstract DiscreteRegistry.DiscreteOps getAllDiscreteOps();
-
private void setDiscreteHistoryParameters(DeviceConfig.Properties p) {
if (p.getKeyset().contains(PROPERTY_DISCRETE_HISTORY_CUTOFF)) {
sDiscreteHistoryCutoff = p.getLong(PROPERTY_DISCRETE_HISTORY_CUTOFF,
@@ -251,11 +236,42 @@ abstract class DiscreteOpsRegistry {
} else {
sDiscreteHistoryQuantization = DEFAULT_DISCRETE_HISTORY_QUANTIZATION;
}
- sDiscreteFlags = p.getKeyset().contains(PROPERTY_DISCRETE_FLAGS) ? sDiscreteFlags =
- p.getInt(PROPERTY_DISCRETE_FLAGS, OP_FLAGS_DISCRETE) : OP_FLAGS_DISCRETE;
- sDiscreteOps = p.getKeyset().contains(PROPERTY_DISCRETE_OPS_LIST) ? parseOpsList(
- p.getString(PROPERTY_DISCRETE_OPS_LIST, DEFAULT_DISCRETE_OPS)) : parseOpsList(
- DEFAULT_DISCRETE_OPS);
+ sDiscreteFlags = p.getKeyset().contains(PROPERTY_DISCRETE_FLAGS)
+ ? p.getInt(PROPERTY_DISCRETE_FLAGS, OP_FLAGS_DISCRETE) : OP_FLAGS_DISCRETE;
+ String opsListConfig = p.getString(PROPERTY_DISCRETE_OPS_LIST, null);
+ sDiscreteOps = opsListConfig == null ? getDefaultOpsList() : parseOpsList(opsListConfig);
+
+ Arrays.sort(sDiscreteOps);
+ }
+
+ // App ops backed by runtime/dangerous permissions.
+ private static IntArray getRuntimePermissionOps() {
+ IntArray runtimeOps = new IntArray();
+ for (int op = 0; op < AppOpsManager._NUM_OP; op++) {
+ if (AppOpsManager.opIsRuntimePermission(op)) {
+ runtimeOps.add(op);
+ }
+ }
+ return runtimeOps;
+ }
+
+ /**
+ * @return an array of app ops captured into discrete database.
+ */
+ private static int[] getDefaultOpsList() {
+ if (!(Flags.recordAllRuntimeAppopsSqlite() && Flags.enableSqliteAppopsAccesses())) {
+ return getDefaultLegacyOps();
+ }
+
+ IntArray discreteOpsArray = getRuntimePermissionOps();
+ discreteOpsArray.addAll(IMPORTANT_OPS_FOR_SECURITY);
+ discreteOpsArray.addAll(ADDITIONAL_DISCRETE_OPS);
+
+ return discreteOpsArray.toArray();
+ }
+
+ private static int[] getDefaultLegacyOps() {
+ return parseOpsList(LEGACY_OPS);
}
private static int[] parseOpsList(String opsList) {
@@ -273,32 +289,8 @@ abstract class DiscreteOpsRegistry {
}
} catch (NumberFormatException e) {
Slog.e(TAG, "Failed to parse Discrete ops list: " + e.getMessage());
- return parseOpsList(DEFAULT_DISCRETE_OPS);
+ return getDefaultOpsList();
}
return result;
}
-
- /**
- * Whether app op access tacking is enabled and a metric event should be logged.
- */
- static boolean shouldLogAccess(int op) {
- return Flags.appopAccessTrackingLoggingEnabled()
- && ArrayUtils.contains(sDiscreteOpsToLog, op);
- }
-
- String getAttributionTag(String attributionTag, String packageName) {
- if (attributionTag == null || packageName == null) {
- return attributionTag;
- }
- int firstChar = 0;
- if (attributionTag.startsWith(packageName)) {
- firstChar = packageName.length();
- if (firstChar < attributionTag.length() && attributionTag.charAt(firstChar)
- == '.') {
- firstChar++;
- }
- }
- return attributionTag.substring(firstChar);
- }
-
}
diff --git a/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java
index dc11be9aadb6..0e1fbf3a6d1a 100644
--- a/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java
+++ b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java
@@ -36,7 +36,6 @@ import android.util.IntArray;
import android.util.LongSparseArray;
import android.util.Slog;
-import com.android.internal.util.FrameworkStatsLog;
import com.android.server.ServiceThread;
import java.io.File;
@@ -97,15 +96,7 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
void recordDiscreteAccess(int uid, String packageName,
@NonNull String deviceId, int op,
@Nullable String attributionTag, int flags, int uidState,
- long accessTime, long accessDuration, int attributionFlags, int attributionChainId,
- int accessType) {
- if (shouldLogAccess(op)) {
- FrameworkStatsLog.write(FrameworkStatsLog.APP_OP_ACCESS_TRACKED, uid, op, accessType,
- uidState, flags, attributionFlags,
- getAttributionTag(attributionTag, packageName),
- attributionChainId);
- }
-
+ long accessTime, long accessDuration, int attributionFlags, int attributionChainId) {
if (!isDiscreteOp(op, flags)) {
return;
}
@@ -189,7 +180,7 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
ChronoUnit.MILLIS).toEpochMilli());
List<DiscreteOp> discreteOps = mDiscreteOpsDbHelper.getDiscreteOps(filter, uidFilter,
packageNameFilter, attributionTagFilter, opCodes, opFlagsFilter, beginTimeMillis,
- endTimeMillis, -1, null);
+ endTimeMillis, -1, null, false);
LongSparseArray<AttributionChain> attributionChains = null;
if (assembleChains) {
@@ -222,14 +213,15 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
@AppOpsManager.HistoricalOpsRequestFilter int filter, int dumpOp,
@NonNull SimpleDateFormat sdf, @NonNull Date date, @NonNull String prefix,
int nDiscreteOps) {
- writeAndClearOldAccessHistory();
+ // flush the cache into database before dump.
+ mDiscreteOpsDbHelper.insertDiscreteOps(mDiscreteOpCache.getAllEventsAndClear());
IntArray opCodes = new IntArray();
if (dumpOp != AppOpsManager.OP_NONE) {
opCodes.add(dumpOp);
}
List<DiscreteOp> discreteOps = mDiscreteOpsDbHelper.getDiscreteOps(filter, uidFilter,
packageNameFilter, attributionTagFilter, opCodes, 0, -1,
- -1, nDiscreteOps, DiscreteOpsTable.Columns.ACCESS_TIME);
+ -1, nDiscreteOps, DiscreteOpsTable.Columns.ACCESS_TIME, false);
pw.print(prefix);
pw.print("Largest chain id: ");
diff --git a/services/core/java/com/android/server/appop/DiscreteOpsTestingShim.java b/services/core/java/com/android/server/appop/DiscreteOpsTestingShim.java
index 1523cca86607..909a04c44ae5 100644
--- a/services/core/java/com/android/server/appop/DiscreteOpsTestingShim.java
+++ b/services/core/java/com/android/server/appop/DiscreteOpsTestingShim.java
@@ -48,15 +48,13 @@ class DiscreteOpsTestingShim extends DiscreteOpsRegistry {
@Override
void recordDiscreteAccess(int uid, String packageName, @NonNull String deviceId, int op,
@Nullable String attributionTag, int flags, int uidState, long accessTime,
- long accessDuration, int attributionFlags, int attributionChainId, int accessType) {
+ long accessDuration, int attributionFlags, int attributionChainId) {
long start = SystemClock.uptimeMillis();
mXmlRegistry.recordDiscreteAccess(uid, packageName, deviceId, op, attributionTag, flags,
- uidState, accessTime, accessDuration, attributionFlags, attributionChainId,
- accessType);
+ uidState, accessTime, accessDuration, attributionFlags, attributionChainId);
long start2 = SystemClock.uptimeMillis();
mSqlRegistry.recordDiscreteAccess(uid, packageName, deviceId, op, attributionTag, flags,
- uidState, accessTime, accessDuration, attributionFlags, attributionChainId,
- accessType);
+ uidState, accessTime, accessDuration, attributionFlags, attributionChainId);
long end = SystemClock.uptimeMillis();
long xmlTimeTaken = start2 - start;
long sqlTimeTaken = end - start2;
diff --git a/services/core/java/com/android/server/appop/DiscreteOpsXmlRegistry.java b/services/core/java/com/android/server/appop/DiscreteOpsXmlRegistry.java
index a6e3fc7cc66a..20706b659ffb 100644
--- a/services/core/java/com/android/server/appop/DiscreteOpsXmlRegistry.java
+++ b/services/core/java/com/android/server/appop/DiscreteOpsXmlRegistry.java
@@ -45,7 +45,6 @@ import android.util.Xml;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.ArrayUtils;
-import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.XmlUtils;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
@@ -159,15 +158,7 @@ class DiscreteOpsXmlRegistry extends DiscreteOpsRegistry {
void recordDiscreteAccess(int uid, String packageName, @NonNull String deviceId, int op,
@Nullable String attributionTag, @AppOpsManager.OpFlags int flags,
@AppOpsManager.UidState int uidState, long accessTime, long accessDuration,
- @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId,
- @AccessType int accessType) {
- if (shouldLogAccess(op)) {
- FrameworkStatsLog.write(FrameworkStatsLog.APP_OP_ACCESS_TRACKED, uid, op, accessType,
- uidState, flags, attributionFlags,
- getAttributionTag(attributionTag, packageName),
- attributionChainId);
- }
-
+ @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId) {
if (!isDiscreteOp(op, flags)) {
return;
}
diff --git a/services/core/java/com/android/server/appop/HistoricalRegistry.java b/services/core/java/com/android/server/appop/HistoricalRegistry.java
index d267e0d9e536..06e43e8ec68d 100644
--- a/services/core/java/com/android/server/appop/HistoricalRegistry.java
+++ b/services/core/java/com/android/server/appop/HistoricalRegistry.java
@@ -497,7 +497,7 @@ final class HistoricalRegistry {
@NonNull String deviceId, @Nullable String attributionTag, @UidState int uidState,
@OpFlags int flags, long accessTime,
@AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId,
- @DiscreteOpsRegistry.AccessType int accessType, int accessCount) {
+ int accessCount) {
synchronized (mInMemoryLock) {
if (mMode == AppOpsManager.HISTORICAL_MODE_ENABLED_ACTIVE) {
if (!isPersistenceInitializedMLocked()) {
@@ -510,7 +510,7 @@ final class HistoricalRegistry {
mDiscreteRegistry.recordDiscreteAccess(uid, packageName, deviceId, op,
attributionTag, flags, uidState, accessTime, -1, attributionFlags,
- attributionChainId, accessType);
+ attributionChainId);
}
}
}
@@ -533,8 +533,7 @@ final class HistoricalRegistry {
void increaseOpAccessDuration(int op, int uid, @NonNull String packageName,
@NonNull String deviceId, @Nullable String attributionTag, @UidState int uidState,
@OpFlags int flags, long eventStartTime, long increment,
- @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId,
- @DiscreteOpsRegistry.AccessType int accessType) {
+ @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId) {
synchronized (mInMemoryLock) {
if (mMode == AppOpsManager.HISTORICAL_MODE_ENABLED_ACTIVE) {
if (!isPersistenceInitializedMLocked()) {
@@ -546,7 +545,7 @@ final class HistoricalRegistry {
attributionTag, uidState, flags, increment);
mDiscreteRegistry.recordDiscreteAccess(uid, packageName, deviceId, op,
attributionTag, flags, uidState, eventStartTime, increment,
- attributionFlags, attributionChainId, accessType);
+ attributionFlags, attributionChainId);
}
}
}
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index ada1cd73f775..766456134b20 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -4997,6 +4997,8 @@ public class AudioService extends IAudioService.Stub
pw.println("\tcom.android.media.audio.disablePrescaleAbsoluteVolume:"
+ disablePrescaleAbsoluteVolume());
pw.println("\tcom.android.media.audio.setStreamVolumeOrder - EOL");
+ pw.println("\tandroid.media.audio.ringtoneUserUriCheck:"
+ + android.media.audio.Flags.ringtoneUserUriCheck());
pw.println("\tandroid.media.audio.roForegroundAudioControl:"
+ roForegroundAudioControl());
pw.println("\tandroid.media.audio.scoManagedByAudio:"
diff --git a/services/core/java/com/android/server/display/DisplayControl.java b/services/core/java/com/android/server/display/DisplayControl.java
index ddea285d3564..1d70953de6c0 100644
--- a/services/core/java/com/android/server/display/DisplayControl.java
+++ b/services/core/java/com/android/server/display/DisplayControl.java
@@ -29,7 +29,7 @@ import java.util.Objects;
*/
public class DisplayControl {
private static native IBinder nativeCreateVirtualDisplay(String name, boolean secure,
- String uniqueId, float requestedRefreshRate);
+ boolean optimizeForPower, String uniqueId, float requestedRefreshRate);
private static native void nativeDestroyVirtualDisplay(IBinder displayToken);
private static native void nativeOverrideHdrTypes(IBinder displayToken, int[] modes);
private static native long[] nativeGetPhysicalDisplayIds();
@@ -49,7 +49,7 @@ public class DisplayControl {
*/
public static IBinder createVirtualDisplay(String name, boolean secure) {
Objects.requireNonNull(name, "name must not be null");
- return nativeCreateVirtualDisplay(name, secure, "", 0.0f);
+ return nativeCreateVirtualDisplay(name, secure, true, "", 0.0f);
}
/**
@@ -57,6 +57,10 @@ public class DisplayControl {
*
* @param name The name of the virtual display.
* @param secure Whether this display is secure.
+ * @param optimizeForPower Whether SurfaceFlinger should optimize for power (instead of
+ * performance). Such displays will depend on another display for it to
+ * be shown and rendered, and that display will optimize for
+ * performance when it is on.
* @param uniqueId The unique ID for the display.
* @param requestedRefreshRate The requested refresh rate in frames per second.
* For best results, specify a divisor of the physical refresh rate, e.g., 30 or 60 on
@@ -66,10 +70,11 @@ public class DisplayControl {
* @return The token reference for the display in SurfaceFlinger.
*/
public static IBinder createVirtualDisplay(String name, boolean secure,
- String uniqueId, float requestedRefreshRate) {
+ boolean optimizeForPower, String uniqueId, float requestedRefreshRate) {
Objects.requireNonNull(name, "name must not be null");
Objects.requireNonNull(uniqueId, "uniqueId must not be null");
- return nativeCreateVirtualDisplay(name, secure, uniqueId, requestedRefreshRate);
+ return nativeCreateVirtualDisplay(name, secure, optimizeForPower, uniqueId,
+ requestedRefreshRate);
}
/**
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 7016c11b69e7..a28069bbf050 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -674,9 +674,9 @@ public final class DisplayManagerService extends SystemService {
mConfigParameterProvider = new DeviceConfigParameterProvider(DeviceConfigInterface.REAL);
mExtraDisplayLoggingPackageName = DisplayProperties.debug_vri_package().orElse(null);
mExtraDisplayEventLogging = !TextUtils.isEmpty(mExtraDisplayLoggingPackageName);
-
+ // TODO(b/400384229): stats service needs to react to mirror-extended switch
mExternalDisplayStatsService = new ExternalDisplayStatsService(mContext, mHandler,
- this::isExtendedDisplayEnabled);
+ this::isExtendedDisplayAllowed);
mDisplayNotificationManager = new DisplayNotificationManager(mFlags, mContext,
mExternalDisplayStatsService);
mExternalDisplayPolicy = new ExternalDisplayPolicy(new ExternalDisplayPolicyInjector());
@@ -690,7 +690,7 @@ public final class DisplayManagerService extends SystemService {
deliverTopologyUpdate(update.first);
};
mDisplayTopologyCoordinator = new DisplayTopologyCoordinator(
- this::isExtendedDisplayEnabled, topologyChangedCallback,
+ this::isExtendedDisplayAllowed, topologyChangedCallback,
new HandlerExecutor(mHandler), mSyncRoot, backupManager::dataChanged);
} else {
mDisplayTopologyCoordinator = null;
@@ -2411,7 +2411,10 @@ public final class DisplayManagerService extends SystemService {
updateLogicalDisplayState(display);
}
- private boolean isExtendedDisplayEnabled() {
+ private boolean isExtendedDisplayAllowed() {
+ if (mFlags.isDisplayContentModeManagementEnabled()) {
+ return true;
+ }
try {
return 0 != Settings.Global.getInt(
mContext.getContentResolver(),
@@ -6045,7 +6048,13 @@ public final class DisplayManagerService extends SystemService {
return;
}
if (inTopology) {
- mDisplayTopologyCoordinator.onDisplayAdded(getDisplayInfo(displayId));
+ var info = getDisplayInfo(displayId);
+ if (info == null) {
+ Slog.w(TAG, "onDisplayBelongToTopologyChanged: cancelled displayId="
+ + displayId + " info=null");
+ return;
+ }
+ mDisplayTopologyCoordinator.onDisplayAdded(info);
} else {
mDisplayTopologyCoordinator.onDisplayRemoved(displayId);
}
diff --git a/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java b/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java
index 997fff58b952..b4df1f76dccb 100644
--- a/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java
+++ b/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java
@@ -69,9 +69,9 @@ class DisplayTopologyCoordinator {
private final SparseArray<String> mDisplayIdToUniqueIdMapping = new SparseArray<>();
/**
- * Check if extended displays are enabled. If not, a topology is not needed.
+ * Check if extended displays are allowed. If not, a topology is not needed.
*/
- private final BooleanSupplier mIsExtendedDisplayEnabled;
+ private final BooleanSupplier mIsExtendedDisplayAllowed;
/**
* Callback used to send topology updates.
@@ -83,21 +83,21 @@ class DisplayTopologyCoordinator {
private final DisplayManagerService.SyncRoot mSyncRoot;
private final Runnable mTopologySavedCallback;
- DisplayTopologyCoordinator(BooleanSupplier isExtendedDisplayEnabled,
+ DisplayTopologyCoordinator(BooleanSupplier isExtendedDisplayAllowed,
Consumer<Pair<DisplayTopology, DisplayTopologyGraph>> onTopologyChangedCallback,
Executor topologyChangeExecutor, DisplayManagerService.SyncRoot syncRoot,
Runnable topologySavedCallback) {
- this(new Injector(), isExtendedDisplayEnabled, onTopologyChangedCallback,
+ this(new Injector(), isExtendedDisplayAllowed, onTopologyChangedCallback,
topologyChangeExecutor, syncRoot, topologySavedCallback);
}
@VisibleForTesting
- DisplayTopologyCoordinator(Injector injector, BooleanSupplier isExtendedDisplayEnabled,
+ DisplayTopologyCoordinator(Injector injector, BooleanSupplier isExtendedDisplayAllowed,
Consumer<Pair<DisplayTopology, DisplayTopologyGraph>> onTopologyChangedCallback,
Executor topologyChangeExecutor, DisplayManagerService.SyncRoot syncRoot,
Runnable topologySavedCallback) {
mTopology = injector.getTopology();
- mIsExtendedDisplayEnabled = isExtendedDisplayEnabled;
+ mIsExtendedDisplayAllowed = isExtendedDisplayAllowed;
mOnTopologyChangedCallback = onTopologyChangedCallback;
mTopologyChangeExecutor = topologyChangeExecutor;
mSyncRoot = syncRoot;
@@ -262,9 +262,9 @@ class DisplayTopologyCoordinator {
return false;
}
if ((info.type == Display.TYPE_EXTERNAL || info.type == Display.TYPE_OVERLAY)
- && !mIsExtendedDisplayEnabled.getAsBoolean()) {
+ && !mIsExtendedDisplayAllowed.getAsBoolean()) {
Slog.d(TAG, "Display " + info.displayId + " not allowed in topology because "
- + "type is EXTERNAL or OVERLAY and !mIsExtendedDisplayEnabled");
+ + "type is EXTERNAL or OVERLAY and !mIsExtendedDisplayAllowed");
return false;
}
return true;
diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
index ac03a93ca9e1..c2eac8605851 100644
--- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
@@ -103,10 +103,10 @@ public class VirtualDisplayAdapter extends DisplayAdapter {
Context context, Handler handler, Listener listener, DisplayManagerFlags featureFlags) {
this(syncRoot, context, handler, listener, new SurfaceControlDisplayFactory() {
@Override
- public IBinder createDisplay(String name, boolean secure, String uniqueId,
- float requestedRefreshRate) {
- return DisplayControl.createVirtualDisplay(name, secure, uniqueId,
- requestedRefreshRate);
+ public IBinder createDisplay(String name, boolean secure, boolean optimizeForPower,
+ String uniqueId, float requestedRefreshRate) {
+ return DisplayControl.createVirtualDisplay(name, secure, optimizeForPower, uniqueId,
+ requestedRefreshRate);
}
@Override
@@ -182,9 +182,13 @@ public class VirtualDisplayAdapter extends DisplayAdapter {
String name = virtualDisplayConfig.getName();
boolean secure = (flags & VIRTUAL_DISPLAY_FLAG_SECURE) != 0;
+ boolean neverBlank = isNeverBlank(flags);
- IBinder displayToken = mSurfaceControlDisplayFactory.createDisplay(name, secure, uniqueId,
- virtualDisplayConfig.getRequestedRefreshRate());
+ // Never-blank displays are considered to be dependent on another display to be rendered.
+ // As a result, such displays should optimize for power instead of performance when it is
+ // powered on.
+ IBinder displayToken = mSurfaceControlDisplayFactory.createDisplay(name, secure, neverBlank,
+ uniqueId, virtualDisplayConfig.getRequestedRefreshRate());
MediaProjectionCallback mediaProjectionCallback = null;
if (projection != null) {
mediaProjectionCallback = new MediaProjectionCallback(appToken);
@@ -318,6 +322,12 @@ public class VirtualDisplayAdapter extends DisplayAdapter {
return mVirtualDisplayDevices.remove(appToken);
}
+ private static boolean isNeverBlank(int flags) {
+ // Private non-mirror displays are never blank and always on.
+ return (flags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) == 0
+ && (flags & VIRTUAL_DISPLAY_FLAG_PUBLIC) == 0;
+ }
+
private final class VirtualDisplayDevice extends DisplayDevice implements DeathRecipient {
private static final int PENDING_SURFACE_CHANGE = 0x01;
private static final int PENDING_RESIZE = 0x02;
@@ -377,9 +387,7 @@ public class VirtualDisplayAdapter extends DisplayAdapter {
mCallback = callback;
mProjection = projection;
mMediaProjectionCallback = mediaProjectionCallback;
- // Private non-mirror displays are never blank and always on.
- mNeverBlank = (flags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) == 0
- && (flags & VIRTUAL_DISPLAY_FLAG_PUBLIC) == 0;
+ mNeverBlank = isNeverBlank(flags);
if (android.companion.virtualdevice.flags.Flags.correctVirtualDisplayPowerState()
&& !mNeverBlank) {
// The display's power state depends on the power state of the state of its
@@ -782,6 +790,10 @@ public class VirtualDisplayAdapter extends DisplayAdapter {
*
* @param name The name of the display.
* @param secure Whether this display is secure.
+ * @param optimizeForPower Whether SurfaceFlinger should optimize for power (instead of
+ * performance). Such displays will depend on another display for
+ * it to be shown and rendered, and that display will optimize for
+ * performance when it is on.
* @param uniqueId The unique ID for the display.
* @param requestedRefreshRate
* The refresh rate, frames per second, to request on the virtual display.
@@ -791,8 +803,8 @@ public class VirtualDisplayAdapter extends DisplayAdapter {
* the refresh rate of the leader physical display.
* @return The token reference for the display in SurfaceFlinger.
*/
- IBinder createDisplay(String name, boolean secure, String uniqueId,
- float requestedRefreshRate);
+ IBinder createDisplay(String name, boolean secure, boolean optimizeForPower,
+ String uniqueId, float requestedRefreshRate);
/**
* Destroy a display in SurfaceFlinger.
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 97f9a7c4f2b0..8f5b831ca0b4 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -219,7 +219,9 @@ public class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
&& reason != HdmiControlService.INITIATED_BY_BOOT_UP;
List<HdmiCecMessage> bufferedActiveSource = mDelayedMessageBuffer
.getBufferedMessagesWithOpcode(Constants.MESSAGE_ACTIVE_SOURCE);
- if (bufferedActiveSource.isEmpty()) {
+ List<HdmiCecMessage> bufferedActiveSourceFromService = mService.getCecMessageWithOpcode(
+ Constants.MESSAGE_ACTIVE_SOURCE);
+ if (bufferedActiveSource.isEmpty() && bufferedActiveSourceFromService.isEmpty()) {
addAndStartAction(new RequestActiveSourceAction(this, new IHdmiControlCallback.Stub() {
@Override
public void onComplete(int result) {
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 6d973ac8d1b5..fdd0ef2f90e1 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -1593,6 +1593,17 @@ public class HdmiControlService extends SystemService {
this.mCecMessageBuffer = cecMessageBuffer;
}
+ List<HdmiCecMessage> getCecMessageWithOpcode(int opcode) {
+ List<HdmiCecMessage> cecMessagesWithOpcode = new ArrayList<>();
+ List<HdmiCecMessage> cecMessages = mCecMessageBuffer.getBuffer();
+ for (HdmiCecMessage message: cecMessages) {
+ if (message.getOpcode() == opcode) {
+ cecMessagesWithOpcode.add(message);
+ }
+ }
+ return cecMessagesWithOpcode;
+ }
+
/**
* Returns {@link Looper} of main thread. Use this {@link Looper} instance
* for tasks that are running on main service thread.
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index c2fecf283a34..d9db178e0dc2 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -568,6 +568,7 @@ public class InputManagerService extends IInputManager.Stub
}
mWindowManagerCallbacks = callbacks;
registerLidSwitchCallbackInternal(mWindowManagerCallbacks);
+ mKeyGestureController.setWindowManagerCallbacks(callbacks);
}
public void setWiredAccessoryCallbacks(WiredAccessoryCallbacks callbacks) {
@@ -2756,24 +2757,6 @@ public class InputManagerService extends IInputManager.Stub
@Nullable IBinder focussedToken) {
return InputManagerService.this.handleKeyGestureEvent(event);
}
-
- @Override
- public boolean isKeyGestureSupported(int gestureType) {
- switch (gestureType) {
- case KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_UP:
- case KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_DOWN:
- case KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_TOGGLE:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_STICKY_KEYS:
- return true;
- default:
- return false;
-
- }
- }
});
}
@@ -3371,6 +3354,11 @@ public class InputManagerService extends IInputManager.Stub
*/
@Nullable
SurfaceControl createSurfaceForGestureMonitor(String name, int displayId);
+
+ /**
+ * Provide information on whether the keyguard is currently locked or not.
+ */
+ boolean isKeyguardLocked(int displayId);
}
/**
diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java
index ef5babf19d83..395c77322c04 100644
--- a/services/core/java/com/android/server/input/KeyGestureController.java
+++ b/services/core/java/com/android/server/input/KeyGestureController.java
@@ -62,8 +62,10 @@ import android.view.Display;
import android.view.InputDevice;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
+import android.view.ViewConfiguration;
import com.android.internal.R;
+import com.android.internal.accessibility.AccessibilityShortcutController;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.policy.IShortcutService;
@@ -104,6 +106,7 @@ final class KeyGestureController {
private static final int MSG_NOTIFY_KEY_GESTURE_EVENT = 1;
private static final int MSG_PERSIST_CUSTOM_GESTURES = 2;
private static final int MSG_LOAD_CUSTOM_GESTURES = 3;
+ private static final int MSG_ACCESSIBILITY_SHORTCUT = 4;
// must match: config_settingsKeyBehavior in config.xml
private static final int SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY = 0;
@@ -122,12 +125,15 @@ final class KeyGestureController {
static final int POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS = 2;
private final Context mContext;
+ private InputManagerService.WindowManagerCallbacks mWindowManagerCallbacks;
private final Handler mHandler;
private final Handler mIoHandler;
private final int mSystemPid;
private final KeyCombinationManager mKeyCombinationManager;
private final SettingsObserver mSettingsObserver;
private final AppLaunchShortcutManager mAppLaunchShortcutManager;
+ @VisibleForTesting
+ final AccessibilityShortcutController mAccessibilityShortcutController;
private final InputGestureManager mInputGestureManager;
private final DisplayManager mDisplayManager;
@GuardedBy("mInputDataStore")
@@ -175,8 +181,14 @@ final class KeyGestureController {
private final boolean mVisibleBackgroundUsersEnabled = isVisibleBackgroundUsersEnabled();
- KeyGestureController(Context context, Looper looper, Looper ioLooper,
+ public KeyGestureController(Context context, Looper looper, Looper ioLooper,
InputDataStore inputDataStore) {
+ this(context, looper, ioLooper, inputDataStore, new Injector());
+ }
+
+ @VisibleForTesting
+ KeyGestureController(Context context, Looper looper, Looper ioLooper,
+ InputDataStore inputDataStore, Injector injector) {
mContext = context;
mHandler = new Handler(looper, this::handleMessage);
mIoHandler = new Handler(ioLooper, this::handleIoMessage);
@@ -197,6 +209,8 @@ final class KeyGestureController {
mSettingsObserver = new SettingsObserver(mHandler);
mAppLaunchShortcutManager = new AppLaunchShortcutManager(mContext);
mInputGestureManager = new InputGestureManager(mContext);
+ mAccessibilityShortcutController = injector.getAccessibilityShortcutController(mContext,
+ mHandler);
mDisplayManager = Objects.requireNonNull(mContext.getSystemService(DisplayManager.class));
mInputDataStore = inputDataStore;
mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
@@ -295,8 +309,8 @@ final class KeyGestureController {
KeyEvent.KEYCODE_VOLUME_UP) {
@Override
public boolean preCondition() {
- return isKeyGestureSupported(
- KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD);
+ return mAccessibilityShortcutController.isAccessibilityShortcutAvailable(
+ mWindowManagerCallbacks.isKeyguardLocked(DEFAULT_DISPLAY));
}
@Override
@@ -376,15 +390,15 @@ final class KeyGestureController {
KeyEvent.KEYCODE_DPAD_DOWN) {
@Override
public boolean preCondition() {
- return isKeyGestureSupported(
- KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD);
+ return mAccessibilityShortcutController
+ .isAccessibilityShortcutAvailable(false);
}
@Override
public void execute() {
handleMultiKeyGesture(
new int[]{KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN},
- KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD,
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
KeyGestureEvent.ACTION_GESTURE_START, 0);
}
@@ -392,7 +406,7 @@ final class KeyGestureController {
public void cancel() {
handleMultiKeyGesture(
new int[]{KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN},
- KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD,
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
KeyGestureEvent.ACTION_GESTURE_COMPLETE,
KeyGestureEvent.FLAG_CANCELLED);
}
@@ -438,6 +452,7 @@ final class KeyGestureController {
mSettingsObserver.observe();
mAppLaunchShortcutManager.systemRunning();
mInputGestureManager.systemRunning();
+ initKeyGestures();
int userId;
synchronized (mUserLock) {
@@ -447,6 +462,27 @@ final class KeyGestureController {
mIoHandler.obtainMessage(MSG_LOAD_CUSTOM_GESTURES, userId).sendToTarget();
}
+ @SuppressLint("MissingPermission")
+ private void initKeyGestures() {
+ InputManager im = Objects.requireNonNull(mContext.getSystemService(InputManager.class));
+ im.registerKeyGestureEventHandler((event, focusedToken) -> {
+ switch (event.getKeyGestureType()) {
+ case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD:
+ if (event.getAction() == KeyGestureEvent.ACTION_GESTURE_START) {
+ mHandler.removeMessages(MSG_ACCESSIBILITY_SHORTCUT);
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(MSG_ACCESSIBILITY_SHORTCUT),
+ getAccessibilityShortcutTimeout());
+ } else {
+ mHandler.removeMessages(MSG_ACCESSIBILITY_SHORTCUT);
+ }
+ return true;
+ default:
+ return false;
+ }
+ });
+ }
+
public boolean interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
if (mVisibleBackgroundUsersEnabled && shouldIgnoreKeyEventForVisibleBackgroundUser(event)) {
return false;
@@ -971,17 +1007,6 @@ final class KeyGestureController {
return false;
}
- private boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType) {
- synchronized (mKeyGestureHandlerRecords) {
- for (KeyGestureHandlerRecord handler : mKeyGestureHandlerRecords.values()) {
- if (handler.isKeyGestureSupported(gestureType)) {
- return true;
- }
- }
- }
- return false;
- }
-
public void notifyKeyGestureCompleted(int deviceId, int[] keycodes, int modifierState,
@KeyGestureEvent.KeyGestureType int gestureType) {
// TODO(b/358569822): Once we move the gesture detection logic to IMS, we ideally
@@ -1019,9 +1044,16 @@ final class KeyGestureController {
synchronized (mUserLock) {
mCurrentUserId = userId;
}
+ mAccessibilityShortcutController.setCurrentUser(userId);
mIoHandler.obtainMessage(MSG_LOAD_CUSTOM_GESTURES, userId).sendToTarget();
}
+
+ public void setWindowManagerCallbacks(
+ @NonNull InputManagerService.WindowManagerCallbacks callbacks) {
+ mWindowManagerCallbacks = callbacks;
+ }
+
private boolean isDefaultDisplayOn() {
Display defaultDisplay = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY);
if (defaultDisplay == null) {
@@ -1068,6 +1100,9 @@ final class KeyGestureController {
AidlKeyGestureEvent event = (AidlKeyGestureEvent) msg.obj;
notifyKeyGestureEvent(event);
break;
+ case MSG_ACCESSIBILITY_SHORTCUT:
+ mAccessibilityShortcutController.performAccessibilityShortcut();
+ break;
}
return true;
}
@@ -1347,17 +1382,6 @@ final class KeyGestureController {
}
return false;
}
-
- public boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType) {
- try {
- return mKeyGestureHandler.isKeyGestureSupported(gestureType);
- } catch (RemoteException ex) {
- Slog.w(TAG, "Failed to identify if key gesture type is supported by the "
- + "process " + mPid + ", assuming it died.", ex);
- binderDied();
- }
- return false;
- }
}
private class SettingsObserver extends ContentObserver {
@@ -1413,6 +1437,25 @@ final class KeyGestureController {
return event;
}
+ private long getAccessibilityShortcutTimeout() {
+ synchronized (mUserLock) {
+ final ViewConfiguration config = ViewConfiguration.get(mContext);
+ final boolean hasDialogShown = Settings.Secure.getIntForUser(
+ mContext.getContentResolver(),
+ Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0, mCurrentUserId) != 0;
+ final boolean skipTimeoutRestriction =
+ Settings.Secure.getIntForUser(mContext.getContentResolver(),
+ Settings.Secure.SKIP_ACCESSIBILITY_SHORTCUT_DIALOG_TIMEOUT_RESTRICTION,
+ 0, mCurrentUserId) != 0;
+
+ // If users manually set the volume key shortcut for any accessibility service, the
+ // system would bypass the timeout restriction of the shortcut dialog.
+ return hasDialogShown || skipTimeoutRestriction
+ ? config.getAccessibilityShortcutKeyTimeoutAfterConfirmation()
+ : config.getAccessibilityShortcutKeyTimeout();
+ }
+ }
+
public void dump(IndentingPrintWriter ipw) {
ipw.println("KeyGestureController:");
ipw.increaseIndent();
@@ -1459,4 +1502,12 @@ final class KeyGestureController {
mAppLaunchShortcutManager.dump(ipw);
mInputGestureManager.dump(ipw);
}
+
+ @VisibleForTesting
+ static class Injector {
+ AccessibilityShortcutController getAccessibilityShortcutController(Context context,
+ Handler handler) {
+ return new AccessibilityShortcutController(context, handler, UserHandle.USER_SYSTEM);
+ }
+ }
}
diff --git a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
index 600cf7f06981..1a4ead22f658 100644
--- a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
+++ b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
@@ -22,6 +22,7 @@ import static com.android.internal.inputmethod.SoftInputShowHideReason.REMOVE_IM
import static com.android.internal.inputmethod.SoftInputShowHideReason.SHOW_IME_SCREENSHOT_FROM_IMMS;
import static com.android.server.EventLogTags.IMF_HIDE_IME;
import static com.android.server.EventLogTags.IMF_SHOW_IME;
+import static com.android.server.inputmethod.ImeProtoLogGroup.IME_VISIBILITY_APPLIER_DEBUG;
import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME;
import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME_EXPLICIT;
import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME_NOT_ALWAYS;
@@ -36,7 +37,6 @@ import android.annotation.UserIdInt;
import android.os.IBinder;
import android.os.ResultReceiver;
import android.util.EventLog;
-import android.util.Slog;
import android.view.MotionEvent;
import android.view.inputmethod.Flags;
import android.view.inputmethod.ImeTracker;
@@ -46,6 +46,7 @@ import android.view.inputmethod.InputMethodManager;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.inputmethod.InputMethodDebug;
import com.android.internal.inputmethod.SoftInputShowHideReason;
+import com.android.internal.protolog.ProtoLog;
import com.android.server.LocalServices;
import com.android.server.wm.ImeTargetVisibilityPolicy;
import com.android.server.wm.WindowManagerInternal;
@@ -58,9 +59,7 @@ import java.util.Objects;
*/
final class DefaultImeVisibilityApplier {
- private static final String TAG = "DefaultImeVisibilityApplier";
-
- private static final boolean DEBUG = InputMethodManagerService.DEBUG;
+ static final String TAG = "DefaultImeVisibilityApplier";
private InputMethodManagerService mService;
@@ -93,11 +92,10 @@ final class DefaultImeVisibilityApplier {
final var bindingController = userData.mBindingController;
final IInputMethodInvoker curMethod = bindingController.getCurMethod();
if (curMethod != null) {
- if (DEBUG) {
- Slog.v(TAG, "Calling " + curMethod + ".showSoftInput(" + showInputToken
- + ", " + showFlags + ", " + resultReceiver + ") for reason: "
- + InputMethodDebug.softInputDisplayReasonToString(reason));
- }
+ ProtoLog.v(IME_VISIBILITY_APPLIER_DEBUG,
+ "Calling %s.showSoftInput(%s, %s, %s) for reason: %s", curMethod,
+ showInputToken, showFlags, resultReceiver,
+ InputMethodDebug.softInputDisplayReasonToString(reason));
// TODO(b/192412909): Check if we can always call onShowHideSoftInputRequested() or not.
if (curMethod.showSoftInput(showInputToken, statsToken, showFlags, resultReceiver)) {
if (DEBUG_IME_VISIBILITY) {
@@ -136,11 +134,9 @@ final class DefaultImeVisibilityApplier {
// delivered to the IME process as an IPC. Hence the inconsistency between
// IMMS#mInputShown and IMMS#mImeWindowVis should be resolved spontaneously in
// the final state.
- if (DEBUG) {
- Slog.v(TAG, "Calling " + curMethod + ".hideSoftInput(0, " + hideInputToken
- + ", " + resultReceiver + ") for reason: "
- + InputMethodDebug.softInputDisplayReasonToString(reason));
- }
+ ProtoLog.v(IME_VISIBILITY_APPLIER_DEBUG,
+ "Calling %s.hideSoftInput(0, %s, %s) for reason: %s", curMethod, hideInputToken,
+ resultReceiver, InputMethodDebug.softInputDisplayReasonToString(reason));
// TODO(b/192412909): Check if we can always call onShowHideSoftInputRequested() or not.
if (curMethod.hideSoftInput(hideInputToken, statsToken, 0, resultReceiver)) {
if (DEBUG_IME_VISIBILITY) {
diff --git a/services/core/java/com/android/server/inputmethod/ImeProtoLogGroup.java b/services/core/java/com/android/server/inputmethod/ImeProtoLogGroup.java
index f9a56effc800..ea4e29564cc0 100644
--- a/services/core/java/com/android/server/inputmethod/ImeProtoLogGroup.java
+++ b/services/core/java/com/android/server/inputmethod/ImeProtoLogGroup.java
@@ -23,7 +23,11 @@ import java.util.UUID;
public enum ImeProtoLogGroup implements IProtoLogGroup {
// TODO(b/393561240): add info/warn/error log level and replace in IMMS
IMMS_DEBUG(Consts.ENABLE_DEBUG, false, false,
- InputMethodManagerService.TAG);
+ InputMethodManagerService.TAG),
+ IME_VISIBILITY_APPLIER_DEBUG(Consts.ENABLE_DEBUG, false, false,
+ DefaultImeVisibilityApplier.TAG),
+ IME_VIS_STATE_COMPUTER_DEBUG(Consts.ENABLE_DEBUG, false, false,
+ ImeVisibilityStateComputer.TAG);
private final boolean mEnabled;
private volatile boolean mLogToProto;
diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
index 5fe8318dbb3f..69353becc692 100644
--- a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
+++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
@@ -32,6 +32,7 @@ import static android.view.WindowManager.LayoutParams.SoftInputModeFlags;
import static com.android.internal.inputmethod.InputMethodDebug.softInputModeToString;
import static com.android.internal.inputmethod.SoftInputShowHideReason.REMOVE_IME_SCREENSHOT_FROM_IMMS;
import static com.android.internal.inputmethod.SoftInputShowHideReason.SHOW_IME_SCREENSHOT_FROM_IMMS;
+import static com.android.server.inputmethod.ImeProtoLogGroup.IME_VIS_STATE_COMPUTER_DEBUG;
import static com.android.server.inputmethod.InputMethodManagerService.computeImeDisplayIdForTarget;
import android.accessibilityservice.AccessibilityService;
@@ -58,6 +59,7 @@ import android.view.inputmethod.InputMethodManager;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.inputmethod.SoftInputShowHideReason;
+import com.android.internal.protolog.ProtoLog;
import com.android.server.LocalServices;
import com.android.server.pm.UserManagerInternal;
import com.android.server.wm.WindowManagerInternal;
@@ -72,9 +74,7 @@ import java.util.WeakHashMap;
*/
public final class ImeVisibilityStateComputer {
- private static final String TAG = "ImeVisibilityStateComputer";
-
- private static final boolean DEBUG = InputMethodManagerService.DEBUG;
+ static final String TAG = "ImeVisibilityStateComputer";
@UserIdInt
private final int mUserId;
@@ -292,12 +292,14 @@ public final class ImeVisibilityStateComputer {
@InputMethodManager.HideFlags int hideFlags) {
if ((hideFlags & InputMethodManager.HIDE_IMPLICIT_ONLY) != 0
&& (mRequestedShowExplicitly || mShowForced)) {
- if (DEBUG) Slog.v(TAG, "Not hiding: explicit show not cancelled by non-explicit hide");
+ ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG,
+ "Not hiding: explicit show not cancelled by non-explicit hide");
ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_HIDE_IMPLICIT);
return false;
}
if (mShowForced && (hideFlags & InputMethodManager.HIDE_NOT_ALWAYS) != 0) {
- if (DEBUG) Slog.v(TAG, "Not hiding: forced show not cancelled by not-always hide");
+ ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG,
+ "Not hiding: forced show not cancelled by not-always hide");
ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_HIDE_NOT_ALWAYS);
return false;
}
@@ -417,8 +419,8 @@ public final class ImeVisibilityStateComputer {
@GuardedBy("ImfLock.class")
private void setWindowStateInner(IBinder windowToken, @NonNull ImeTargetWindowState newState) {
- if (DEBUG) Slog.d(TAG, "setWindowStateInner, windowToken=" + windowToken
- + ", state=" + newState);
+ ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG, "setWindowStateInner, windowToken=%s, state=%s",
+ windowToken, newState);
mRequestWindowStateMap.put(windowToken, newState);
}
@@ -466,7 +468,7 @@ public final class ImeVisibilityStateComputer {
// Because the app might leverage these flags to hide soft-keyboard with showing their own
// UI for input.
if (state.hasEditorFocused() && shouldRestoreImeVisibility(state)) {
- if (DEBUG) Slog.v(TAG, "Will show input to restore visibility");
+ ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG, "Will show input to restore visibility");
// Inherit the last requested IME visible state when the target window is still
// focused with an editor.
state.setRequestedImeVisible(true);
@@ -483,7 +485,8 @@ public final class ImeVisibilityStateComputer {
// There is no focus view, and this window will
// be behind any soft input window, so hide the
// soft input window if it is shown.
- if (DEBUG) Slog.v(TAG, "Unspecified window will hide input");
+ ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG,
+ "Unspecified window will hide input");
return new ImeVisibilityResult(STATE_HIDE_IME_NOT_ALWAYS,
SoftInputShowHideReason.HIDE_UNSPECIFIED_WINDOW);
}
@@ -495,7 +498,7 @@ public final class ImeVisibilityStateComputer {
// them good context without input information being obscured
// by the IME) or if running on a large screen where there
// is more room for the target window + IME.
- if (DEBUG) Slog.v(TAG, "Unspecified window will show input");
+ ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG, "Unspecified window will show input");
return new ImeVisibilityResult(STATE_SHOW_IME_IMPLICIT,
SoftInputShowHideReason.SHOW_AUTO_EDITOR_FORWARD_NAV);
}
@@ -513,7 +516,8 @@ public final class ImeVisibilityStateComputer {
// the WindowState, as they're already in the correct state
break;
} else if (isForwardNavigation) {
- if (DEBUG) Slog.v(TAG, "Window asks to hide input going forward");
+ ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG,
+ "Window asks to hide input going forward");
return new ImeVisibilityResult(STATE_HIDE_IME_EXPLICIT,
SoftInputShowHideReason.HIDE_STATE_HIDDEN_FORWARD_NAV);
}
@@ -524,7 +528,7 @@ public final class ImeVisibilityStateComputer {
// the WindowState, as they're already in the correct state
break;
} else if (state.hasImeFocusChanged()) {
- if (DEBUG) Slog.v(TAG, "Window asks to hide input");
+ ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG, "Window asks to hide input");
return new ImeVisibilityResult(STATE_HIDE_IME_EXPLICIT,
SoftInputShowHideReason.HIDE_ALWAYS_HIDDEN_STATE);
}
@@ -532,7 +536,8 @@ public final class ImeVisibilityStateComputer {
case WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE:
if (isForwardNavigation) {
if (allowVisible) {
- if (DEBUG) Slog.v(TAG, "Window asks to show input going forward");
+ ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG,
+ "Window asks to show input going forward");
return new ImeVisibilityResult(STATE_SHOW_IME_IMPLICIT,
SoftInputShowHideReason.SHOW_STATE_VISIBLE_FORWARD_NAV);
} else {
@@ -543,7 +548,7 @@ public final class ImeVisibilityStateComputer {
}
break;
case WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE:
- if (DEBUG) Slog.v(TAG, "Window asks to always show input");
+ ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG, "Window asks to always show input");
if (allowVisible) {
if (state.hasImeFocusChanged()) {
return new ImeVisibilityResult(STATE_SHOW_IME_IMPLICIT,
@@ -565,7 +570,8 @@ public final class ImeVisibilityStateComputer {
// To maintain compatibility, we are now hiding the IME when we don't have
// an editor upon refocusing a window.
if (state.isStartInputByGainFocus()) {
- if (DEBUG) Slog.v(TAG, "Same window without editor will hide input");
+ ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG,
+ "Same window without editor will hide input");
return new ImeVisibilityResult(STATE_HIDE_IME_EXPLICIT,
SoftInputShowHideReason.HIDE_SAME_WINDOW_FOCUSED_WITHOUT_EDITOR);
}
@@ -579,7 +585,7 @@ public final class ImeVisibilityStateComputer {
// 1) SOFT_INPUT_STATE_UNCHANGED state without an editor
// 2) SOFT_INPUT_STATE_VISIBLE state without an editor
// 3) SOFT_INPUT_STATE_ALWAYS_VISIBLE state without an editor
- if (DEBUG) Slog.v(TAG, "Window without editor will hide input");
+ ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG, "Window without editor will hide input");
if (Flags.refactorInsetsController()) {
state.setRequestedImeVisible(false);
}
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index a0e543300ce7..42d0a5c4757a 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -3618,6 +3618,12 @@ public class LockSettingsService extends ILockSettings.Stub {
return;
}
+ UserInfo userInfo = mInjector.getUserManagerInternal().getUserInfo(userId);
+ if (userInfo != null && userInfo.isForTesting()) {
+ Slog.i(TAG, "Keeping escrow data for test-only user");
+ return;
+ }
+
// Disable escrow token permanently on all other device/user types.
Slogf.i(TAG, "Permanently disabling support for escrow tokens on user %d", userId);
mSpManager.destroyEscrowData(userId);
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index f137de1b3e1d..988924d9f498 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -25,6 +25,7 @@ import static android.media.MediaRouter2.SCANNING_STATE_SCANNING_FULL;
import static android.media.MediaRouter2.SCANNING_STATE_WHILE_INTERACTIVE;
import static android.media.MediaRouter2Utils.getOriginalId;
import static android.media.MediaRouter2Utils.getProviderId;
+
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_CREATE_SESSION;
import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_DESELECT_ROUTE;
@@ -63,6 +64,7 @@ import android.media.MediaRouter2Manager;
import android.media.RouteDiscoveryPreference;
import android.media.RouteListingPreference;
import android.media.RoutingSessionInfo;
+import android.media.SuggestedDeviceInfo;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
@@ -76,18 +78,21 @@ import android.util.ArrayMap;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
+
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.function.pooled.PooledLambda;
import com.android.media.flags.Flags;
import com.android.server.LocalServices;
import com.android.server.pm.UserManagerInternal;
import com.android.server.statusbar.StatusBarManagerInternal;
+
import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -551,6 +556,36 @@ class MediaRouter2ServiceImpl {
}
}
+ public void setDeviceSuggestionsWithRouter2(
+ @NonNull IMediaRouter2 router,
+ @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) {
+ Objects.requireNonNull(router, "router must not be null");
+
+ final long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ setDeviceSuggestionsWithRouter2Locked(router, suggestedDeviceInfo);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Nullable
+ public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithRouter2(
+ @NonNull IMediaRouter2 router) {
+ Objects.requireNonNull(router, "router must not be null");
+
+ final long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ return getDeviceSuggestionsWithRouter2Locked(router);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
// End of methods that implement MediaRouter2 operations.
// Start of methods that implement MediaRouter2Manager operations.
@@ -805,6 +840,36 @@ class MediaRouter2ServiceImpl {
}
}
+ public void setDeviceSuggestionsWithManager(
+ @NonNull IMediaRouter2Manager manager,
+ @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) {
+ Objects.requireNonNull(manager, "manager must not be null");
+
+ final long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ setDeviceSuggestionsWithManagerLocked(manager, suggestedDeviceInfo);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Nullable
+ public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithManager(
+ @NonNull IMediaRouter2Manager manager) {
+ Objects.requireNonNull(manager, "manager must not be null");
+
+ final long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ return getDeviceSuggestionsWithManagerLocked(manager);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
@RequiresPermission(Manifest.permission.PACKAGE_USAGE_STATS)
public boolean showMediaOutputSwitcherWithProxyRouter(
@NonNull IMediaRouter2Manager proxyRouter) {
@@ -1582,6 +1647,61 @@ class MediaRouter2ServiceImpl {
DUMMY_REQUEST_ID, routerRecord, uniqueSessionId));
}
+ @GuardedBy("mLock")
+ private void setDeviceSuggestionsWithRouter2Locked(
+ @NonNull IMediaRouter2 router,
+ @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) {
+ final IBinder binder = router.asBinder();
+ final RouterRecord routerRecord = mAllRouterRecords.get(binder);
+
+ if (routerRecord == null) {
+ Slog.w(
+ TAG,
+ TextUtils.formatSimple(
+ "Ignoring set device suggestion for unknown router: %s", router));
+ return;
+ }
+
+ Slog.i(
+ TAG,
+ TextUtils.formatSimple(
+ "setDeviceSuggestions | router: %d suggestion: %d",
+ routerRecord.mPackageName, suggestedDeviceInfo));
+
+ routerRecord.mUserRecord.updateDeviceSuggestionsLocked(
+ routerRecord.mPackageName, routerRecord.mPackageName, suggestedDeviceInfo);
+ routerRecord.mUserRecord.mHandler.sendMessage(
+ obtainMessage(
+ UserHandler::notifyDeviceSuggestionsUpdatedOnHandler,
+ routerRecord.mUserRecord.mHandler,
+ routerRecord.mPackageName,
+ routerRecord.mPackageName,
+ suggestedDeviceInfo));
+ }
+
+ @GuardedBy("mLock")
+ @Nullable
+ private Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithRouter2Locked(
+ @NonNull IMediaRouter2 router) {
+ final IBinder binder = router.asBinder();
+ final RouterRecord routerRecord = mAllRouterRecords.get(binder);
+
+ if (routerRecord == null) {
+ Slog.w(
+ TAG,
+ TextUtils.formatSimple(
+ "Attempted to get device suggestion for unknown router: %s", router));
+ return null;
+ }
+
+ Slog.i(
+ TAG,
+ TextUtils.formatSimple(
+ "getDeviceSuggestions | router: %d", routerRecord.mPackageName));
+
+ return routerRecord.mUserRecord.getDeviceSuggestionsLocked(routerRecord.mPackageName);
+ }
+
// End of locked methods that are used by MediaRouter2.
// Start of locked methods that are used by MediaRouter2Manager.
@@ -1972,6 +2092,68 @@ class MediaRouter2ServiceImpl {
uniqueRequestId, routerRecord, uniqueSessionId));
}
+ @GuardedBy("mLock")
+ private void setDeviceSuggestionsWithManagerLocked(
+ @NonNull IMediaRouter2Manager manager,
+ @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) {
+ final IBinder binder = manager.asBinder();
+ ManagerRecord managerRecord = mAllManagerRecords.get(binder);
+
+ if (managerRecord == null || managerRecord.mTargetPackageName == null) {
+ Slog.w(
+ TAG,
+ TextUtils.formatSimple(
+ "Ignoring set device suggestion for unknown manager: %s", manager));
+ return;
+ }
+
+ Slog.i(
+ TAG,
+ TextUtils.formatSimple(
+ "setDeviceSuggestions | manager: %d, suggestingPackageName: %d suggestion:"
+ + " %d",
+ managerRecord.mManagerId,
+ managerRecord.mOwnerPackageName,
+ suggestedDeviceInfo));
+
+ managerRecord.mUserRecord.updateDeviceSuggestionsLocked(
+ managerRecord.mTargetPackageName,
+ managerRecord.mOwnerPackageName,
+ suggestedDeviceInfo);
+ managerRecord.mUserRecord.mHandler.sendMessage(
+ obtainMessage(
+ UserHandler::notifyDeviceSuggestionsUpdatedOnHandler,
+ managerRecord.mUserRecord.mHandler,
+ managerRecord.mTargetPackageName,
+ managerRecord.mOwnerPackageName,
+ suggestedDeviceInfo));
+ }
+
+ @GuardedBy("mLock")
+ @Nullable
+ private Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithManagerLocked(
+ @NonNull IMediaRouter2Manager manager) {
+ final IBinder binder = manager.asBinder();
+ ManagerRecord managerRecord = mAllManagerRecords.get(binder);
+
+ if (managerRecord == null || managerRecord.mTargetPackageName == null) {
+ Slog.w(
+ TAG,
+ TextUtils.formatSimple(
+ "Attempted to get device suggestion for unknown manager: %s", manager));
+ return null;
+ }
+
+ Slog.i(
+ TAG,
+ TextUtils.formatSimple(
+ "getDeviceSuggestionsWithManagerLocked | manager: %d",
+ managerRecord.mManagerId));
+
+ return managerRecord.mUserRecord.getDeviceSuggestionsLocked(
+ managerRecord.mTargetPackageName);
+ }
+
// End of locked methods that are used by MediaRouter2Manager.
// Start of locked methods that are used by both MediaRouter2 and MediaRouter2Manager.
@@ -2047,6 +2229,11 @@ class MediaRouter2ServiceImpl {
//TODO: make records private for thread-safety
final ArrayList<RouterRecord> mRouterRecords = new ArrayList<>();
final ArrayList<ManagerRecord> mManagerRecords = new ArrayList<>();
+
+ // @GuardedBy("mLock")
+ private final Map<String, Map<String, List<SuggestedDeviceInfo>>> mDeviceSuggestions =
+ new HashMap<>();
+
RouteDiscoveryPreference mCompositeDiscoveryPreference = RouteDiscoveryPreference.EMPTY;
Set<String> mActivelyScanningPackages = Set.of();
final UserHandler mHandler;
@@ -2076,6 +2263,25 @@ class MediaRouter2ServiceImpl {
return null;
}
+ // @GuardedBy("mLock")
+ public void updateDeviceSuggestionsLocked(
+ String packageName,
+ String suggestingPackageName,
+ List<SuggestedDeviceInfo> deviceSuggestions) {
+ mDeviceSuggestions.putIfAbsent(
+ packageName, new HashMap<String, List<SuggestedDeviceInfo>>());
+ Map<String, List<SuggestedDeviceInfo>> suggestions =
+ mDeviceSuggestions.get(packageName);
+ suggestions.put(suggestingPackageName, deviceSuggestions);
+ }
+
+ // @GuardedBy("mLock")
+ @Nullable
+ public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsLocked(
+ String packageName) {
+ return mDeviceSuggestions.get(packageName);
+ }
+
public void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
pw.println(prefix + "UserRecord");
@@ -2314,6 +2520,15 @@ class MediaRouter2ServiceImpl {
}
}
+ public void notifyDeviceSuggestionsUpdated(
+ String suggestingPackageName, List<SuggestedDeviceInfo> suggestedDeviceInfo) {
+ try {
+ mRouter.notifyDeviceSuggestionsUpdated(suggestingPackageName, suggestedDeviceInfo);
+ } catch (RemoteException ex) {
+ logRemoteException("notifyDeviceSuggestionsUpdated", ex);
+ }
+ }
+
/**
* Sends the corresponding router a {@link RoutingSessionInfo session} creation request,
* with the given {@link MediaRoute2Info} as the initial member.
@@ -3556,6 +3771,41 @@ class MediaRouter2ServiceImpl {
// need to update routers other than the one making the update.
}
+ private void notifyDeviceSuggestionsUpdatedOnHandler(
+ String routerPackageName,
+ String suggestingPackageName,
+ @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) {
+ MediaRouter2ServiceImpl service = mServiceRef.get();
+ if (service == null) {
+ return;
+ }
+ List<IMediaRouter2Manager> managers = new ArrayList<>();
+ synchronized (service.mLock) {
+ for (ManagerRecord managerRecord : mUserRecord.mManagerRecords) {
+ if (TextUtils.equals(managerRecord.mTargetPackageName, routerPackageName)) {
+ managers.add(managerRecord.mManager);
+ }
+ }
+ for (IMediaRouter2Manager manager : managers) {
+ try {
+ manager.notifyDeviceSuggestionsUpdated(
+ routerPackageName, suggestingPackageName, suggestedDeviceInfo);
+ } catch (RemoteException ex) {
+ Slog.w(
+ TAG,
+ "Failed to notify suggesteion changed. Manager probably died.",
+ ex);
+ }
+ }
+ for (RouterRecord routerRecord : mUserRecord.mRouterRecords) {
+ if (TextUtils.equals(routerRecord.mPackageName, routerPackageName)) {
+ routerRecord.notifyDeviceSuggestionsUpdated(
+ suggestingPackageName, suggestedDeviceInfo);
+ }
+ }
+ }
+ }
+
private void updateDiscoveryPreferenceOnHandler() {
MediaRouter2ServiceImpl service = mServiceRef.get();
if (service == null) {
diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java
index 35bb19943a24..11f449e790a8 100644
--- a/services/core/java/com/android/server/media/MediaRouterService.java
+++ b/services/core/java/com/android/server/media/MediaRouterService.java
@@ -49,6 +49,7 @@ import android.media.RemoteDisplayState.RemoteDisplayInfo;
import android.media.RouteDiscoveryPreference;
import android.media.RouteListingPreference;
import android.media.RoutingSessionInfo;
+import android.media.SuggestedDeviceInfo;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
@@ -80,6 +81,7 @@ import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
/**
@@ -526,6 +528,21 @@ public final class MediaRouterService extends IMediaRouterService.Stub
// Binder call
@Override
+ public void setDeviceSuggestionsWithRouter2(
+ IMediaRouter2 router, @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) {
+ mService2.setDeviceSuggestionsWithRouter2(router, suggestedDeviceInfo);
+ }
+
+ // Binder call
+ @Override
+ @Nullable
+ public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithRouter2(
+ IMediaRouter2 router) {
+ return mService2.getDeviceSuggestionsWithRouter2(router);
+ }
+
+ // Binder call
+ @Override
public List<RoutingSessionInfo> getRemoteSessions(IMediaRouter2Manager manager) {
return mService2.getRemoteSessions(manager);
}
@@ -666,6 +683,22 @@ public final class MediaRouterService extends IMediaRouterService.Stub
return mService2.showMediaOutputSwitcherWithProxyRouter(proxyRouter);
}
+ // Binder call
+ @Override
+ public void setDeviceSuggestionsWithManager(
+ @NonNull IMediaRouter2Manager manager,
+ @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) {
+ mService2.setDeviceSuggestionsWithManager(manager, suggestedDeviceInfo);
+ }
+
+ // Binder call
+ @Override
+ @Nullable
+ public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithManager(
+ IMediaRouter2Manager manager) {
+ return mService2.getDeviceSuggestionsWithManager(manager);
+ }
+
void restoreBluetoothA2dp() {
try {
boolean a2dpOn;
diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java
index 91a2843ccaf7..ad108f64ffe3 100644
--- a/services/core/java/com/android/server/media/quality/MediaQualityService.java
+++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java
@@ -28,6 +28,7 @@ import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
+import android.hardware.audio.effect.DefaultExtension;
import android.hardware.tv.mediaquality.AmbientBacklightColorFormat;
import android.hardware.tv.mediaquality.IMediaQuality;
import android.hardware.tv.mediaquality.IPictureProfileAdjustmentListener;
@@ -48,7 +49,6 @@ import android.media.quality.IMediaQualityManager;
import android.media.quality.IPictureProfileCallback;
import android.media.quality.ISoundProfileCallback;
import android.media.quality.MediaQualityContract.BaseParameters;
-import android.media.quality.MediaQualityManager;
import android.media.quality.ParameterCapability;
import android.media.quality.PictureProfile;
import android.media.quality.PictureProfileHandle;
@@ -58,6 +58,7 @@ import android.os.Binder;
import android.os.Bundle;
import android.os.Environment;
import android.os.IBinder;
+import android.os.Parcel;
import android.os.PersistableBundle;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
@@ -187,7 +188,7 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mPictureProfileLock")
@Override
- public PictureProfile createPictureProfile(PictureProfile pp, UserHandle user) {
+ public PictureProfile createPictureProfile(PictureProfile pp, int userId) {
if ((pp.getPackageName() != null && !pp.getPackageName().isEmpty()
&& !incomingPackageEqualsCallingUidPackage(pp.getPackageName()))
&& !hasGlobalPictureQualityServicePermission()) {
@@ -221,7 +222,7 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mPictureProfileLock")
@Override
- public void updatePictureProfile(String id, PictureProfile pp, UserHandle user) {
+ public void updatePictureProfile(String id, PictureProfile pp, int userId) {
Long dbId = mPictureProfileTempIdMap.getKey(id);
if (!hasPermissionToUpdatePictureProfile(dbId, pp)) {
mMqManagerNotifier.notifyOnPictureProfileError(id,
@@ -249,7 +250,7 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mPictureProfileLock")
@Override
- public void removePictureProfile(String id, UserHandle user) {
+ public void removePictureProfile(String id, int userId) {
synchronized (mPictureProfileLock) {
Long dbId = mPictureProfileTempIdMap.getKey(id);
@@ -290,10 +291,8 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mPictureProfileLock")
@Override
- public PictureProfile getPictureProfile(int type, String name, Bundle options,
- UserHandle user) {
- boolean includeParams =
- options.getBoolean(MediaQualityManager.OPTION_INCLUDE_PARAMETERS, false);
+ public PictureProfile getPictureProfile(int type, String name, boolean includeParams,
+ int userId) {
String selection = BaseParameters.PARAMETER_TYPE + " = ? AND "
+ BaseParameters.PARAMETER_NAME + " = ? AND "
+ BaseParameters.PARAMETER_PACKAGE + " = ?";
@@ -327,7 +326,7 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mPictureProfileLock")
@Override
public List<PictureProfile> getPictureProfilesByPackage(
- String packageName, Bundle options, UserHandle user) {
+ String packageName, boolean includeParams, int userId) {
if (!hasGlobalPictureQualityServicePermission()) {
mMqManagerNotifier.notifyOnPictureProfileError(null,
PictureProfile.ERROR_NO_PERMISSION,
@@ -335,8 +334,6 @@ public class MediaQualityService extends SystemService {
}
synchronized (mPictureProfileLock) {
- boolean includeParams =
- options.getBoolean(MediaQualityManager.OPTION_INCLUDE_PARAMETERS, false);
String selection = BaseParameters.PARAMETER_PACKAGE + " = ?";
String[] selectionArguments = {packageName};
return mMqDatabaseUtils.getPictureProfilesBasedOnConditions(MediaQualityUtils
@@ -347,17 +344,17 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mPictureProfileLock")
@Override
- public List<PictureProfile> getAvailablePictureProfiles(Bundle options, UserHandle user) {
+ public List<PictureProfile> getAvailablePictureProfiles(boolean includeParams, int userId) {
String packageName = getPackageOfCallingUid();
if (packageName != null) {
- return getPictureProfilesByPackage(packageName, options, user);
+ return getPictureProfilesByPackage(packageName, includeParams, userId);
}
return new ArrayList<>();
}
@GuardedBy("mPictureProfileLock")
@Override
- public boolean setDefaultPictureProfile(String profileId, UserHandle user) {
+ public boolean setDefaultPictureProfile(String profileId, int userId) {
if (!hasGlobalPictureQualityServicePermission()) {
mMqManagerNotifier.notifyOnPictureProfileError(profileId,
PictureProfile.ERROR_NO_PERMISSION,
@@ -370,13 +367,21 @@ public class MediaQualityService extends SystemService {
try {
if (mMediaQuality != null) {
+ PictureParameters pp = new PictureParameters();
PictureParameter[] pictureParameters = MediaQualityUtils
.convertPersistableBundleToPictureParameterList(params);
- PictureParameters pp = new PictureParameters();
+ PersistableBundle vendorPictureParameters = params
+ .getPersistableBundle(BaseParameters.VENDOR_PARAMETERS);
+ Parcel parcel = Parcel.obtain();
+ if (vendorPictureParameters != null) {
+ setVendorPictureParameters(pp, parcel, vendorPictureParameters);
+ }
+
pp.pictureParameters = pictureParameters;
mMediaQuality.sendDefaultPictureParameters(pp);
+ parcel.recycle();
return true;
}
} catch (RemoteException e) {
@@ -387,7 +392,7 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mPictureProfileLock")
@Override
- public List<String> getPictureProfilePackageNames(UserHandle user) {
+ public List<String> getPictureProfilePackageNames(int userId) {
if (!hasGlobalPictureQualityServicePermission()) {
mMqManagerNotifier.notifyOnPictureProfileError(null,
PictureProfile.ERROR_NO_PERMISSION,
@@ -406,7 +411,7 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mPictureProfileLock")
@Override
- public List<PictureProfileHandle> getPictureProfileHandle(String[] ids, UserHandle user) {
+ public List<PictureProfileHandle> getPictureProfileHandle(String[] ids, int userId) {
List<PictureProfileHandle> toReturn = new ArrayList<>();
synchronized (mPictureProfileLock) {
for (String id : ids) {
@@ -423,13 +428,13 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mSoundProfileLock")
@Override
- public List<SoundProfileHandle> getSoundProfileHandle(String[] ids, UserHandle user) {
+ public List<SoundProfileHandle> getSoundProfileHandle(String[] ids, int userId) {
List<SoundProfileHandle> toReturn = new ArrayList<>();
synchronized (mSoundProfileLock) {
for (String id : ids) {
Long key = mSoundProfileTempIdMap.getKey(id);
if (key != null) {
- toReturn.add(new SoundProfileHandle(key));
+ toReturn.add(MediaQualityUtils.SOUND_PROFILE_HANDLE_NONE);
} else {
toReturn.add(null);
}
@@ -440,7 +445,7 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mSoundProfileLock")
@Override
- public SoundProfile createSoundProfile(SoundProfile sp, UserHandle user) {
+ public SoundProfile createSoundProfile(SoundProfile sp, int userId) {
if ((sp.getPackageName() != null && !sp.getPackageName().isEmpty()
&& !incomingPackageEqualsCallingUidPackage(sp.getPackageName()))
&& !hasGlobalPictureQualityServicePermission()) {
@@ -473,7 +478,7 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mSoundProfileLock")
@Override
- public void updateSoundProfile(String id, SoundProfile sp, UserHandle user) {
+ public void updateSoundProfile(String id, SoundProfile sp, int userId) {
Long dbId = mSoundProfileTempIdMap.getKey(id);
if (!hasPermissionToUpdateSoundProfile(dbId, sp)) {
mMqManagerNotifier.notifyOnSoundProfileError(id, SoundProfile.ERROR_NO_PERMISSION,
@@ -502,7 +507,7 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mSoundProfileLock")
@Override
- public void removeSoundProfile(String id, UserHandle user) {
+ public void removeSoundProfile(String id, int userId) {
synchronized (mSoundProfileLock) {
Long dbId = mSoundProfileTempIdMap.getKey(id);
SoundProfile toDelete = mMqDatabaseUtils.getSoundProfile(dbId);
@@ -542,10 +547,8 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mSoundProfileLock")
@Override
- public SoundProfile getSoundProfile(int type, String name, Bundle options,
- UserHandle user) {
- boolean includeParams =
- options.getBoolean(MediaQualityManager.OPTION_INCLUDE_PARAMETERS, false);
+ public SoundProfile getSoundProfile(int type, String name, boolean includeParams,
+ int userId) {
String selection = BaseParameters.PARAMETER_TYPE + " = ? AND "
+ BaseParameters.PARAMETER_NAME + " = ? AND "
+ BaseParameters.PARAMETER_PACKAGE + " = ?";
@@ -579,15 +582,13 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mSoundProfileLock")
@Override
public List<SoundProfile> getSoundProfilesByPackage(
- String packageName, Bundle options, UserHandle user) {
+ String packageName, boolean includeParams, int userId) {
if (!hasGlobalSoundQualityServicePermission()) {
mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION,
Binder.getCallingUid(), Binder.getCallingPid());
}
synchronized (mSoundProfileLock) {
- boolean includeParams =
- options.getBoolean(MediaQualityManager.OPTION_INCLUDE_PARAMETERS, false);
String selection = BaseParameters.PARAMETER_PACKAGE + " = ?";
String[] selectionArguments = {packageName};
return mMqDatabaseUtils.getSoundProfilesBasedOnConditions(MediaQualityUtils
@@ -598,17 +599,17 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mSoundProfileLock")
@Override
- public List<SoundProfile> getAvailableSoundProfiles(Bundle options, UserHandle user) {
+ public List<SoundProfile> getAvailableSoundProfiles(boolean includeParams, int userId) {
String packageName = getPackageOfCallingUid();
if (packageName != null) {
- return getSoundProfilesByPackage(packageName, options, user);
+ return getSoundProfilesByPackage(packageName, includeParams, userId);
}
return new ArrayList<>();
}
@GuardedBy("mSoundProfileLock")
@Override
- public boolean setDefaultSoundProfile(String profileId, UserHandle user) {
+ public boolean setDefaultSoundProfile(String profileId, int userId) {
if (!hasGlobalSoundQualityServicePermission()) {
mMqManagerNotifier.notifyOnSoundProfileError(profileId,
SoundProfile.ERROR_NO_PERMISSION,
@@ -638,7 +639,7 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mSoundProfileLock")
@Override
- public List<String> getSoundProfilePackageNames(UserHandle user) {
+ public List<String> getSoundProfilePackageNames(int userId) {
if (!hasGlobalSoundQualityServicePermission()) {
mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION,
Binder.getCallingUid(), Binder.getCallingPid());
@@ -737,7 +738,7 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mAmbientBacklightLock")
@Override
public void setAmbientBacklightSettings(
- AmbientBacklightSettings settings, UserHandle user) {
+ AmbientBacklightSettings settings, int userId) {
if (DEBUG) {
Slogf.d(TAG, "setAmbientBacklightSettings " + settings);
}
@@ -775,7 +776,7 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mAmbientBacklightLock")
@Override
- public void setAmbientBacklightEnabled(boolean enabled, UserHandle user) {
+ public void setAmbientBacklightEnabled(boolean enabled, int userId) {
if (DEBUG) {
Slogf.d(TAG, "setAmbientBacklightEnabled " + enabled);
}
@@ -795,7 +796,7 @@ public class MediaQualityService extends SystemService {
@Override
public List<ParameterCapability> getParameterCapabilities(
- List<String> names, UserHandle user) {
+ List<String> names, int userId) {
byte[] byteArray = MediaQualityUtils.convertParameterToByteArray(names);
ParamCapability[] caps = new ParamCapability[byteArray.length];
try {
@@ -828,7 +829,7 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mPictureProfileLock")
@Override
- public List<String> getPictureProfileAllowList(UserHandle user) {
+ public List<String> getPictureProfileAllowList(int userId) {
if (!hasGlobalPictureQualityServicePermission()) {
mMqManagerNotifier.notifyOnPictureProfileError(null,
PictureProfile.ERROR_NO_PERMISSION,
@@ -844,7 +845,7 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mPictureProfileLock")
@Override
- public void setPictureProfileAllowList(List<String> packages, UserHandle user) {
+ public void setPictureProfileAllowList(List<String> packages, int userId) {
if (!hasGlobalPictureQualityServicePermission()) {
mMqManagerNotifier.notifyOnPictureProfileError(null,
PictureProfile.ERROR_NO_PERMISSION,
@@ -857,7 +858,7 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mSoundProfileLock")
@Override
- public List<String> getSoundProfileAllowList(UserHandle user) {
+ public List<String> getSoundProfileAllowList(int userId) {
if (!hasGlobalSoundQualityServicePermission()) {
mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION,
Binder.getCallingUid(), Binder.getCallingPid());
@@ -872,7 +873,7 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mSoundProfileLock")
@Override
- public void setSoundProfileAllowList(List<String> packages, UserHandle user) {
+ public void setSoundProfileAllowList(List<String> packages, int userId) {
if (!hasGlobalSoundQualityServicePermission()) {
mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION,
Binder.getCallingUid(), Binder.getCallingPid());
@@ -883,13 +884,13 @@ public class MediaQualityService extends SystemService {
}
@Override
- public boolean isSupported(UserHandle user) {
+ public boolean isSupported(int userId) {
return false;
}
@GuardedBy("mPictureProfileLock")
@Override
- public void setAutoPictureQualityEnabled(boolean enabled, UserHandle user) {
+ public void setAutoPictureQualityEnabled(boolean enabled, int userId) {
if (!hasGlobalPictureQualityServicePermission()) {
mMqManagerNotifier.notifyOnPictureProfileError(null,
PictureProfile.ERROR_NO_PERMISSION,
@@ -910,7 +911,7 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mPictureProfileLock")
@Override
- public boolean isAutoPictureQualityEnabled(UserHandle user) {
+ public boolean isAutoPictureQualityEnabled(int userId) {
synchronized (mPictureProfileLock) {
try {
if (mMediaQuality != null) {
@@ -927,7 +928,7 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mPictureProfileLock")
@Override
- public void setSuperResolutionEnabled(boolean enabled, UserHandle user) {
+ public void setSuperResolutionEnabled(boolean enabled, int userId) {
if (!hasGlobalPictureQualityServicePermission()) {
mMqManagerNotifier.notifyOnPictureProfileError(null,
PictureProfile.ERROR_NO_PERMISSION,
@@ -948,7 +949,7 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mPictureProfileLock")
@Override
- public boolean isSuperResolutionEnabled(UserHandle user) {
+ public boolean isSuperResolutionEnabled(int userId) {
synchronized (mPictureProfileLock) {
try {
if (mMediaQuality != null) {
@@ -965,7 +966,7 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mSoundProfileLock")
@Override
- public void setAutoSoundQualityEnabled(boolean enabled, UserHandle user) {
+ public void setAutoSoundQualityEnabled(boolean enabled, int userId) {
if (!hasGlobalSoundQualityServicePermission()) {
mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION,
Binder.getCallingUid(), Binder.getCallingPid());
@@ -986,7 +987,7 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mSoundProfileLock")
@Override
- public boolean isAutoSoundQualityEnabled(UserHandle user) {
+ public boolean isAutoSoundQualityEnabled(int userId) {
synchronized (mSoundProfileLock) {
try {
if (mMediaQuality != null) {
@@ -1003,7 +1004,7 @@ public class MediaQualityService extends SystemService {
@GuardedBy("mAmbientBacklightLock")
@Override
- public boolean isAmbientBacklightEnabled(UserHandle user) {
+ public boolean isAmbientBacklightEnabled(int userId) {
return false;
}
}
@@ -1428,11 +1429,19 @@ public class MediaQualityService extends SystemService {
MediaQualityUtils.convertPersistableBundleToPictureParameterList(
params);
+ PersistableBundle vendorPictureParameters = params
+ .getPersistableBundle(BaseParameters.VENDOR_PARAMETERS);
+ Parcel parcel = Parcel.obtain();
+ if (vendorPictureParameters != null) {
+ setVendorPictureParameters(pictureParameters, parcel, vendorPictureParameters);
+ }
+
android.hardware.tv.mediaquality.PictureProfile toReturn =
new android.hardware.tv.mediaquality.PictureProfile();
toReturn.pictureProfileId = id;
toReturn.parameters = pictureParameters;
+ parcel.recycle();
return toReturn;
}
@@ -1738,4 +1747,16 @@ public class MediaQualityService extends SystemService {
return android.hardware.tv.mediaquality.IMediaQualityCallback.Stub.VERSION;
}
}
+
+ private void setVendorPictureParameters(
+ PictureParameters pictureParameters,
+ Parcel parcel,
+ PersistableBundle vendorPictureParameters) {
+ vendorPictureParameters.writeToParcel(parcel, 0);
+ byte[] vendorBundleToByteArray = parcel.marshall();
+ DefaultExtension defaultExtension = new DefaultExtension();
+ defaultExtension.bytes = Arrays.copyOf(
+ vendorBundleToByteArray, vendorBundleToByteArray.length);
+ pictureParameters.vendorPictureParameters.setParcelable(defaultExtension);
+ }
}
diff --git a/services/core/java/com/android/server/media/quality/MediaQualityUtils.java b/services/core/java/com/android/server/media/quality/MediaQualityUtils.java
index 88d3f1ff7c52..303c96750098 100644
--- a/services/core/java/com/android/server/media/quality/MediaQualityUtils.java
+++ b/services/core/java/com/android/server/media/quality/MediaQualityUtils.java
@@ -60,6 +60,11 @@ public final class MediaQualityUtils {
private static final String TAG = "MediaQualityUtils";
public static final String SETTINGS = "settings";
+ public static final SoundProfileHandle SOUND_PROFILE_HANDLE_NONE = new SoundProfileHandle();
+ static {
+ SOUND_PROFILE_HANDLE_NONE.id = -10000;
+ }
+
/**
* Convert PictureParameter List to PersistableBundle.
*/
@@ -1022,7 +1027,7 @@ public final class MediaQualityUtils {
getInputId(cursor),
getPackageName(cursor),
jsonToPersistableBundle(getSettingsString(cursor)),
- SoundProfileHandle.NONE
+ SOUND_PROFILE_HANDLE_NONE
);
}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index bfe0d32f4cb6..7a544cf1c26c 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -1075,6 +1075,7 @@ public class NotificationManagerService extends SystemService {
summary.getSbn().getNotification().getGroupAlertBehavior();
if (notificationForceGrouping()) {
+ summary.getNotification().flags |= Notification.FLAG_SILENT;
if (!summary.getChannel().getId().equals(summaryAttr.channelId)) {
NotificationChannel newChannel = mPreferencesHelper.getNotificationChannel(pkg,
summary.getUid(), summaryAttr.channelId, false);
@@ -7450,6 +7451,7 @@ public class NotificationManagerService extends SystemService {
// Override group key early for forced grouped notifications
r.setOverrideGroupKey(groupName);
}
+ r.getNotification().flags |= Notification.FLAG_SILENT;
}
addAutoGroupAdjustment(r, groupName);
diff --git a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
index ee29849465e2..737d943f084d 100644
--- a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
+++ b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
@@ -308,7 +308,9 @@ final class OverlayManagerServiceImpl {
Slog.d(TAG, "onPackageRemoved pkgName=" + pkgName + " userId=" + userId);
}
// Update the state of all overlays that target this package.
- final Set<UserPackage> targets = updateOverlaysForTarget(pkgName, userId, 0 /* flags */);
+ Set<UserPackage> targets = Collections.emptySet();
+ targets = CollectionUtils.addAll(targets,
+ updateOverlaysForTarget(pkgName, userId, 0 /* flags */));
// Remove all the overlays this package declares.
return CollectionUtils.addAll(targets,
diff --git a/services/core/java/com/android/server/os/instrumentation/OWNERS b/services/core/java/com/android/server/os/instrumentation/OWNERS
new file mode 100644
index 000000000000..2522426d93f8
--- /dev/null
+++ b/services/core/java/com/android/server/os/instrumentation/OWNERS
@@ -0,0 +1 @@
+include platform/packages/modules/UprobeStats:/OWNERS \ No newline at end of file
diff --git a/services/core/java/com/android/server/pm/InstallRequest.java b/services/core/java/com/android/server/pm/InstallRequest.java
index 734920435e26..3361dbc2df07 100644
--- a/services/core/java/com/android/server/pm/InstallRequest.java
+++ b/services/core/java/com/android/server/pm/InstallRequest.java
@@ -644,20 +644,6 @@ final class InstallRequest {
return mScanResult.mPkgSetting;
}
- @Nullable
- public PackageSetting getRealPackageSetting() {
- // TODO: Fix this to have 1 mutable PackageSetting for scan/install. If the previous
- // setting needs to be passed to have a comparison, hide it behind an immutable
- // interface. There's no good reason to have 3 different ways to access the real
- // PackageSetting object, only one of which is actually correct.
- PackageSetting realPkgSetting = isExistingSettingCopied()
- ? getScanRequestPackageSetting() : getScannedPackageSetting();
- if (realPkgSetting == null) {
- realPkgSetting = getScannedPackageSetting();
- }
- return realPkgSetting;
- }
-
public boolean isExistingSettingCopied() {
assertScanResultExists();
return mScanResult.mExistingSettingCopied;
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 93837b34f7b3..092ec8ef4a8a 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -7490,6 +7490,7 @@ public class UserManagerService extends IUserManager.Stub {
final long now = System.currentTimeMillis();
final long nowRealtime = SystemClock.elapsedRealtime();
final StringBuilder sb = new StringBuilder();
+ final Resources resources = Resources.getSystem();
if (args != null && args.length > 0) {
switch (args[0]) {
@@ -7570,10 +7571,15 @@ public class UserManagerService extends IUserManager.Stub {
// Dump some capabilities
pw.println();
- pw.print(" Max users: " + UserManager.getMaxSupportedUsers());
+ int effectiveMaxSupportedUsers = UserManager.getMaxSupportedUsers();
+ pw.print(" Max users: " + effectiveMaxSupportedUsers);
+ int defaultMaxSupportedUsers = resources.getInteger(R.integer.config_multiuserMaximumUsers);
+ if (effectiveMaxSupportedUsers != defaultMaxSupportedUsers) {
+ pw.print(" (built-in value: " + defaultMaxSupportedUsers + ")");
+ }
pw.println(" (limit reached: " + isUserLimitReached() + ")");
pw.println(" Supports switchable users: " + UserManager.supportsMultipleUsers());
- pw.println(" All guests ephemeral: " + Resources.getSystem().getBoolean(
+ pw.println(" All guests ephemeral: " + resources.getBoolean(
com.android.internal.R.bool.config_guestUserEphemeral));
pw.println(" Force ephemeral users: " + mForceEphemeralUsers);
final boolean isHeadlessSystemUserMode = isHeadlessSystemUserMode();
@@ -7588,7 +7594,7 @@ public class UserManagerService extends IUserManager.Stub {
}
}
if (isHeadlessSystemUserMode) {
- pw.println(" Can switch to headless system user: " + Resources.getSystem()
+ pw.println(" Can switch to headless system user: " + resources
.getBoolean(com.android.internal.R.bool.config_canSwitchToHeadlessSystemUser));
}
pw.println(" User version: " + mUserVersion);
diff --git a/services/core/java/com/android/server/pm/UserTypeFactory.java b/services/core/java/com/android/server/pm/UserTypeFactory.java
index 58c5b1c90a66..5798aa919d96 100644
--- a/services/core/java/com/android/server/pm/UserTypeFactory.java
+++ b/services/core/java/com/android/server/pm/UserTypeFactory.java
@@ -36,6 +36,7 @@ import static android.os.UserManager.USER_TYPE_PROFILE_CLONE;
import static android.os.UserManager.USER_TYPE_PROFILE_COMMUNAL;
import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED;
import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE;
+import static android.os.UserManager.USER_TYPE_PROFILE_SUPERVISING;
import static android.os.UserManager.USER_TYPE_PROFILE_TEST;
import static android.os.UserManager.USER_TYPE_SYSTEM_HEADLESS;
@@ -111,6 +112,7 @@ public final class UserTypeFactory {
builders.put(USER_TYPE_PROFILE_CLONE, getDefaultTypeProfileClone());
builders.put(USER_TYPE_PROFILE_COMMUNAL, getDefaultTypeProfileCommunal());
builders.put(USER_TYPE_PROFILE_PRIVATE, getDefaultTypeProfilePrivate());
+ builders.put(USER_TYPE_PROFILE_SUPERVISING, getDefaultTypeProfileSupervising());
if (Build.IS_DEBUGGABLE) {
builders.put(USER_TYPE_PROFILE_TEST, getDefaultTypeProfileTest());
}
@@ -343,6 +345,29 @@ public final class UserTypeFactory {
}
/**
+ * Returns the Builder for the default {@link UserManager#USER_TYPE_PROFILE_SUPERVISING}
+ * configuration.
+ */
+ private static UserTypeDetails.Builder getDefaultTypeProfileSupervising() {
+ return new UserTypeDetails.Builder()
+ .setName(USER_TYPE_PROFILE_SUPERVISING)
+ .setBaseType(FLAG_PROFILE)
+ .setMaxAllowed(1)
+ .setProfileParentRequired(false)
+ .setEnabled(android.multiuser.Flags.allowSupervisingProfile() ? 1 : 0)
+ .setLabels(R.string.profile_label_supervising)
+ .setDefaultRestrictions(getDefaultSupervisingProfileRestrictions())
+ .setDefaultSecureSettings(getDefaultNonManagedProfileSecureSettings())
+ .setDefaultUserProperties(new UserProperties.Builder()
+ .setStartWithParent(false)
+ .setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_NO)
+ .setShowInSettings(UserProperties.SHOW_IN_SETTINGS_NO)
+ .setShowInQuietMode(UserProperties.SHOW_IN_QUIET_MODE_HIDDEN)
+ .setCredentialShareableWithParent(false)
+ .setAlwaysVisible(true));
+ }
+
+ /**
* Returns the Builder for the default {@link UserManager#USER_TYPE_FULL_SECONDARY}
* configuration.
*/
@@ -449,6 +474,12 @@ public final class UserTypeFactory {
return restrictions;
}
+ private static Bundle getDefaultSupervisingProfileRestrictions() {
+ final Bundle restrictions = getDefaultProfileRestrictions();
+ restrictions.putBoolean(UserManager.DISALLOW_INSTALL_APPS, true);
+ return restrictions;
+ }
+
private static Bundle getDefaultManagedProfileSecureSettings() {
// Only add String values to the bundle, settings are written as Strings eventually
final Bundle settings = new Bundle();
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 46dc75817a36..3230e891db55 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -4240,66 +4240,14 @@ public class PhoneWindowManager implements WindowManagerPolicy {
if (!useKeyGestureEventHandler()) {
return;
}
- mInputManager.registerKeyGestureEventHandler(new InputManager.KeyGestureEventHandler() {
- @Override
- public boolean handleKeyGestureEvent(@NonNull KeyGestureEvent event,
- @Nullable IBinder focusedToken) {
- boolean handled = PhoneWindowManager.this.handleKeyGestureEvent(event,
- focusedToken);
- if (handled && !event.isCancelled() && Arrays.stream(event.getKeycodes()).anyMatch(
- (keycode) -> keycode == KeyEvent.KEYCODE_POWER)) {
- mPowerKeyHandled = true;
- }
- return handled;
- }
-
- @Override
- public boolean isKeyGestureSupported(int gestureType) {
- switch (gestureType) {
- case KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS:
- case KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH:
- case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT:
- case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT:
- case KeyGestureEvent.KEY_GESTURE_TYPE_HOME:
- case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS:
- case KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT:
- case KeyGestureEvent.KEY_GESTURE_TYPE_BACK:
- case KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION:
- case KeyGestureEvent.KEY_GESTURE_TYPE_DESKTOP_MODE:
- case KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT:
- case KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT:
- case KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER:
- case KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_UP:
- case KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN:
- case KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER:
- case KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS:
- case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS:
- case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH:
- case KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH:
- case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT:
- case KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS:
- case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB:
- case KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD:
- case KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD:
- case KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS:
- return true;
- case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD:
- return mAccessibilityShortcutController.isAccessibilityShortcutAvailable(
- isKeyguardLocked());
- case KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD:
- return mAccessibilityShortcutController.isAccessibilityShortcutAvailable(
- false);
- default:
- return false;
- }
+ mInputManager.registerKeyGestureEventHandler((event, focusedToken) -> {
+ boolean handled = PhoneWindowManager.this.handleKeyGestureEvent(event,
+ focusedToken);
+ if (handled && !event.isCancelled() && Arrays.stream(event.getKeycodes()).anyMatch(
+ (keycode) -> keycode == KeyEvent.KEYCODE_POWER)) {
+ mPowerKeyHandled = true;
}
+ return handled;
});
}
@@ -4457,13 +4405,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
cancelPendingScreenshotChordAction();
}
return true;
- case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD:
- if (start) {
- interceptAccessibilityShortcutChord();
- } else {
- cancelPendingAccessibilityShortcutAction();
- }
- return true;
case KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD:
if (start) {
interceptRingerToggleChord();
@@ -4481,14 +4422,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
cancelGlobalActionsAction();
}
return true;
- // TODO (b/358569822): Consolidate TV and non-TV gestures into same KeyGestureEvent
- case KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD:
- if (start) {
- interceptAccessibilityGestureTv();
- } else {
- cancelAccessibilityGestureTv();
- }
- return true;
case KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT:
if (start) {
interceptBugreportGestureTv();
diff --git a/services/core/java/com/android/server/power/ScreenUndimDetector.java b/services/core/java/com/android/server/power/ScreenUndimDetector.java
index c4929c210e2c..b376417061db 100644
--- a/services/core/java/com/android/server/power/ScreenUndimDetector.java
+++ b/services/core/java/com/android/server/power/ScreenUndimDetector.java
@@ -30,11 +30,11 @@ import android.provider.DeviceConfig;
import android.util.Slog;
import android.view.Display;
+import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.FrameworkStatsLog;
import java.util.Set;
-import java.util.concurrent.TimeUnit;
/**
* Detects when user manually undims the screen (x times) and acquires a wakelock to keep the screen
@@ -48,7 +48,6 @@ public class ScreenUndimDetector {
/** DeviceConfig flag: is keep screen on feature enabled. */
static final String KEY_KEEP_SCREEN_ON_ENABLED = "keep_screen_on_enabled";
- private static final boolean DEFAULT_KEEP_SCREEN_ON_ENABLED = true;
private static final int OUTCOME_POWER_BUTTON =
FrameworkStatsLog.TIMEOUT_AUTO_EXTENDED_REPORTED__OUTCOME__POWER_BUTTON;
private static final int OUTCOME_TIMEOUT =
@@ -58,15 +57,11 @@ public class ScreenUndimDetector {
/** DeviceConfig flag: how long should we keep the screen on. */
@VisibleForTesting
static final String KEY_KEEP_SCREEN_ON_FOR_MILLIS = "keep_screen_on_for_millis";
- @VisibleForTesting
- static final long DEFAULT_KEEP_SCREEN_ON_FOR_MILLIS = TimeUnit.MINUTES.toMillis(10);
private long mKeepScreenOnForMillis;
/** DeviceConfig flag: how many user undims required to trigger keeping the screen on. */
@VisibleForTesting
static final String KEY_UNDIMS_REQUIRED = "undims_required";
- @VisibleForTesting
- static final int DEFAULT_UNDIMS_REQUIRED = 2;
private int mUndimsRequired;
/**
@@ -76,8 +71,6 @@ public class ScreenUndimDetector {
@VisibleForTesting
static final String KEY_MAX_DURATION_BETWEEN_UNDIMS_MILLIS =
"max_duration_between_undims_millis";
- @VisibleForTesting
- static final long DEFAULT_MAX_DURATION_BETWEEN_UNDIMS_MILLIS = TimeUnit.MINUTES.toMillis(5);
private long mMaxDurationBetweenUndimsMillis;
@VisibleForTesting
@@ -92,6 +85,7 @@ public class ScreenUndimDetector {
private long mUndimOccurredTime = -1;
private long mInteractionAfterUndimTime = -1;
private InternalClock mClock;
+ private Context mContext;
public ScreenUndimDetector() {
mClock = new InternalClock();
@@ -109,12 +103,13 @@ public class ScreenUndimDetector {
/** Should be called in parent's systemReady() */
public void systemReady(Context context) {
+ mContext = context;
readValuesFromDeviceConfig();
DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_ATTENTION_MANAGER_SERVICE,
- context.getMainExecutor(),
+ mContext.getMainExecutor(),
(properties) -> onDeviceConfigChange(properties.getKeyset()));
- final PowerManager powerManager = context.getSystemService(PowerManager.class);
+ final PowerManager powerManager = mContext.getSystemService(PowerManager.class);
mWakeLock = powerManager.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK
| PowerManager.ON_AFTER_RELEASE,
UNDIM_DETECTOR_WAKE_LOCK);
@@ -203,36 +198,44 @@ public class ScreenUndimDetector {
}
}
- private boolean readKeepScreenOnNotificationEnabled() {
+ private boolean readKeepScreenOnEnabled() {
+ boolean defaultKeepScreenOnEnabled = mContext.getResources().getBoolean(
+ R.bool.config_defaultPreventScreenTimeoutEnabled);
return DeviceConfig.getBoolean(NAMESPACE_ATTENTION_MANAGER_SERVICE,
KEY_KEEP_SCREEN_ON_ENABLED,
- DEFAULT_KEEP_SCREEN_ON_ENABLED);
+ defaultKeepScreenOnEnabled);
}
private long readKeepScreenOnForMillis() {
+ long defaultKeepScreenOnDuration = mContext.getResources().getInteger(
+ R.integer.config_defaultPreventScreenTimeoutForMillis);
return DeviceConfig.getLong(NAMESPACE_ATTENTION_MANAGER_SERVICE,
KEY_KEEP_SCREEN_ON_FOR_MILLIS,
- DEFAULT_KEEP_SCREEN_ON_FOR_MILLIS);
+ defaultKeepScreenOnDuration);
}
private int readUndimsRequired() {
+ int defaultUndimsRequired = mContext.getResources().getInteger(
+ R.integer.config_defaultUndimsRequired);
int undimsRequired = DeviceConfig.getInt(NAMESPACE_ATTENTION_MANAGER_SERVICE,
KEY_UNDIMS_REQUIRED,
- DEFAULT_UNDIMS_REQUIRED);
+ defaultUndimsRequired);
if (undimsRequired < 1 || undimsRequired > 5) {
Slog.e(TAG, "Provided undimsRequired=" + undimsRequired
- + " is not allowed [1, 5]; using the default=" + DEFAULT_UNDIMS_REQUIRED);
- return DEFAULT_UNDIMS_REQUIRED;
+ + " is not allowed [1, 5]; using the default=" + defaultUndimsRequired);
+ return defaultUndimsRequired;
}
return undimsRequired;
}
private long readMaxDurationBetweenUndimsMillis() {
+ long defaultMaxDurationBetweenUndimsMillis = mContext.getResources().getInteger(
+ R.integer.config_defaultMaxDurationBetweenUndimsMillis);
return DeviceConfig.getLong(NAMESPACE_ATTENTION_MANAGER_SERVICE,
KEY_MAX_DURATION_BETWEEN_UNDIMS_MILLIS,
- DEFAULT_MAX_DURATION_BETWEEN_UNDIMS_MILLIS);
+ defaultMaxDurationBetweenUndimsMillis);
}
private void onDeviceConfigChange(@NonNull Set<String> keys) {
@@ -253,15 +256,16 @@ public class ScreenUndimDetector {
@VisibleForTesting
void readValuesFromDeviceConfig() {
- mKeepScreenOnEnabled = readKeepScreenOnNotificationEnabled();
+ mKeepScreenOnEnabled = readKeepScreenOnEnabled();
mKeepScreenOnForMillis = readKeepScreenOnForMillis();
mUndimsRequired = readUndimsRequired();
mMaxDurationBetweenUndimsMillis = readMaxDurationBetweenUndimsMillis();
Slog.i(TAG, "readValuesFromDeviceConfig():"
+ "\nmKeepScreenOnForMillis=" + mKeepScreenOnForMillis
- + "\nmKeepScreenOnNotificationEnabled=" + mKeepScreenOnEnabled
- + "\nmUndimsRequired=" + mUndimsRequired);
+ + "\nmKeepScreenOnEnabled=" + mKeepScreenOnEnabled
+ + "\nmUndimsRequired=" + mUndimsRequired
+ + "\nmMaxDurationBetweenUndimsMillis=" + mMaxDurationBetweenUndimsMillis);
}
diff --git a/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java b/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java
index 5563f98e8842..7cd9bdbc662c 100644
--- a/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java
+++ b/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java
@@ -374,6 +374,10 @@ public class BatteryHistoryDirectory implements BatteryStatsHistory.BatteryHisto
@SuppressWarnings("unchecked")
@Override
public List<BatteryHistoryFragment> getFragments() {
+ if (!mLock.isHeldByCurrentThread()) {
+ throw new IllegalStateException("Reading battery history without a lock");
+ }
+
ensureInitialized();
return (List<BatteryHistoryFragment>)
(List<? extends BatteryHistoryFragment>) mHistoryFiles;
@@ -443,44 +447,6 @@ public class BatteryHistoryDirectory implements BatteryStatsHistory.BatteryHisto
}
@Override
- public BatteryHistoryFragment getNextFragment(BatteryHistoryFragment current, long startTimeMs,
- long endTimeMs) {
- ensureInitialized();
-
- if (!mLock.isHeldByCurrentThread()) {
- throw new IllegalStateException("Iterating battery history without a lock");
- }
-
- int nextFileIndex = 0;
- int firstFileIndex = 0;
- // skip the last file because its data is in history buffer.
- int lastFileIndex = mHistoryFiles.size() - 2;
- for (int i = lastFileIndex; i >= 0; i--) {
- BatteryHistoryFragment fragment = mHistoryFiles.get(i);
- if (current != null && fragment.monotonicTimeMs == current.monotonicTimeMs) {
- nextFileIndex = i + 1;
- }
- if (fragment.monotonicTimeMs > endTimeMs) {
- lastFileIndex = i - 1;
- }
- if (fragment.monotonicTimeMs <= startTimeMs) {
- firstFileIndex = i;
- break;
- }
- }
-
- if (nextFileIndex < firstFileIndex) {
- nextFileIndex = firstFileIndex;
- }
-
- if (nextFileIndex <= lastFileIndex) {
- return mHistoryFiles.get(nextFileIndex);
- }
-
- return null;
- }
-
- @Override
public boolean hasCompletedFragments() {
ensureInitialized();
diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java
index 54365ff03db0..c5a43a57da82 100644
--- a/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java
+++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java
@@ -72,14 +72,20 @@ class SelinuxAuditLogsCollector {
}
SelinuxAuditLogsCollector(RateLimiter rateLimiter, QuotaLimiter quotaLimiter) {
- this(
- () ->
- DeviceConfig.getString(
- DeviceConfig.NAMESPACE_ADSERVICES,
- CONFIG_SELINUX_AUDIT_DOMAIN,
- DEFAULT_SELINUX_AUDIT_DOMAIN),
- rateLimiter,
- quotaLimiter);
+ this(new DefaultDomainSupplier(), rateLimiter, quotaLimiter);
+ }
+
+ private static class DefaultDomainSupplier implements Supplier<String> {
+ @Override
+ public String get() {
+ if (SelinuxAuditLogsService.enabledForAllDomains()) {
+ return "\\w+";
+ }
+ return DeviceConfig.getString(
+ DeviceConfig.NAMESPACE_ADSERVICES,
+ CONFIG_SELINUX_AUDIT_DOMAIN,
+ DEFAULT_SELINUX_AUDIT_DOMAIN);
+ }
}
public void setStopRequested(boolean stopRequested) {
diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogsService.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogsService.java
index d46e8916d9e9..9dc457c5d63b 100644
--- a/services/core/java/com/android/server/selinux/SelinuxAuditLogsService.java
+++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogsService.java
@@ -16,6 +16,7 @@
package com.android.server.selinux;
import static com.android.sdksandbox.flags.Flags.selinuxSdkSandboxAudit;
+import static com.android.server.selinux.flags.Flags.selinuxLogsCollect;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
@@ -49,6 +50,9 @@ public class SelinuxAuditLogsService extends JobService {
"selinux_audit_job_frequency_hours";
private static final String CONFIG_SELINUX_ENABLE_AUDIT_JOB = "selinux_enable_audit_job";
private static final String CONFIG_SELINUX_AUDIT_CAP = "selinux_audit_cap";
+ private static final String DEVICE_CONFIG_SECURITY_NAMESPACE = "security";
+ private static final String CONFIG_SECURITY_SELINUX_AUDIT_JOB_ENABLED =
+ "selinux_audit_job_enabled";
private static final int MAX_PERMITS_CAP_DEFAULT = 50000;
private static final int SELINUX_AUDIT_JOB_ID = 25327386;
@@ -76,7 +80,7 @@ public class SelinuxAuditLogsService extends JobService {
/** Schedule jobs with the {@link JobScheduler}. */
public static void schedule(Context context) {
- if (!selinuxSdkSandboxAudit()) {
+ if (!selinuxSdkSandboxAudit() && !enabledForAllDomains()) {
Slog.d(TAG, "SelinuxAuditLogsService not enabled");
return;
}
@@ -86,13 +90,20 @@ public class SelinuxAuditLogsService extends JobService {
return;
}
- LogsCollectorJobScheduler propertiesListener =
+ LogsCollectorJobScheduler scheduler =
new LogsCollectorJobScheduler(
context.getSystemService(JobScheduler.class)
.forNamespace(SELINUX_AUDIT_NAMESPACE));
- propertiesListener.schedule();
+ scheduler.schedule();
+
+ AdServicesPropertyMonitor adServicesProperties = new AdServicesPropertyMonitor(scheduler);
+ DeviceConfig.addOnPropertiesChangedListener(
+ DeviceConfig.NAMESPACE_ADSERVICES, context.getMainExecutor(), adServicesProperties);
+
+ SecurityPropertyMonitor securityProperties = new SecurityPropertyMonitor(scheduler);
DeviceConfig.addOnPropertiesChangedListener(
- DeviceConfig.NAMESPACE_ADSERVICES, context.getMainExecutor(), propertiesListener);
+ DEVICE_CONFIG_SECURITY_NAMESPACE, context.getMainExecutor(), securityProperties);
+
}
@Override
@@ -101,7 +112,7 @@ public class SelinuxAuditLogsService extends JobService {
Slog.e(TAG, "The job id does not match the expected selinux job id.");
return false;
}
- if (!selinuxSdkSandboxAudit()) {
+ if (!selinuxSdkSandboxAudit() && !enabledForAllDomains()) {
Slog.i(TAG, "Selinux audit job disabled.");
return false;
}
@@ -123,17 +134,33 @@ public class SelinuxAuditLogsService extends JobService {
return false;
}
- /**
- * This class is in charge of scheduling the job service, and keeping the scheduling up to date
- * when the parameters change.
- */
- private static final class LogsCollectorJobScheduler
+ /** Checks if the service is enabled for all domains */
+ public static final boolean enabledForAllDomains() {
+ if (selinuxLogsCollect()) {
+ return DeviceConfig.getBoolean(
+ DEVICE_CONFIG_SECURITY_NAMESPACE,
+ CONFIG_SECURITY_SELINUX_AUDIT_JOB_ENABLED,
+ false);
+ }
+ return false;
+ }
+
+ /** Checks if the service is enabled for SDK Sandbox */
+ public static final boolean enabledForSdkSandbox() {
+ if (selinuxSdkSandboxAudit()) {
+ return DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_ADSERVICES, CONFIG_SELINUX_ENABLE_AUDIT_JOB, false);
+ }
+ return false;
+ }
+
+ private static final class AdServicesPropertyMonitor
implements DeviceConfig.OnPropertiesChangedListener {
- private final JobScheduler mJobScheduler;
+ private final LogsCollectorJobScheduler mScheduler;
- private LogsCollectorJobScheduler(JobScheduler jobScheduler) {
- mJobScheduler = jobScheduler;
+ private AdServicesPropertyMonitor(LogsCollectorJobScheduler scheduler) {
+ mScheduler = scheduler;
}
@Override
@@ -149,19 +176,65 @@ public class SelinuxAuditLogsService extends JobService {
if (keyset.contains(CONFIG_SELINUX_ENABLE_AUDIT_JOB)) {
boolean enabled =
changedProperties.getBoolean(
- CONFIG_SELINUX_ENABLE_AUDIT_JOB, /* defaultValue= */ false);
+ CONFIG_SELINUX_ENABLE_AUDIT_JOB, /* defaultValue= */ false)
+ || enabledForAllDomains();
if (enabled) {
- schedule();
+ mScheduler.schedule();
} else {
- mJobScheduler.cancel(SELINUX_AUDIT_JOB_ID);
+ mScheduler.cancel();
}
} else if (keyset.contains(CONFIG_SELINUX_AUDIT_JOB_FREQUENCY_HOURS)) {
// The job frequency changed, reschedule.
- schedule();
+ mScheduler.schedule();
}
}
+ }
+
+ private static final class SecurityPropertyMonitor
+ implements DeviceConfig.OnPropertiesChangedListener {
+
+ private final LogsCollectorJobScheduler mScheduler;
+
+ private SecurityPropertyMonitor(LogsCollectorJobScheduler scheduler) {
+ mScheduler = scheduler;
+ }
+
+ @Override
+ public void onPropertiesChanged(Properties changedProperties) {
+ Set<String> keyset = changedProperties.getKeyset();
+
+ if (keyset.contains(CONFIG_SECURITY_SELINUX_AUDIT_JOB_ENABLED)) {
+ boolean enabled =
+ changedProperties.getBoolean(
+ CONFIG_SECURITY_SELINUX_AUDIT_JOB_ENABLED,
+ /* defaultValue= */ false)
+ || enabledForSdkSandbox();
+ if (enabled) {
+ mScheduler.schedule();
+ } else {
+ mScheduler.cancel();
+ }
+ }
+ }
+ }
+
+ /**
+ * This class is in charge of scheduling the job service, and keeping the scheduling up to date
+ * when the parameters change.
+ */
+ private static final class LogsCollectorJobScheduler {
+
+ private final JobScheduler mJobScheduler;
+
+ private LogsCollectorJobScheduler(JobScheduler jobScheduler) {
+ mJobScheduler = jobScheduler;
+ }
+
+ public void cancel() {
+ mJobScheduler.cancel(SELINUX_AUDIT_JOB_ID);
+ }
- private void schedule() {
+ public void schedule() {
long frequencyMillis =
TimeUnit.HOURS.toMillis(
DeviceConfig.getInt(
diff --git a/services/core/java/com/android/server/selinux/flags.aconfig b/services/core/java/com/android/server/selinux/flags.aconfig
new file mode 100644
index 000000000000..3bb5a6bda1de
--- /dev/null
+++ b/services/core/java/com/android/server/selinux/flags.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.server.selinux.flags"
+container: "system"
+
+flag {
+ name: "selinux_logs_collect"
+ namespace: "network_security"
+ description: "Enable collection of SELinux denials based on selinux_audit_job_enabled"
+ bug: "372950125"
+}
diff --git a/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java b/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java
index 31348cd9156f..17980c02502f 100644
--- a/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java
+++ b/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java
@@ -177,6 +177,7 @@ public final class TextClassificationManagerService extends ITextClassifierServi
private final String mDefaultTextClassifierPackage;
@Nullable
private final String mSystemTextClassifierPackage;
+ private final MyPackageMonitor mPackageMonitor;
private TextClassificationManagerService(Context context) {
mContext = Objects.requireNonNull(context);
@@ -187,50 +188,50 @@ public final class TextClassificationManagerService extends ITextClassifierServi
mDefaultTextClassifierPackage = packageManager.getDefaultTextClassifierPackageName();
mSystemTextClassifierPackage = packageManager.getSystemTextClassifierPackageName();
mSessionCache = new SessionCache(mLock);
+ mPackageMonitor = new MyPackageMonitor();
}
private void startListenSettings() {
mSettingsListener.registerObserver();
}
- void startTrackingPackageChanges() {
- final PackageMonitor monitor = new PackageMonitor() {
-
- @Override
- public void onPackageAdded(String packageName, int uid) {
- notifyPackageInstallStatusChange(packageName, /* installed*/ true);
- }
+ private class MyPackageMonitor extends PackageMonitor {
+ @Override
+ public void onPackageAdded(String packageName, int uid) {
+ notifyPackageInstallStatusChange(packageName, /* installed*/ true);
+ }
- @Override
- public void onPackageRemoved(String packageName, int uid) {
- notifyPackageInstallStatusChange(packageName, /* installed= */ false);
- }
+ @Override
+ public void onPackageRemoved(String packageName, int uid) {
+ notifyPackageInstallStatusChange(packageName, /* installed= */ false);
+ }
- @Override
- public void onPackageModified(String packageName) {
- final int userId = getChangingUserId();
- synchronized (mLock) {
- final UserState userState = getUserStateLocked(userId);
- final ServiceState serviceState = userState.getServiceStateLocked(packageName);
- if (serviceState != null) {
- serviceState.onPackageModifiedLocked();
- }
+ @Override
+ public void onPackageModified(String packageName) {
+ final int userId = getChangingUserId();
+ synchronized (mLock) {
+ final UserState userState = getUserStateLocked(userId);
+ final ServiceState serviceState = userState.getServiceStateLocked(packageName);
+ if (serviceState != null) {
+ serviceState.onPackageModifiedLocked();
}
}
+ }
- private void notifyPackageInstallStatusChange(String packageName, boolean installed) {
- final int userId = getChangingUserId();
- synchronized (mLock) {
- final UserState userState = getUserStateLocked(userId);
- final ServiceState serviceState = userState.getServiceStateLocked(packageName);
- if (serviceState != null) {
- serviceState.onPackageInstallStatusChangeLocked(installed);
- }
+ private void notifyPackageInstallStatusChange(String packageName, boolean installed) {
+ final int userId = getChangingUserId();
+ synchronized (mLock) {
+ final UserState userState = getUserStateLocked(userId);
+ final ServiceState serviceState = userState.getServiceStateLocked(packageName);
+ if (serviceState != null) {
+ serviceState.onPackageInstallStatusChangeLocked(installed);
}
}
- };
+ }
+ }
- monitor.register(mContext, null, UserHandle.ALL, true);
+ void startTrackingPackageChanges() {
+ mPackageMonitor.register(mContext, null, UserHandle.ALL, true);
}
@Override
diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java
index 8bcf1a9be031..47d6879129ee 100644
--- a/services/core/java/com/android/server/tv/TvInputManagerService.java
+++ b/services/core/java/com/android/server/tv/TvInputManagerService.java
@@ -183,7 +183,7 @@ public final class TvInputManagerService extends SystemService {
private final Map<String, SessionState> mSessionIdToSessionStateMap = new HashMap<>();
private final MessageHandler mMessageHandler;
-
+ private final MyPackageMonitor mPackageMonitor;
private final ActivityManager mActivityManager;
private boolean mExternalInputLoggingDisplayNameFilterEnabled = false;
@@ -200,6 +200,7 @@ public final class TvInputManagerService extends SystemService {
mMessageHandler =
new MessageHandler(mContext.getContentResolver(), IoThread.get().getLooper());
mTvInputHardwareManager = new TvInputHardwareManager(context, new HardwareListener());
+ mPackageMonitor = new MyPackageMonitor(/* supportsPackageRestartQuery */ true);
mActivityManager =
(ActivityManager) getContext().getSystemService(Context.ACTIVITY_SERVICE);
@@ -298,74 +299,79 @@ public final class TvInputManagerService extends SystemService {
mExternalInputLoggingDeviceBrandNames.addAll(Arrays.asList(deviceBrandNames));
}
- private void registerBroadcastReceivers() {
- PackageMonitor monitor = new PackageMonitor(/* supportsPackageRestartQuery */ true) {
- private void buildTvInputList(String[] packages) {
- int userId = getChangingUserId();
- synchronized (mLock) {
- if (mCurrentUserId == userId || mRunningProfiles.contains(userId)) {
- buildTvInputListLocked(userId, packages);
- buildTvContentRatingSystemListLocked(userId);
- }
+ private class MyPackageMonitor extends PackageMonitor {
+ MyPackageMonitor(boolean supportsPackageRestartQuery) {
+ super(supportsPackageRestartQuery);
+ }
+
+ private void buildTvInputList(String[] packages) {
+ int userId = getChangingUserId();
+ synchronized (mLock) {
+ if (mCurrentUserId == userId || mRunningProfiles.contains(userId)) {
+ buildTvInputListLocked(userId, packages);
+ buildTvContentRatingSystemListLocked(userId);
}
}
+ }
- @Override
- public void onPackageUpdateFinished(String packageName, int uid) {
- if (DEBUG) Slog.d(TAG, "onPackageUpdateFinished(packageName=" + packageName + ")");
- // This callback is invoked when the TV input is reinstalled.
- // In this case, isReplacing() always returns true.
- buildTvInputList(new String[] { packageName });
- }
+ @Override
+ public void onPackageUpdateFinished(String packageName, int uid) {
+ if (DEBUG) Slog.d(TAG, "onPackageUpdateFinished(packageName=" + packageName + ")");
+ // This callback is invoked when the TV input is reinstalled.
+ // In this case, isReplacing() always returns true.
+ buildTvInputList(new String[] { packageName });
+ }
- @Override
- public void onPackagesAvailable(String[] packages) {
- if (DEBUG) {
- Slog.d(TAG, "onPackagesAvailable(packages=" + Arrays.toString(packages) + ")");
- }
- // This callback is invoked when the media on which some packages exist become
- // available.
- if (isReplacing()) {
- buildTvInputList(packages);
- }
+ @Override
+ public void onPackagesAvailable(String[] packages) {
+ if (DEBUG) {
+ Slog.d(TAG, "onPackagesAvailable(packages=" + Arrays.toString(packages) + ")");
}
+ // This callback is invoked when the media on which some packages exist become
+ // available.
+ if (isReplacing()) {
+ buildTvInputList(packages);
+ }
+ }
- @Override
- public void onPackagesUnavailable(String[] packages) {
- // This callback is invoked when the media on which some packages exist become
- // unavailable.
- if (DEBUG) {
- Slog.d(TAG, "onPackagesUnavailable(packages=" + Arrays.toString(packages)
- + ")");
- }
- if (isReplacing()) {
- buildTvInputList(packages);
- }
+ @Override
+ public void onPackagesUnavailable(String[] packages) {
+ // This callback is invoked when the media on which some packages exist become
+ // unavailable.
+ if (DEBUG) {
+ Slog.d(TAG, "onPackagesUnavailable(packages=" + Arrays.toString(packages)
+ + ")");
}
+ if (isReplacing()) {
+ buildTvInputList(packages);
+ }
+ }
- @Override
- public void onSomePackagesChanged() {
- // TODO: Use finer-grained methods(e.g. onPackageAdded, onPackageRemoved) to manage
- // the TV inputs.
- if (DEBUG) Slog.d(TAG, "onSomePackagesChanged()");
- if (isReplacing()) {
- if (DEBUG) Slog.d(TAG, "Skipped building TV input list due to replacing");
- // When the package is updated, buildTvInputListLocked is called in other
- // methods instead.
- return;
- }
- buildTvInputList(null);
+ @Override
+ public void onSomePackagesChanged() {
+ // TODO: Use finer-grained methods(e.g. onPackageAdded, onPackageRemoved) to manage
+ // the TV inputs.
+ if (DEBUG) Slog.d(TAG, "onSomePackagesChanged()");
+ if (isReplacing()) {
+ if (DEBUG) Slog.d(TAG, "Skipped building TV input list due to replacing");
+ // When the package is updated, buildTvInputListLocked is called in other
+ // methods instead.
+ return;
}
+ buildTvInputList(null);
+ }
- @Override
- public boolean onPackageChanged(String packageName, int uid, String[] components) {
- // The input list needs to be updated in any cases, regardless of whether
- // it happened to the whole package or a specific component. Returning true so that
- // the update can be handled in {@link #onSomePackagesChanged}.
- return true;
- }
- };
- monitor.register(mContext, null, UserHandle.ALL, true);
+ @Override
+ public boolean onPackageChanged(String packageName, int uid, String[] components) {
+ // The input list needs to be updated in any cases, regardless of whether
+ // it happened to the whole package or a specific component. Returning true so that
+ // the update can be handled in {@link #onSomePackagesChanged}.
+ return true;
+ }
+ }
+
+ private void registerBroadcastReceivers() {
+ mPackageMonitor.register(mContext, null, UserHandle.ALL, true);
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_USER_SWITCHED);
diff --git a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
index 6a7fc6dcf7cd..42013fab7a14 100644
--- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
+++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
@@ -105,6 +105,7 @@ public class TvInteractiveAppManagerService extends SystemService {
// A global lock.
private final Object mLock = new Object();
private final Context mContext;
+ private final MyPackageMonitor mPackageMonitor;
// ID of the current user.
@GuardedBy("mLock")
private int mCurrentUserId = UserHandle.USER_SYSTEM;
@@ -138,6 +139,7 @@ public class TvInteractiveAppManagerService extends SystemService {
super(context);
mContext = context;
mUserManager = (UserManager) getContext().getSystemService(Context.USER_SERVICE);
+ mPackageMonitor = new MyPackageMonitor(/* supportsPackageRestartQuery */ true);
}
@GuardedBy("mLock")
@@ -518,86 +520,91 @@ public class TvInteractiveAppManagerService extends SystemService {
}
}
- private void registerBroadcastReceivers() {
- PackageMonitor monitor = new PackageMonitor(/* supportsPackageRestartQuery */ true) {
- private void buildTvInteractiveAppServiceList(String[] packages) {
- int userId = getChangingUserId();
- synchronized (mLock) {
- if (mCurrentUserId == userId || mRunningProfiles.contains(userId)) {
- buildTvInteractiveAppServiceListLocked(userId, packages);
- buildAppLinkInfoLocked(userId);
- }
+ private class MyPackageMonitor extends PackageMonitor {
+ MyPackageMonitor(boolean supportsPackageRestartQuery) {
+ super(supportsPackageRestartQuery);
+ }
+
+ private void buildTvInteractiveAppServiceList(String[] packages) {
+ int userId = getChangingUserId();
+ synchronized (mLock) {
+ if (mCurrentUserId == userId || mRunningProfiles.contains(userId)) {
+ buildTvInteractiveAppServiceListLocked(userId, packages);
+ buildAppLinkInfoLocked(userId);
}
}
- private void buildTvAdServiceList(String[] packages) {
- int userId = getChangingUserId();
- synchronized (mLock) {
- if (mCurrentUserId == userId || mRunningProfiles.contains(userId)) {
- buildTvAdServiceListLocked(userId, packages);
- }
+ }
+ private void buildTvAdServiceList(String[] packages) {
+ int userId = getChangingUserId();
+ synchronized (mLock) {
+ if (mCurrentUserId == userId || mRunningProfiles.contains(userId)) {
+ buildTvAdServiceListLocked(userId, packages);
}
}
+ }
- @Override
- public void onPackageUpdateFinished(String packageName, int uid) {
- if (DEBUG) Slogf.d(TAG, "onPackageUpdateFinished(packageName=" + packageName + ")");
- // This callback is invoked when the TV interactive App service is reinstalled.
- // In this case, isReplacing() always returns true.
- buildTvInteractiveAppServiceList(new String[] { packageName });
- buildTvAdServiceList(new String[] { packageName });
- }
+ @Override
+ public void onPackageUpdateFinished(String packageName, int uid) {
+ if (DEBUG) Slogf.d(TAG, "onPackageUpdateFinished(packageName=" + packageName + ")");
+ // This callback is invoked when the TV interactive App service is reinstalled.
+ // In this case, isReplacing() always returns true.
+ buildTvInteractiveAppServiceList(new String[] { packageName });
+ buildTvAdServiceList(new String[] { packageName });
+ }
- @Override
- public void onPackagesAvailable(String[] packages) {
- if (DEBUG) {
- Slogf.d(TAG, "onPackagesAvailable(packages=" + Arrays.toString(packages) + ")");
- }
- // This callback is invoked when the media on which some packages exist become
- // available.
- if (isReplacing()) {
- buildTvInteractiveAppServiceList(packages);
- buildTvAdServiceList(packages);
- }
+ @Override
+ public void onPackagesAvailable(String[] packages) {
+ if (DEBUG) {
+ Slogf.d(TAG, "onPackagesAvailable(packages=" + Arrays.toString(packages) + ")");
}
+ // This callback is invoked when the media on which some packages exist become
+ // available.
+ if (isReplacing()) {
+ buildTvInteractiveAppServiceList(packages);
+ buildTvAdServiceList(packages);
+ }
+ }
- @Override
- public void onPackagesUnavailable(String[] packages) {
- // This callback is invoked when the media on which some packages exist become
- // unavailable.
- if (DEBUG) {
- Slogf.d(TAG, "onPackagesUnavailable(packages=" + Arrays.toString(packages)
- + ")");
- }
- if (isReplacing()) {
- buildTvInteractiveAppServiceList(packages);
- buildTvAdServiceList(packages);
- }
+ @Override
+ public void onPackagesUnavailable(String[] packages) {
+ // This callback is invoked when the media on which some packages exist become
+ // unavailable.
+ if (DEBUG) {
+ Slogf.d(TAG, "onPackagesUnavailable(packages=" + Arrays.toString(packages)
+ + ")");
}
+ if (isReplacing()) {
+ buildTvInteractiveAppServiceList(packages);
+ buildTvAdServiceList(packages);
+ }
+ }
- @Override
- public void onSomePackagesChanged() {
- if (DEBUG) Slogf.d(TAG, "onSomePackagesChanged()");
- if (isReplacing()) {
- if (DEBUG) {
- Slogf.d(TAG, "Skipped building TV interactive App list due to replacing");
- }
- // When the package is updated, buildTvInteractiveAppServiceListLocked is called
- // in other methods instead.
- return;
+ @Override
+ public void onSomePackagesChanged() {
+ if (DEBUG) Slogf.d(TAG, "onSomePackagesChanged()");
+ if (isReplacing()) {
+ if (DEBUG) {
+ Slogf.d(TAG, "Skipped building TV interactive App list due to replacing");
}
- buildTvInteractiveAppServiceList(null);
- buildTvAdServiceList(null);
+ // When the package is updated, buildTvInteractiveAppServiceListLocked is called
+ // in other methods instead.
+ return;
}
+ buildTvInteractiveAppServiceList(null);
+ buildTvAdServiceList(null);
+ }
- @Override
- public boolean onPackageChanged(String packageName, int uid, String[] components) {
- // The interactive App list needs to be updated in any cases, regardless of whether
- // it happened to the whole package or a specific component. Returning true so that
- // the update can be handled in {@link #onSomePackagesChanged}.
- return true;
- }
- };
- monitor.register(mContext, null, UserHandle.ALL, true);
+ @Override
+ public boolean onPackageChanged(String packageName, int uid, String[] components) {
+ // The interactive App list needs to be updated in any cases, regardless of whether
+ // it happened to the whole package or a specific component. Returning true so that
+ // the update can be handled in {@link #onSomePackagesChanged}.
+ return true;
+ }
+ }
+
+ private void registerBroadcastReceivers() {
+ mPackageMonitor.register(mContext, null, UserHandle.ALL, true);
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_USER_SWITCHED);
diff --git a/services/core/java/com/android/server/updates/CertPinInstallReceiver.java b/services/core/java/com/android/server/updates/CertPinInstallReceiver.java
index c8e7a8dea5c3..250e99b47b1a 100644
--- a/services/core/java/com/android/server/updates/CertPinInstallReceiver.java
+++ b/services/core/java/com/android/server/updates/CertPinInstallReceiver.java
@@ -19,10 +19,7 @@ package com.android.server.updates;
import android.content.Context;
import android.content.Intent;
-import java.io.File;
-
public class CertPinInstallReceiver extends ConfigUpdateInstallReceiver {
- private static final String KEYCHAIN_DIR = "/data/misc/keychain/";
public CertPinInstallReceiver() {
super("/data/misc/keychain/", "pins", "metadata/", "version");
@@ -30,22 +27,7 @@ public class CertPinInstallReceiver extends ConfigUpdateInstallReceiver {
@Override
public void onReceive(final Context context, final Intent intent) {
- if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
- if (com.android.server.flags.Flags.certpininstallerRemoval()) {
- File pins = new File(KEYCHAIN_DIR + "pins");
- if (pins.exists()) {
- pins.delete();
- }
- File version = new File(KEYCHAIN_DIR + "metadata/version");
- if (version.exists()) {
- version.delete();
- }
- File metadata = new File(KEYCHAIN_DIR + "metadata");
- if (metadata.exists()) {
- metadata.delete();
- }
- }
- } else if (!com.android.server.flags.Flags.certpininstallerRemoval()) {
+ if (!com.android.server.flags.Flags.certpininstallerRemoval()) {
super.onReceive(context, intent);
}
}
diff --git a/services/core/java/com/android/server/vr/EnabledComponentsObserver.java b/services/core/java/com/android/server/vr/EnabledComponentsObserver.java
index 7c2ce6467122..458cb023cc4e 100644
--- a/services/core/java/com/android/server/vr/EnabledComponentsObserver.java
+++ b/services/core/java/com/android/server/vr/EnabledComponentsObserver.java
@@ -58,6 +58,7 @@ public class EnabledComponentsObserver implements SettingChangeListener {
private final Object mLock;
private final Context mContext;
+ private final PackageMonitor mPackageMonitor;
private final String mSettingName;
private final String mServiceName;
private final String mServicePermission;
@@ -78,13 +79,39 @@ public class EnabledComponentsObserver implements SettingChangeListener {
private EnabledComponentsObserver(@NonNull Context context, @NonNull String settingName,
@NonNull String servicePermission, @NonNull String serviceName, @NonNull Object lock,
- @NonNull Collection<EnabledComponentChangeListener> listeners) {
+ @NonNull Collection<EnabledComponentChangeListener> listeners,
+ @NonNull Looper looper) {
mLock = lock;
mContext = context;
mSettingName = settingName;
mServiceName = serviceName;
mServicePermission = servicePermission;
mEnabledComponentListeners.addAll(listeners);
+ mPackageMonitor = new PackageMonitor(true) {
+ @Override
+ public void onSomePackagesChanged() {
+ onPackagesChanged();
+ }
+
+ @Override
+ public void onPackageDisappeared(String packageName, int reason) {
+ onPackagesChanged();
+ }
+
+ @Override
+ public void onPackageModified(String packageName) {
+ onPackagesChanged();
+ }
+
+ @Override
+ public boolean onHandleForceStop(Intent intent, String[] packages, int uid,
+ boolean doit) {
+ onPackagesChanged();
+ return super.onHandleForceStop(intent, packages, uid, doit);
+ }
+ };
+
+ mPackageMonitor.register(context, looper, UserHandle.ALL, true);;
}
/**
@@ -108,38 +135,7 @@ public class EnabledComponentsObserver implements SettingChangeListener {
SettingsObserver s = SettingsObserver.build(context, handler, settingName);
final EnabledComponentsObserver o = new EnabledComponentsObserver(context, settingName,
- servicePermission, serviceName, lock, listeners);
-
- PackageMonitor packageMonitor = new PackageMonitor(true) {
- @Override
- public void onSomePackagesChanged() {
- o.onPackagesChanged();
-
- }
-
- @Override
- public void onPackageDisappeared(String packageName, int reason) {
- o.onPackagesChanged();
-
- }
-
- @Override
- public void onPackageModified(String packageName) {
- o.onPackagesChanged();
-
- }
-
- @Override
- public boolean onHandleForceStop(Intent intent, String[] packages, int uid,
- boolean doit) {
- o.onPackagesChanged();
-
- return super.onHandleForceStop(intent, packages, uid, doit);
- }
- };
-
- packageMonitor.register(context, looper, UserHandle.ALL, true);
-
+ servicePermission, serviceName, lock, listeners, looper);
s.addListener(o);
return o;
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
index 8e8455ad5288..6e640d890fb8 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
@@ -17,7 +17,6 @@
package com.android.server.wallpaper;
import static android.app.WallpaperManager.ORIENTATION_LANDSCAPE;
-import static android.app.WallpaperManager.ORIENTATION_SQUARE_LANDSCAPE;
import static android.app.WallpaperManager.ORIENTATION_UNKNOWN;
import static android.app.WallpaperManager.getOrientation;
import static android.app.WallpaperManager.getRotatedOrientation;
@@ -85,20 +84,11 @@ public class WallpaperCropper {
private final WallpaperDisplayHelper mWallpaperDisplayHelper;
- /**
- * Helpers exposed to the window manager part (WallpaperController)
- */
- public interface WallpaperCropUtils {
-
- /**
- * Equivalent to {@link WallpaperCropper#getCrop(Point, Point, SparseArray, boolean)}
- */
- Rect getCrop(Point displaySize, Point bitmapSize,
- SparseArray<Rect> suggestedCrops, boolean rtl);
- }
+ private final WallpaperDefaultDisplayInfo mDefaultDisplayInfo;
WallpaperCropper(WallpaperDisplayHelper wallpaperDisplayHelper) {
mWallpaperDisplayHelper = wallpaperDisplayHelper;
+ mDefaultDisplayInfo = mWallpaperDisplayHelper.getDefaultDisplayInfo();
}
/**
@@ -116,16 +106,16 @@ public class WallpaperCropper {
* {@link #getAdjustedCrop}.
* </ul>
*
- * @param displaySize The dimensions of the surface where we want to render the wallpaper
- * @param bitmapSize The dimensions of the wallpaper bitmap
- * @param rtl Whether the device is right-to-left
- * @param suggestedCrops An optional list of user-defined crops for some orientations.
- * If there is a suggested crop for
+ * @param displaySize The dimensions of the surface where we want to render the wallpaper
+ * @param defaultDisplayInfo The default display info
+ * @param bitmapSize The dimensions of the wallpaper bitmap
+ * @param rtl Whether the device is right-to-left
+ * @param suggestedCrops An optional list of user-defined crops for some orientations.
*
* @return A Rect indicating how to crop the bitmap for the current display.
*/
- public Rect getCrop(Point displaySize, Point bitmapSize,
- SparseArray<Rect> suggestedCrops, boolean rtl) {
+ public static Rect getCrop(Point displaySize, WallpaperDefaultDisplayInfo defaultDisplayInfo,
+ Point bitmapSize, SparseArray<Rect> suggestedCrops, boolean rtl) {
int orientation = getOrientation(displaySize);
@@ -135,23 +125,24 @@ public class WallpaperCropper {
// The first exception is if the device is a foldable and we're on the folded screen.
// In that case, show the center of what's on the unfolded screen.
- int unfoldedOrientation = mWallpaperDisplayHelper.getUnfoldedOrientation(orientation);
+ int unfoldedOrientation = defaultDisplayInfo.getUnfoldedOrientation(orientation);
if (unfoldedOrientation != ORIENTATION_UNKNOWN) {
// Let the system know that we're showing the full image on the unfolded screen
SparseArray<Rect> newSuggestedCrops = new SparseArray<>();
newSuggestedCrops.put(unfoldedOrientation, crop);
// This will fall into "Case 4" of this function and center the folded screen
- return getCrop(displaySize, bitmapSize, newSuggestedCrops, rtl);
+ return getCrop(displaySize, defaultDisplayInfo, bitmapSize, newSuggestedCrops,
+ rtl);
}
// The second exception is if we're on tablet and we're on portrait mode.
// In that case, center the wallpaper relatively to landscape and put some parallax.
- boolean isTablet = mWallpaperDisplayHelper.isLargeScreen()
- && !mWallpaperDisplayHelper.isFoldable();
+ boolean isTablet = defaultDisplayInfo.isLargeScreen && !defaultDisplayInfo.isFoldable;
if (isTablet && displaySize.x < displaySize.y) {
Point rotatedDisplaySize = new Point(displaySize.y, displaySize.x);
// compute the crop on landscape (without parallax)
- Rect landscapeCrop = getCrop(rotatedDisplaySize, bitmapSize, suggestedCrops, rtl);
+ Rect landscapeCrop = getCrop(rotatedDisplaySize, defaultDisplayInfo, bitmapSize,
+ suggestedCrops, rtl);
landscapeCrop = noParallax(landscapeCrop, rotatedDisplaySize, bitmapSize, rtl);
// compute the crop on portrait at the center of the landscape crop
crop = getAdjustedCrop(landscapeCrop, bitmapSize, displaySize, false, rtl, ADD);
@@ -173,7 +164,8 @@ public class WallpaperCropper {
if (testCrop == null || testCrop.left < 0 || testCrop.top < 0
|| testCrop.right > bitmapSize.x || testCrop.bottom > bitmapSize.y) {
Slog.w(TAG, "invalid crop: " + testCrop + " for bitmap size: " + bitmapSize);
- return getCrop(displaySize, bitmapSize, new SparseArray<>(), rtl);
+ return getCrop(displaySize, defaultDisplayInfo, bitmapSize, new SparseArray<>(),
+ rtl);
}
}
@@ -185,10 +177,9 @@ public class WallpaperCropper {
// Case 3: if we have the 90° rotated orientation in the suggested crops, reuse it and
// trying to preserve the zoom level and the center of the image
- SparseArray<Point> defaultDisplaySizes = mWallpaperDisplayHelper.getDefaultDisplaySizes();
int rotatedOrientation = getRotatedOrientation(orientation);
suggestedCrop = suggestedCrops.get(rotatedOrientation);
- Point suggestedDisplaySize = defaultDisplaySizes.get(rotatedOrientation);
+ Point suggestedDisplaySize = defaultDisplayInfo.defaultDisplaySizes.get(rotatedOrientation);
if (suggestedCrop != null) {
// only keep the visible part (without parallax)
Rect adjustedCrop = noParallax(suggestedCrop, suggestedDisplaySize, bitmapSize, rtl);
@@ -197,9 +188,9 @@ public class WallpaperCropper {
// Case 4: if the device is a foldable, if we're looking for a folded orientation and have
// the suggested crop of the relative unfolded orientation, reuse it by removing content.
- int unfoldedOrientation = mWallpaperDisplayHelper.getUnfoldedOrientation(orientation);
+ int unfoldedOrientation = defaultDisplayInfo.getUnfoldedOrientation(orientation);
suggestedCrop = suggestedCrops.get(unfoldedOrientation);
- suggestedDisplaySize = defaultDisplaySizes.get(unfoldedOrientation);
+ suggestedDisplaySize = defaultDisplayInfo.defaultDisplaySizes.get(unfoldedOrientation);
if (suggestedCrop != null) {
// compute the visible part (without parallax) of the unfolded screen
Rect adjustedCrop = noParallax(suggestedCrop, suggestedDisplaySize, bitmapSize, rtl);
@@ -207,8 +198,11 @@ public class WallpaperCropper {
Rect res = getAdjustedCrop(adjustedCrop, bitmapSize, displaySize, false, rtl, REMOVE);
// if we removed some width, add it back to add a parallax effect
if (res.width() < adjustedCrop.width()) {
- if (rtl) res.left = Math.min(res.left, adjustedCrop.left);
- else res.right = Math.max(res.right, adjustedCrop.right);
+ if (rtl) {
+ res.left = Math.min(res.left, adjustedCrop.left);
+ } else {
+ res.right = Math.max(res.right, adjustedCrop.right);
+ }
// use getAdjustedCrop(parallax=true) to make sure we don't exceed MAX_PARALLAX
res = getAdjustedCrop(res, bitmapSize, displaySize, true, rtl, ADD);
}
@@ -218,9 +212,9 @@ public class WallpaperCropper {
// Case 5: if the device is a foldable, if we're looking for an unfolded orientation and
// have the suggested crop of the relative folded orientation, reuse it by adding content.
- int foldedOrientation = mWallpaperDisplayHelper.getFoldedOrientation(orientation);
+ int foldedOrientation = defaultDisplayInfo.getFoldedOrientation(orientation);
suggestedCrop = suggestedCrops.get(foldedOrientation);
- suggestedDisplaySize = defaultDisplaySizes.get(foldedOrientation);
+ suggestedDisplaySize = defaultDisplayInfo.defaultDisplaySizes.get(foldedOrientation);
if (suggestedCrop != null) {
// only keep the visible part (without parallax)
Rect adjustedCrop = noParallax(suggestedCrop, suggestedDisplaySize, bitmapSize, rtl);
@@ -229,17 +223,19 @@ public class WallpaperCropper {
// Case 6: for a foldable device, try to combine case 3 + case 4 or 5:
// rotate, then fold or unfold
- Point rotatedDisplaySize = defaultDisplaySizes.get(rotatedOrientation);
+ Point rotatedDisplaySize = defaultDisplayInfo.defaultDisplaySizes.get(rotatedOrientation);
if (rotatedDisplaySize != null) {
- int rotatedFolded = mWallpaperDisplayHelper.getFoldedOrientation(rotatedOrientation);
- int rotateUnfolded = mWallpaperDisplayHelper.getUnfoldedOrientation(rotatedOrientation);
+ int rotatedFolded = defaultDisplayInfo.getFoldedOrientation(rotatedOrientation);
+ int rotateUnfolded = defaultDisplayInfo.getUnfoldedOrientation(rotatedOrientation);
for (int suggestedOrientation : new int[]{rotatedFolded, rotateUnfolded}) {
suggestedCrop = suggestedCrops.get(suggestedOrientation);
if (suggestedCrop != null) {
- Rect rotatedCrop = getCrop(rotatedDisplaySize, bitmapSize, suggestedCrops, rtl);
+ Rect rotatedCrop = getCrop(rotatedDisplaySize, defaultDisplayInfo, bitmapSize,
+ suggestedCrops, rtl);
SparseArray<Rect> rotatedCropMap = new SparseArray<>();
rotatedCropMap.put(rotatedOrientation, rotatedCrop);
- return getCrop(displaySize, bitmapSize, rotatedCropMap, rtl);
+ return getCrop(displaySize, defaultDisplayInfo, bitmapSize, rotatedCropMap,
+ rtl);
}
}
}
@@ -248,8 +244,8 @@ public class WallpaperCropper {
Slog.w(TAG, "Could not find a proper default crop for display: " + displaySize
+ ", bitmap size: " + bitmapSize + ", suggested crops: " + suggestedCrops
+ ", orientation: " + orientation + ", rtl: " + rtl
- + ", defaultDisplaySizes: " + defaultDisplaySizes);
- return getCrop(displaySize, bitmapSize, new SparseArray<>(), rtl);
+ + ", defaultDisplaySizes: " + defaultDisplayInfo.defaultDisplaySizes);
+ return getCrop(displaySize, defaultDisplayInfo, bitmapSize, new SparseArray<>(), rtl);
}
/**
@@ -445,7 +441,7 @@ public class WallpaperCropper {
Rect suggestedCrop = suggestedCrops.get(orientation);
if (suggestedCrop != null) {
adjustedSuggestedCrops.put(orientation,
- getCrop(displaySize, bitmapSize, suggestedCrops, rtl));
+ getCrop(displaySize, mDefaultDisplayInfo, bitmapSize, suggestedCrops, rtl));
}
}
@@ -455,7 +451,8 @@ public class WallpaperCropper {
int orientation = defaultDisplaySizes.keyAt(i);
if (result.contains(orientation)) continue;
Point displaySize = defaultDisplaySizes.valueAt(i);
- Rect newCrop = getCrop(displaySize, bitmapSize, adjustedSuggestedCrops, rtl);
+ Rect newCrop = getCrop(displaySize, mDefaultDisplayInfo, bitmapSize,
+ adjustedSuggestedCrops, rtl);
result.put(orientation, newCrop);
}
return result;
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
index ba0262a8bd19..69f0ef7c430e 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
@@ -542,9 +542,11 @@ public class WallpaperDataParser {
// to support back compatibility in B&R, save the crops for one orientation in the
// legacy "cropLeft", "cropTop", "cropRight", "cropBottom" entries
int orientationToPutInLegacyCrop = wallpaper.mOrientationWhenSet;
- if (mWallpaperDisplayHelper.isFoldable()) {
- int unfoldedOrientation = mWallpaperDisplayHelper
- .getUnfoldedOrientation(orientationToPutInLegacyCrop);
+ WallpaperDefaultDisplayInfo defaultDisplayInfo =
+ mWallpaperDisplayHelper.getDefaultDisplayInfo();
+ if (defaultDisplayInfo.isFoldable) {
+ int unfoldedOrientation = defaultDisplayInfo.getUnfoldedOrientation(
+ orientationToPutInLegacyCrop);
if (unfoldedOrientation != ORIENTATION_UNKNOWN) {
orientationToPutInLegacyCrop = unfoldedOrientation;
}
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDefaultDisplayInfo.java b/services/core/java/com/android/server/wallpaper/WallpaperDefaultDisplayInfo.java
new file mode 100644
index 000000000000..dabe91968338
--- /dev/null
+++ b/services/core/java/com/android/server/wallpaper/WallpaperDefaultDisplayInfo.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wallpaper;
+
+import static android.app.WallpaperManager.ORIENTATION_UNKNOWN;
+import static android.app.WallpaperManager.getRotatedOrientation;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP;
+
+import android.app.WallpaperManager;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.util.SparseArray;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+
+/** A data class for the default display attributes used in wallpaper related operations. */
+public final class WallpaperDefaultDisplayInfo {
+ /**
+ * A data class representing the screen orientations for a foldable device in the folded and
+ * unfolded states.
+ */
+ @VisibleForTesting
+ static final class FoldableOrientations {
+ @WallpaperManager.ScreenOrientation
+ public final int foldedOrientation;
+ @WallpaperManager.ScreenOrientation
+ public final int unfoldedOrientation;
+
+ FoldableOrientations(int foldedOrientation, int unfoldedOrientation) {
+ this.foldedOrientation = foldedOrientation;
+ this.unfoldedOrientation = unfoldedOrientation;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) return true;
+ if (!(other instanceof FoldableOrientations that)) return false;
+ return foldedOrientation == that.foldedOrientation
+ && unfoldedOrientation == that.unfoldedOrientation;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(foldedOrientation, unfoldedOrientation);
+ }
+ }
+
+ public final SparseArray<Point> defaultDisplaySizes;
+ public final boolean isLargeScreen;
+ public final boolean isFoldable;
+ @VisibleForTesting
+ final List<FoldableOrientations> foldableOrientations;
+
+ public WallpaperDefaultDisplayInfo() {
+ this.defaultDisplaySizes = new SparseArray<>();
+ this.isLargeScreen = false;
+ this.isFoldable = false;
+ this.foldableOrientations = Collections.emptyList();
+ }
+
+ public WallpaperDefaultDisplayInfo(WindowManager windowManager, Resources resources) {
+ Set<WindowMetrics> metrics = windowManager.getPossibleMaximumWindowMetrics(DEFAULT_DISPLAY);
+ boolean isFoldable = resources.getIntArray(R.array.config_foldedDeviceStates).length > 0;
+ if (isFoldable) {
+ this.foldableOrientations = getFoldableOrientations(metrics);
+ } else {
+ this.foldableOrientations = Collections.emptyList();
+ }
+ this.defaultDisplaySizes = getDisplaySizes(metrics);
+ this.isLargeScreen = isLargeScreen(metrics);
+ this.isFoldable = isFoldable;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) return true;
+ if (!(other instanceof WallpaperDefaultDisplayInfo that)) return false;
+ return isLargeScreen == that.isLargeScreen && isFoldable == that.isFoldable
+ && defaultDisplaySizes.contentEquals(that.defaultDisplaySizes)
+ && Objects.equals(foldableOrientations, that.foldableOrientations);
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * Objects.hash(isLargeScreen, isFoldable, foldableOrientations)
+ + defaultDisplaySizes.contentHashCode();
+ }
+
+ /**
+ * Returns the folded orientation corresponds to the {@code unfoldedOrientation} found in
+ * {@link #foldableOrientations}. If not found, returns
+ * {@link WallpaperManager.ORIENTATION_UNKNOWN}.
+ */
+ public int getFoldedOrientation(int unfoldedOrientation) {
+ for (FoldableOrientations orientations : foldableOrientations) {
+ if (orientations.unfoldedOrientation == unfoldedOrientation) {
+ return orientations.foldedOrientation;
+ }
+ }
+ return ORIENTATION_UNKNOWN;
+ }
+
+ /**
+ * Returns the unfolded orientation corresponds to the {@code foldedOrientation} found in
+ * {@link #foldableOrientations}. If not found, returns
+ * {@link WallpaperManager.ORIENTATION_UNKNOWN}.
+ */
+ public int getUnfoldedOrientation(int foldedOrientation) {
+ for (FoldableOrientations orientations : foldableOrientations) {
+ if (orientations.foldedOrientation == foldedOrientation) {
+ return orientations.unfoldedOrientation;
+ }
+ }
+ return ORIENTATION_UNKNOWN;
+ }
+
+ private static SparseArray<Point> getDisplaySizes(Set<WindowMetrics> displayMetrics) {
+ SparseArray<Point> displaySizes = new SparseArray<>();
+ for (WindowMetrics metric : displayMetrics) {
+ Rect bounds = metric.getBounds();
+ Point displaySize = new Point(bounds.width(), bounds.height());
+ Point reversedDisplaySize = new Point(displaySize.y, displaySize.x);
+ for (Point point : List.of(displaySize, reversedDisplaySize)) {
+ int orientation = WallpaperManager.getOrientation(point);
+ // don't add an entry if there is already a larger display of the same orientation
+ Point display = displaySizes.get(orientation);
+ if (display == null || display.x * display.y < point.x * point.y) {
+ displaySizes.put(orientation, point);
+ }
+ }
+ }
+ return displaySizes;
+ }
+
+ private static boolean isLargeScreen(Set<WindowMetrics> displayMetrics) {
+ float smallestWidth = Float.MAX_VALUE;
+ for (WindowMetrics metric : displayMetrics) {
+ Rect bounds = metric.getBounds();
+ smallestWidth = Math.min(smallestWidth, bounds.width() / metric.getDensity());
+ }
+ return smallestWidth >= LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP;
+ }
+
+ /**
+ * Determines all potential foldable orientations, populating {@code
+ * outFoldableOrientationPairs} with pairs of (folded orientation, unfolded orientation). If
+ * {@code defaultDisplayMetrics} isn't for foldable, {@code outFoldableOrientationPairs} will
+ * not be populated.
+ */
+ private static List<FoldableOrientations> getFoldableOrientations(
+ Set<WindowMetrics> defaultDisplayMetrics) {
+ if (defaultDisplayMetrics.size() != 2) {
+ return Collections.emptyList();
+ }
+ List<FoldableOrientations> foldableOrientations = new ArrayList<>();
+ float surface = 0;
+ int firstOrientation = -1;
+ for (WindowMetrics metric : defaultDisplayMetrics) {
+ Rect bounds = metric.getBounds();
+ Point displaySize = new Point(bounds.width(), bounds.height());
+
+ int orientation = WallpaperManager.getOrientation(displaySize);
+ float newSurface = displaySize.x * displaySize.y
+ / (metric.getDensity() * metric.getDensity());
+ if (surface <= 0) {
+ surface = newSurface;
+ firstOrientation = orientation;
+ } else {
+ FoldableOrientations orientations = (newSurface > surface)
+ ? new FoldableOrientations(firstOrientation, orientation)
+ : new FoldableOrientations(orientation, firstOrientation);
+ FoldableOrientations rotatedOrientations = new FoldableOrientations(
+ getRotatedOrientation(orientations.foldedOrientation),
+ getRotatedOrientation(orientations.unfoldedOrientation));
+ foldableOrientations.add(orientations);
+ foldableOrientations.add(rotatedOrientations);
+ }
+ }
+ return Collections.unmodifiableList(foldableOrientations);
+ }
+}
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java b/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java
index 3636f5aa8f27..bff5fc9c49f3 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java
@@ -16,31 +16,25 @@
package com.android.server.wallpaper;
-import static android.app.WallpaperManager.ORIENTATION_UNKNOWN;
-import static android.app.WallpaperManager.getRotatedOrientation;
import static android.view.Display.DEFAULT_DISPLAY;
import static com.android.window.flags.Flags.multiCrop;
import android.app.WallpaperManager;
+import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.os.Binder;
import android.os.Debug;
-import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
import android.view.Display;
import android.view.DisplayInfo;
import android.view.WindowManager;
-import android.view.WindowMetrics;
import com.android.server.wm.WindowManagerInternal;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
import java.util.function.Consumer;
/**
@@ -59,65 +53,25 @@ class WallpaperDisplayHelper {
}
private static final String TAG = WallpaperDisplayHelper.class.getSimpleName();
- private static final float LARGE_SCREEN_MIN_DP = 600f;
private final SparseArray<DisplayData> mDisplayDatas = new SparseArray<>();
private final DisplayManager mDisplayManager;
private final WindowManagerInternal mWindowManagerInternal;
- private final SparseArray<Point> mDefaultDisplaySizes = new SparseArray<>();
- // related orientations pairs for foldable (folded orientation, unfolded orientation)
- private final List<Pair<Integer, Integer>> mFoldableOrientationPairs = new ArrayList<>();
-
- private final boolean mIsFoldable;
- private boolean mIsLargeScreen = false;
+ private final WallpaperDefaultDisplayInfo mDefaultDisplayInfo;
WallpaperDisplayHelper(
DisplayManager displayManager,
WindowManager windowManager,
WindowManagerInternal windowManagerInternal,
- boolean isFoldable) {
+ Resources resources) {
mDisplayManager = displayManager;
mWindowManagerInternal = windowManagerInternal;
- mIsFoldable = isFoldable;
- if (!multiCrop()) return;
- Set<WindowMetrics> metrics = windowManager.getPossibleMaximumWindowMetrics(DEFAULT_DISPLAY);
- boolean populateOrientationPairs = isFoldable && metrics.size() == 2;
- float surface = 0;
- int firstOrientation = -1;
- for (WindowMetrics metric: metrics) {
- Rect bounds = metric.getBounds();
- Point displaySize = new Point(bounds.width(), bounds.height());
- Point reversedDisplaySize = new Point(displaySize.y, displaySize.x);
- for (Point point : List.of(displaySize, reversedDisplaySize)) {
- int orientation = WallpaperManager.getOrientation(point);
- // don't add an entry if there is already a larger display of the same orientation
- Point display = mDefaultDisplaySizes.get(orientation);
- if (display == null || display.x * display.y < point.x * point.y) {
- mDefaultDisplaySizes.put(orientation, point);
- }
- }
-
- mIsLargeScreen |= (displaySize.x / metric.getDensity() >= LARGE_SCREEN_MIN_DP);
-
- if (populateOrientationPairs) {
- int orientation = WallpaperManager.getOrientation(displaySize);
- float newSurface = displaySize.x * displaySize.y
- / (metric.getDensity() * metric.getDensity());
- if (surface <= 0) {
- surface = newSurface;
- firstOrientation = orientation;
- } else {
- Pair<Integer, Integer> pair = (newSurface > surface)
- ? new Pair<>(firstOrientation, orientation)
- : new Pair<>(orientation, firstOrientation);
- Pair<Integer, Integer> rotatedPair = new Pair<>(
- getRotatedOrientation(pair.first), getRotatedOrientation(pair.second));
- mFoldableOrientationPairs.add(pair);
- mFoldableOrientationPairs.add(rotatedPair);
- }
- }
+ if (!multiCrop()) {
+ mDefaultDisplayInfo = new WallpaperDefaultDisplayInfo();
+ return;
}
+ mDefaultDisplayInfo = new WallpaperDefaultDisplayInfo(windowManager, resources);
}
DisplayData getDisplayDataOrCreate(int displayId) {
@@ -203,51 +157,21 @@ class WallpaperDisplayHelper {
}
SparseArray<Point> getDefaultDisplaySizes() {
- return mDefaultDisplaySizes;
+ return mDefaultDisplayInfo.defaultDisplaySizes;
}
/** Return the number of pixel of the largest dimension of the default display */
int getDefaultDisplayLargestDimension() {
+ SparseArray<Point> defaultDisplaySizes = mDefaultDisplayInfo.defaultDisplaySizes;
int result = -1;
- for (int i = 0; i < mDefaultDisplaySizes.size(); i++) {
- Point size = mDefaultDisplaySizes.valueAt(i);
+ for (int i = 0; i < defaultDisplaySizes.size(); i++) {
+ Point size = defaultDisplaySizes.valueAt(i);
result = Math.max(result, Math.max(size.x, size.y));
}
return result;
}
- boolean isFoldable() {
- return mIsFoldable;
- }
-
- /**
- * Return true if any of the screens of the default display is considered large (DP >= 600)
- */
- boolean isLargeScreen() {
- return mIsLargeScreen;
- }
-
- /**
- * If a given orientation corresponds to an unfolded orientation on foldable, return the
- * corresponding folded orientation. Otherwise, return UNKNOWN. Always return UNKNOWN if the
- * device is not a foldable.
- */
- int getFoldedOrientation(int orientation) {
- for (Pair<Integer, Integer> pair : mFoldableOrientationPairs) {
- if (pair.second.equals(orientation)) return pair.first;
- }
- return ORIENTATION_UNKNOWN;
- }
-
- /**
- * If a given orientation corresponds to a folded orientation on foldable, return the
- * corresponding unfolded orientation. Otherwise, return UNKNOWN. Always return UNKNOWN if the
- * device is not a foldable.
- */
- int getUnfoldedOrientation(int orientation) {
- for (Pair<Integer, Integer> pair : mFoldableOrientationPairs) {
- if (pair.first.equals(orientation)) return pair.second;
- }
- return ORIENTATION_UNKNOWN;
+ public WallpaperDefaultDisplayInfo getDefaultDisplayInfo() {
+ return mDefaultDisplayInfo;
}
}
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index bac732637d8d..e7da33d50b27 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -1666,12 +1666,9 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
displayManager.registerDisplayListener(mDisplayListener, null /* handler */);
WindowManager windowManager = mContext.getSystemService(WindowManager.class);
- boolean isFoldable = mContext.getResources()
- .getIntArray(R.array.config_foldedDeviceStates).length > 0;
mWallpaperDisplayHelper = new WallpaperDisplayHelper(
- displayManager, windowManager, mWindowManagerInternal, isFoldable);
+ displayManager, windowManager, mWindowManagerInternal, mContext.getResources());
mWallpaperCropper = new WallpaperCropper(mWallpaperDisplayHelper);
- mWindowManagerInternal.setWallpaperCropUtils(mWallpaperCropper::getCrop);
mActivityManager = mContext.getSystemService(ActivityManager.class);
if (mContext.getResources().getBoolean(
@@ -2510,9 +2507,11 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
List<Rect> result = new ArrayList<>();
boolean rtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault())
== View.LAYOUT_DIRECTION_RTL;
+ WallpaperDefaultDisplayInfo defaultDisplayInfo =
+ mWallpaperDisplayHelper.getDefaultDisplayInfo();
for (Point displaySize : displaySizes) {
- result.add(mWallpaperCropper.getCrop(
- displaySize, croppedBitmapSize, adjustedRelativeSuggestedCrops, rtl));
+ result.add(WallpaperCropper.getCrop(displaySize, defaultDisplayInfo,
+ croppedBitmapSize, adjustedRelativeSuggestedCrops, rtl));
}
if (originalBitmap) result = WallpaperCropper.getOriginalCropHints(wallpaper, result);
return result;
@@ -2548,8 +2547,11 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
List<Rect> result = new ArrayList<>();
boolean rtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault())
== View.LAYOUT_DIRECTION_RTL;
+ WallpaperDefaultDisplayInfo defaultDisplayInfo =
+ mWallpaperDisplayHelper.getDefaultDisplayInfo();
for (Point displaySize : displaySizes) {
- result.add(mWallpaperCropper.getCrop(displaySize, bitmapSize, defaultCrops, rtl));
+ result.add(WallpaperCropper.getCrop(displaySize, defaultDisplayInfo, bitmapSize,
+ defaultCrops, rtl));
}
return result;
}
diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
index a94183849bc5..e2b47b92f232 100644
--- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
+++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
@@ -112,7 +112,6 @@ import com.android.server.FgThread;
import com.android.server.LocalServices;
import com.android.server.apphibernation.AppHibernationManagerInternal;
import com.android.server.apphibernation.AppHibernationService;
-import com.android.window.flags.Flags;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
@@ -807,14 +806,8 @@ class ActivityMetricsLogger {
}
final Task otherTask = otherInfo.mLastLaunchedActivity.getTask();
// The adjacent task is the split root in which activities are started
- final boolean isDescendantOfAdjacent;
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- isDescendantOfAdjacent = launchedSplitRootTask.forOtherAdjacentTasks(
- otherTask::isDescendantOf);
- } else {
- isDescendantOfAdjacent = otherTask.isDescendantOf(
- launchedSplitRootTask.getAdjacentTask());
- }
+ final boolean isDescendantOfAdjacent = launchedSplitRootTask.forOtherAdjacentTasks(
+ otherTask::isDescendantOf);
if (isDescendantOfAdjacent) {
if (DEBUG_METRICS) {
Slog.i(TAG, "Found adjacent tasks t1=" + launchedActivityTask.mTaskId
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 3cd4db7d8dfc..e91d88901751 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -1716,6 +1716,7 @@ final class ActivityRecord extends WindowToken {
}
mAppCompatController.getLetterboxPolicy().onMovedToDisplay(mDisplayContent.getDisplayId());
+ mAppCompatController.getDisplayCompatModePolicy().onMovedToDisplay();
}
void layoutLetterboxIfNeeded(WindowState winHint) {
@@ -3801,19 +3802,10 @@ final class ActivityRecord extends WindowToken {
final TaskFragment taskFragment = getTaskFragment();
if (next != null && taskFragment != null && taskFragment.isEmbedded()) {
final TaskFragment organized = taskFragment.getOrganizedTaskFragment();
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- delayRemoval = organized != null
- && organized.topRunningActivity() == null
- && organized.isDelayLastActivityRemoval()
- && organized.forOtherAdjacentTaskFragments(next::isDescendantOf);
- } else {
- final TaskFragment adjacent =
- organized != null ? organized.getAdjacentTaskFragment() : null;
- if (adjacent != null && next.isDescendantOf(adjacent)
- && organized.topRunningActivity() == null) {
- delayRemoval = organized.isDelayLastActivityRemoval();
- }
- }
+ delayRemoval = organized != null
+ && organized.topRunningActivity() == null
+ && organized.isDelayLastActivityRemoval()
+ && organized.forOtherAdjacentTaskFragments(next::isDescendantOf);
}
// isNextNotYetVisible is to check if the next activity is invisible, or it has been
@@ -4787,11 +4779,6 @@ final class ActivityRecord extends WindowToken {
}
// Make sure the embedded adjacent can also be shown.
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- final ActivityRecord adjacentActivity = taskFragment.getAdjacentTaskFragment()
- .getTopNonFinishingActivity();
- return canShowWhenLocked(adjacentActivity);
- }
final boolean hasAdjacentNotAllowToShow = taskFragment.forOtherAdjacentTaskFragments(
adjacentTF -> !canShowWhenLocked(adjacentTF.getTopNonFinishingActivity()));
return !hasAdjacentNotAllowToShow;
@@ -8980,6 +8967,7 @@ final class ActivityRecord extends WindowToken {
// Reset the existing override configuration so it can be updated according to the latest
// configuration.
mAppCompatController.getSizeCompatModePolicy().clearSizeCompatMode();
+ mAppCompatController.getDisplayCompatModePolicy().onProcessRestarted();
if (!attachedToProcess()) {
return;
diff --git a/services/core/java/com/android/server/wm/ActivitySnapshotController.java b/services/core/java/com/android/server/wm/ActivitySnapshotController.java
index 21628341ea62..0f1939bfbb49 100644
--- a/services/core/java/com/android/server/wm/ActivitySnapshotController.java
+++ b/services/core/java/com/android/server/wm/ActivitySnapshotController.java
@@ -34,7 +34,6 @@ import android.window.TaskSnapshot;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.wm.BaseAppSnapshotPersister.PersistInfoProvider;
-import com.android.window.flags.Flags;
import java.io.File;
import java.io.PrintWriter;
@@ -107,8 +106,7 @@ class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord
&& !ActivityManager.isLowRamDeviceStatic(); // Don't support Android Go
setSnapshotEnabled(snapshotEnabled);
mSnapshotPersistQueue = persistQueue;
- mPersistInfoProvider = createPersistInfoProvider(service,
- Environment::getDataSystemCeDirectory);
+ mPersistInfoProvider = createPersistInfoProvider(service);
mPersister = new TaskSnapshotPersister(
persistQueue,
mPersistInfoProvider,
@@ -117,6 +115,11 @@ class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord
initialize(new ActivitySnapshotCache());
}
+ @VisibleForTesting
+ PersistInfoProvider createPersistInfoProvider(WindowManagerService service) {
+ return createPersistInfoProvider(service, Environment::getDataSystemCeDirectory);
+ }
+
@Override
protected float initSnapshotScale() {
final float config = mService.mContext.getResources().getFloat(
@@ -528,26 +531,6 @@ class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord
final int currentIndex = currTF.asTask() != null
? currentTask.mChildren.indexOf(currentActivity)
: currentTask.mChildren.indexOf(currTF);
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- final int prevAdjacentIndex = currentTask.mChildren.indexOf(
- prevTF.getAdjacentTaskFragment());
- if (prevAdjacentIndex > currentIndex) {
- // PrevAdjacentTF already above currentActivity
- return;
- }
- // Add both the one below, and its adjacent.
- if (!inTransition || isInParticipant(initPrev, mTmpTransitionParticipants)) {
- result.add(initPrev);
- }
- final ActivityRecord prevAdjacentActivity = prevTF.getAdjacentTaskFragment()
- .getTopMostActivity();
- if (prevAdjacentActivity != null && (!inTransition
- || isInParticipant(prevAdjacentActivity, mTmpTransitionParticipants))) {
- result.add(prevAdjacentActivity);
- }
- return;
- }
-
final boolean hasAdjacentAboveCurrent = prevTF.forOtherAdjacentTaskFragments(
prevAdjacentTF -> {
final int prevAdjacentIndex = currentTask.mChildren.indexOf(prevAdjacentTF);
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index a4c061e9d010..a84a008f66eb 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -1111,8 +1111,11 @@ class ActivityStarter {
if (sourceRecord != null) {
if (requestCode >= 0 && !sourceRecord.finishing) {
resultRecord = sourceRecord;
+ request.logMessage.append(" (rr=");
+ } else {
+ request.logMessage.append(" (sr=");
}
- request.logMessage.append(" (sr=" + System.identityHashCode(sourceRecord) + ")");
+ request.logMessage.append(System.identityHashCode(sourceRecord) + ")");
}
}
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index a7f2153993bb..b0563128870a 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -164,7 +164,6 @@ import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
import com.android.server.pm.SaferIntentUtils;
import com.android.server.utils.Slogf;
import com.android.server.wm.ActivityMetricsLogger.LaunchingState;
-import com.android.window.flags.Flags;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -2991,17 +2990,9 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks {
if (child.asTaskFragment() != null
&& child.asTaskFragment().hasAdjacentTaskFragment()) {
- final boolean isAnyTranslucent;
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- final TaskFragment.AdjacentSet set =
- child.asTaskFragment().getAdjacentTaskFragments();
- isAnyTranslucent = set.forAllTaskFragments(
- tf -> !isOpaque(tf), null);
- } else {
- final TaskFragment adjacent = child.asTaskFragment()
- .getAdjacentTaskFragment();
- isAnyTranslucent = !isOpaque(child) || !isOpaque(adjacent);
- }
+ final boolean isAnyTranslucent = !isOpaque(child)
+ || child.asTaskFragment().forOtherAdjacentTaskFragments(
+ tf -> !isOpaque(tf));
if (!isAnyTranslucent) {
// This task fragment and all its adjacent task fragments are opaque,
// consider it opaque even if it doesn't fill its parent.
diff --git a/services/core/java/com/android/server/wm/AppCompatController.java b/services/core/java/com/android/server/wm/AppCompatController.java
index 48f08e945a59..c479591a5e0d 100644
--- a/services/core/java/com/android/server/wm/AppCompatController.java
+++ b/services/core/java/com/android/server/wm/AppCompatController.java
@@ -46,6 +46,8 @@ class AppCompatController {
private final AppCompatSizeCompatModePolicy mSizeCompatModePolicy;
@NonNull
private final AppCompatSandboxingPolicy mSandboxingPolicy;
+ @NonNull
+ private final AppCompatDisplayCompatModePolicy mDisplayCompatModePolicy;
AppCompatController(@NonNull WindowManagerService wmService,
@NonNull ActivityRecord activityRecord) {
@@ -69,6 +71,7 @@ class AppCompatController {
mSizeCompatModePolicy = new AppCompatSizeCompatModePolicy(activityRecord,
mAppCompatOverrides);
mSandboxingPolicy = new AppCompatSandboxingPolicy(activityRecord);
+ mDisplayCompatModePolicy = new AppCompatDisplayCompatModePolicy();
}
@NonNull
@@ -151,6 +154,11 @@ class AppCompatController {
return mSandboxingPolicy;
}
+ @NonNull
+ AppCompatDisplayCompatModePolicy getDisplayCompatModePolicy() {
+ return mDisplayCompatModePolicy;
+ }
+
void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
getTransparentPolicy().dump(pw, prefix);
getLetterboxPolicy().dump(pw, prefix);
diff --git a/services/core/java/com/android/server/wm/AppCompatDisplayCompatModePolicy.java b/services/core/java/com/android/server/wm/AppCompatDisplayCompatModePolicy.java
new file mode 100644
index 000000000000..acf51707c894
--- /dev/null
+++ b/services/core/java/com/android/server/wm/AppCompatDisplayCompatModePolicy.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import com.android.window.flags.Flags;
+
+/**
+ * Encapsulate app-compat logic for multi-display environments.
+ */
+class AppCompatDisplayCompatModePolicy {
+
+ private boolean mIsRestartMenuEnabledForDisplayMove;
+
+ boolean isRestartMenuEnabledForDisplayMove() {
+ return Flags.enableRestartMenuForConnectedDisplays() && mIsRestartMenuEnabledForDisplayMove;
+ }
+
+ void onMovedToDisplay() {
+ mIsRestartMenuEnabledForDisplayMove = true;
+ }
+
+ void onProcessRestarted() {
+ mIsRestartMenuEnabledForDisplayMove = false;
+ }
+}
diff --git a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java
index b03aa5263927..0f1e36d70db2 100644
--- a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java
@@ -361,7 +361,10 @@ class AppCompatSizeCompatModePolicy {
if (enableSizeCompatModeImprovementsForConnectedDisplays()) {
overrideConfig.touchscreen = fullConfig.touchscreen;
overrideConfig.navigation = fullConfig.navigation;
- overrideConfig.fontScale = fullConfig.fontScale;
+ overrideConfig.keyboard = fullConfig.keyboard;
+ overrideConfig.keyboardHidden = fullConfig.keyboardHidden;
+ overrideConfig.hardKeyboardHidden = fullConfig.hardKeyboardHidden;
+ overrideConfig.navigationHidden = fullConfig.navigationHidden;
}
// The smallest screen width is the short side of screen bounds. Because the bounds
// and density won't be changed, smallestScreenWidthDp is also fixed.
diff --git a/services/core/java/com/android/server/wm/AppCompatUtils.java b/services/core/java/com/android/server/wm/AppCompatUtils.java
index 146044008b3f..b91a12598e01 100644
--- a/services/core/java/com/android/server/wm/AppCompatUtils.java
+++ b/services/core/java/com/android/server/wm/AppCompatUtils.java
@@ -161,6 +161,9 @@ final class AppCompatUtils {
top.mAppCompatController.getLetterboxOverrides()
.isLetterboxEducationEnabled());
+ appCompatTaskInfo.setRestartMenuEnabledForDisplayMove(top.mAppCompatController
+ .getDisplayCompatModePolicy().isRestartMenuEnabledForDisplayMove());
+
final AppCompatAspectRatioOverrides aspectRatioOverrides =
top.mAppCompatController.getAspectRatioOverrides();
appCompatTaskInfo.setUserFullscreenOverrideEnabled(
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index e9b7649e8cbd..dfe323c43abb 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -479,21 +479,16 @@ class BackNavigationController {
}
} else {
// If adjacent TF has companion to current TF, those two TF will be closed together.
- final TaskFragment adjacentTF;
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- if (currTF.getAdjacentTaskFragments().size() > 2) {
- throw new IllegalStateException(
- "Not yet support 3+ adjacent for non-Task TFs");
- }
- final TaskFragment[] tmpAdjacent = new TaskFragment[1];
- currTF.forOtherAdjacentTaskFragments(tf -> {
- tmpAdjacent[0] = tf;
- return true;
- });
- adjacentTF = tmpAdjacent[0];
- } else {
- adjacentTF = currTF.getAdjacentTaskFragment();
+ if (currTF.getAdjacentTaskFragments().size() > 2) {
+ throw new IllegalStateException(
+ "Not yet support 3+ adjacent for non-Task TFs");
}
+ final TaskFragment[] tmpAdjacent = new TaskFragment[1];
+ currTF.forOtherAdjacentTaskFragments(tf -> {
+ tmpAdjacent[0] = tf;
+ return true;
+ });
+ final TaskFragment adjacentTF = tmpAdjacent[0];
if (isSecondCompanionToFirst(currTF, adjacentTF)) {
// The two TFs are adjacent (visually displayed side-by-side), search if any
// activity below the lowest one.
@@ -553,15 +548,6 @@ class BackNavigationController {
if (!prevTF.hasAdjacentTaskFragment()) {
return;
}
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- final TaskFragment prevTFAdjacent = prevTF.getAdjacentTaskFragment();
- final ActivityRecord prevActivityAdjacent =
- prevTFAdjacent.getTopNonFinishingActivity();
- if (prevActivityAdjacent != null) {
- outPrevActivities.add(prevActivityAdjacent);
- }
- return;
- }
prevTF.forOtherAdjacentTaskFragments(prevTFAdjacent -> {
final ActivityRecord prevActivityAdjacent =
prevTFAdjacent.getTopNonFinishingActivity();
@@ -577,14 +563,6 @@ class BackNavigationController {
if (mainTF == null || !mainTF.hasAdjacentTaskFragment()) {
return;
}
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- final TaskFragment adjacentTF = mainTF.getAdjacentTaskFragment();
- final ActivityRecord topActivity = adjacentTF.getTopNonFinishingActivity();
- if (topActivity != null) {
- outList.add(topActivity);
- }
- return;
- }
mainTF.forOtherAdjacentTaskFragments(adjacentTF -> {
final ActivityRecord topActivity = adjacentTF.getTopNonFinishingActivity();
if (topActivity != null) {
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index 2287a687700c..f50a68cc5389 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -49,8 +49,8 @@ import static com.android.window.flags.Flags.balDontBringExistingBackgroundTaskS
import static com.android.window.flags.Flags.balImprovedMetrics;
import static com.android.window.flags.Flags.balRequireOptInByPendingIntentCreator;
import static com.android.window.flags.Flags.balShowToastsBlocked;
-import static com.android.window.flags.Flags.balStrictModeRo;
import static com.android.window.flags.Flags.balStrictModeGracePeriod;
+import static com.android.window.flags.Flags.balStrictModeRo;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import static java.util.Objects.requireNonNull;
@@ -91,7 +91,6 @@ import com.android.internal.util.Preconditions;
import com.android.server.UiThread;
import com.android.server.am.PendingIntentRecord;
import com.android.server.wm.BackgroundLaunchProcessController.BalCheckConfiguration;
-import com.android.window.flags.Flags;
import java.lang.annotation.Retention;
import java.util.ArrayList;
@@ -1687,14 +1686,6 @@ public class BackgroundActivityStartController {
}
// Check the adjacent fragment.
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- TaskFragment adjacentTaskFragment = taskFragment.getAdjacentTaskFragment();
- topActivity = adjacentTaskFragment.getActivity(topOfStackPredicate);
- if (topActivity == null) {
- return bas;
- }
- return checkCrossUidActivitySwitchFromBelow(topActivity, uid, bas);
- }
final BlockActivityStart[] out = { bas };
taskFragment.forOtherAdjacentTaskFragments(adjacentTaskFragment -> {
final ActivityRecord top = adjacentTaskFragment.getActivity(topOfStackPredicate);
diff --git a/services/core/java/com/android/server/wm/BaseAppSnapshotPersister.java b/services/core/java/com/android/server/wm/BaseAppSnapshotPersister.java
index 5db02dff8351..aaa5a0074506 100644
--- a/services/core/java/com/android/server/wm/BaseAppSnapshotPersister.java
+++ b/services/core/java/com/android/server/wm/BaseAppSnapshotPersister.java
@@ -16,10 +16,20 @@
package com.android.server.wm;
+import static com.android.server.wm.AbsAppSnapshotController.TAG;
+
import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
import android.window.TaskSnapshot;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.window.flags.Flags;
+
import java.io.File;
+import java.util.UUID;
class BaseAppSnapshotPersister {
static final String LOW_RES_FILE_POSTFIX = "_reduced";
@@ -29,6 +39,7 @@ class BaseAppSnapshotPersister {
// Shared with SnapshotPersistQueue
protected final Object mLock;
protected final SnapshotPersistQueue mSnapshotPersistQueue;
+ @VisibleForTesting
protected final PersistInfoProvider mPersistInfoProvider;
BaseAppSnapshotPersister(SnapshotPersistQueue persistQueue,
@@ -79,6 +90,8 @@ class BaseAppSnapshotPersister {
private final boolean mEnableLowResSnapshots;
private final float mLowResScaleFactor;
private final boolean mUse16BitFormat;
+ private final SparseBooleanArray mInitializedUsers = new SparseBooleanArray();
+ private final SparseArray<File> mScrambleDirectories = new SparseArray<>();
PersistInfoProvider(DirectoryResolver directoryResolver, String dirName,
boolean enableLowResSnapshots, float lowResScaleFactor, boolean use16BitFormat) {
@@ -91,9 +104,80 @@ class BaseAppSnapshotPersister {
@NonNull
File getDirectory(int userId) {
+ if (Flags.scrambleSnapshotFileName()) {
+ final File directory = getOrInitScrambleDirectory(userId);
+ if (directory != null) {
+ return directory;
+ }
+ }
+ return getBaseDirectory(userId);
+ }
+
+ @NonNull
+ private File getBaseDirectory(int userId) {
return new File(mDirectoryResolver.getSystemDirectoryForUser(userId), mDirName);
}
+ @Nullable
+ private File getOrInitScrambleDirectory(int userId) {
+ synchronized (mScrambleDirectories) {
+ if (mInitializedUsers.get(userId)) {
+ return mScrambleDirectories.get(userId);
+ }
+ mInitializedUsers.put(userId, true);
+ final File scrambledDirectory = getScrambleDirectory(userId);
+ final File baseDir = getBaseDirectory(userId);
+ String newName = null;
+ // If directory exists, rename
+ if (scrambledDirectory.exists()) {
+ newName = UUID.randomUUID().toString();
+ final File scrambleTo = new File(baseDir, newName);
+ if (!scrambledDirectory.renameTo(scrambleTo)) {
+ Slog.w(TAG, "SnapshotPersister rename scramble folder fail.");
+ return null;
+ }
+ } else {
+ // If directory not exists, mkDir.
+ if (!baseDir.exists() && !baseDir.mkdir()) {
+ Slog.w(TAG, "SnapshotPersister make base folder fail.");
+ return null;
+ }
+ if (!scrambledDirectory.mkdir()) {
+ Slog.e(TAG, "SnapshotPersister make scramble folder fail");
+ return null;
+ }
+ // Move any existing files to this folder.
+ final String[] files = baseDir.list();
+ if (files != null) {
+ for (String file : files) {
+ final File original = new File(baseDir, file);
+ if (original.isDirectory()) {
+ newName = file;
+ } else {
+ File to = new File(scrambledDirectory, file);
+ original.renameTo(to);
+ }
+ }
+ }
+ }
+ final File newFolder = new File(baseDir, newName);
+ mScrambleDirectories.put(userId, newFolder);
+ return newFolder;
+ }
+ }
+
+ @NonNull
+ private File getScrambleDirectory(int userId) {
+ final File dir = getBaseDirectory(userId);
+ final String[] directories = dir.list(
+ (current, name) -> new File(current, name).isDirectory());
+ if (directories != null && directories.length > 0) {
+ return new File(dir, directories[0]);
+ } else {
+ return new File(dir, UUID.randomUUID().toString());
+ }
+ }
+
/**
* Return if task snapshots are stored in 16 bit pixel format.
*
diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
index f473b7b7e4fb..fcc697242ff6 100644
--- a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
+++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
@@ -18,7 +18,7 @@ package com.android.server.wm;
import static android.view.WindowManager.TRANSIT_CHANGE;
-import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS;
+import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS_MIN;
import static com.android.server.wm.ActivityTaskManagerService.POWER_MODE_REASON_CHANGE_DISPLAY;
import static com.android.server.wm.utils.DisplayInfoOverrides.WM_OVERRIDE_FIELDS;
import static com.android.server.wm.utils.DisplayInfoOverrides.copyDisplayInfoFields;
@@ -140,8 +140,9 @@ class DeferredDisplayUpdater {
if (displayInfoDiff == DIFF_EVERYTHING
|| !mDisplayContent.getLastHasContent()
|| !mDisplayContent.mTransitionController.isShellTransitionsEnabled()) {
- ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS,
- "DeferredDisplayUpdater: applying DisplayInfo immediately");
+ ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS_MIN,
+ "DeferredDisplayUpdater: applying DisplayInfo(%d x %d) immediately",
+ displayInfo.logicalWidth, displayInfo.logicalHeight);
mLastWmDisplayInfo = displayInfo;
applyLatestDisplayInfo();
@@ -151,17 +152,23 @@ class DeferredDisplayUpdater {
// If there are non WM-specific display info changes, apply only these fields immediately
if ((displayInfoDiff & DIFF_NOT_WM_DEFERRABLE) > 0) {
- ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS,
- "DeferredDisplayUpdater: partially applying DisplayInfo immediately");
+ ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS_MIN,
+ "DeferredDisplayUpdater: partially applying DisplayInfo(%d x %d) immediately",
+ displayInfo.logicalWidth, displayInfo.logicalHeight);
applyLatestDisplayInfo();
}
// If there are WM-specific display info changes, apply them through a Shell transition
if ((displayInfoDiff & DIFF_WM_DEFERRABLE) > 0) {
- ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS,
- "DeferredDisplayUpdater: deferring DisplayInfo update");
+ ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS_MIN,
+ "DeferredDisplayUpdater: deferring DisplayInfo(%d x %d) update",
+ displayInfo.logicalWidth, displayInfo.logicalHeight);
requestDisplayChangeTransition(physicalDisplayUpdated, () -> {
+ ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS_MIN,
+ "DeferredDisplayUpdater: applying DisplayInfo(%d x %d) after deferring",
+ displayInfo.logicalWidth, displayInfo.logicalHeight);
+
// Apply deferrable fields to DisplayContent only when the transition
// starts collecting, non-deferrable fields are ignored in mLastWmDisplayInfo
mLastWmDisplayInfo = displayInfo;
@@ -199,7 +206,7 @@ class DeferredDisplayUpdater {
mDisplayContent.getDisplayPolicy().getNotificationShade();
if (notificationShade != null && notificationShade.isVisible()
&& mDisplayContent.mAtmService.mKeyguardController.isKeyguardOrAodShowing(
- mDisplayContent.mDisplayId)) {
+ mDisplayContent.mDisplayId)) {
Slog.i(TAG, notificationShade + " uses blast for display switch");
notificationShade.mSyncMethodOverride = BLASTSyncEngine.METHOD_BLAST;
}
@@ -209,9 +216,6 @@ class DeferredDisplayUpdater {
try {
onStartCollect.run();
- ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS,
- "DeferredDisplayUpdater: applied DisplayInfo after deferring");
-
if (physicalDisplayUpdated) {
onDisplayUpdated(transition, fromRotation, startBounds);
} else {
diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
index d466a646c7dd..ddcb5eccb1d8 100644
--- a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
+++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
@@ -19,6 +19,12 @@ package com.android.server.wm;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE;
+import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE_PER_TASK;
+import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_TASK;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
@@ -131,6 +137,18 @@ class DesktopModeLaunchParamsModifier implements LaunchParamsModifier {
return RESULT_SKIP;
}
+ if (DesktopModeFlags.INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES.isTrue()) {
+ ActivityRecord topVisibleFreeformActivity =
+ task.getDisplayContent().getTopMostVisibleFreeformActivity();
+ if (shouldInheritExistingTaskBounds(topVisibleFreeformActivity, activity, task)) {
+ appendLog("inheriting bounds from existing closing instance");
+ outParams.mBounds.set(topVisibleFreeformActivity.getBounds());
+ appendLog("final desktop mode task bounds set to %s", outParams.mBounds);
+ // Return result done to prevent other modifiers from changing or cascading bounds.
+ return RESULT_DONE;
+ }
+ }
+
DesktopModeBoundsCalculator.updateInitialBounds(task, layout, activity, options,
outParams.mBounds, this::appendLog);
appendLog("final desktop mode task bounds set to %s", outParams.mBounds);
@@ -159,7 +177,7 @@ class DesktopModeLaunchParamsModifier implements LaunchParamsModifier {
// activity will also enter desktop mode. On this same relationship, we can also assume
// if there are not visible freeform tasks but a freeform activity is now launching, it
// will force the device into desktop mode.
- return (task.getDisplayContent().getTopMostVisibleFreeformActivity() != null
+ return (task.getDisplayContent().getTopMostFreeformActivity() != null
&& checkSourceWindowModesCompatible(task, options, currentParams))
|| isRequestingFreeformWindowMode(task, options, currentParams);
}
@@ -201,6 +219,40 @@ class DesktopModeLaunchParamsModifier implements LaunchParamsModifier {
};
}
+ /**
+ * Whether the launching task should inherit the task bounds of an existing closing instance.
+ */
+ private boolean shouldInheritExistingTaskBounds(
+ @Nullable ActivityRecord existingTaskActivity,
+ @Nullable ActivityRecord launchingActivity,
+ @NonNull Task launchingTask) {
+ if (existingTaskActivity == null || launchingActivity == null) return false;
+ return (existingTaskActivity.packageName == launchingActivity.packageName)
+ && isLaunchingNewTask(launchingActivity.launchMode,
+ launchingTask.getBaseIntent().getFlags())
+ && isClosingExitingInstance(launchingTask.getBaseIntent().getFlags());
+ }
+
+ /**
+ * Returns true if the launch mode or intent will result in a new task being created for the
+ * activity.
+ */
+ private boolean isLaunchingNewTask(int launchMode, int intentFlags) {
+ return launchMode == LAUNCH_SINGLE_TASK
+ || launchMode == LAUNCH_SINGLE_INSTANCE
+ || launchMode == LAUNCH_SINGLE_INSTANCE_PER_TASK
+ || (intentFlags & FLAG_ACTIVITY_NEW_TASK) != 0;
+ }
+
+ /**
+ * Returns true if the intent will result in an existing task instance being closed if a new
+ * one appears.
+ */
+ private boolean isClosingExitingInstance(int intentFlags) {
+ return (intentFlags & FLAG_ACTIVITY_CLEAR_TASK) != 0
+ || (intentFlags & FLAG_ACTIVITY_MULTIPLE_TASK) == 0;
+ }
+
private void initLogBuilder(Task task, ActivityRecord activity) {
if (DEBUG) {
mLogBuilder = new StringBuilder(
diff --git a/services/core/java/com/android/server/wm/DisplayWindowListenerController.java b/services/core/java/com/android/server/wm/DisplayWindowListenerController.java
index fa7a99d55896..d90fff229cd9 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowListenerController.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowListenerController.java
@@ -40,14 +40,14 @@ class DisplayWindowListenerController {
}
int[] registerListener(IDisplayWindowListener listener) {
+ mDisplayListeners.register(listener);
+ final IntArray displayIds = new IntArray();
synchronized (mService.mGlobalLock) {
- mDisplayListeners.register(listener);
- final IntArray displayIds = new IntArray();
mService.mAtmService.mRootWindowContainer.forAllDisplays((displayContent) -> {
displayIds.add(displayContent.mDisplayId);
});
- return displayIds.toArray();
}
+ return displayIds.toArray();
}
void unregisterListener(IDisplayWindowListener listener) {
diff --git a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
index a017a1173d97..e508a6d23178 100644
--- a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
+++ b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
@@ -23,8 +23,6 @@ import static com.android.server.wm.Task.TAG_VISIBILITY;
import android.annotation.Nullable;
import android.util.Slog;
-import com.android.window.flags.Flags;
-
import java.util.ArrayList;
/** Helper class to ensure activities are in the right visible state for a container. */
@@ -112,18 +110,11 @@ class EnsureActivitiesVisibleHelper {
if (adjacentTaskFragments != null && adjacentTaskFragments.contains(
childTaskFragment)) {
- final boolean isTranslucent;
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- isTranslucent = childTaskFragment.isTranslucent(starting)
- || childTaskFragment.forOtherAdjacentTaskFragments(
- adjacentTaskFragment -> {
- return adjacentTaskFragment.isTranslucent(starting);
- });
- } else {
- isTranslucent = childTaskFragment.isTranslucent(starting)
- || childTaskFragment.getAdjacentTaskFragment()
- .isTranslucent(starting);
- }
+ final boolean isTranslucent = childTaskFragment.isTranslucent(starting)
+ || childTaskFragment.forOtherAdjacentTaskFragments(
+ adjacentTaskFragment -> {
+ return adjacentTaskFragment.isTranslucent(starting);
+ });
if (!isTranslucent) {
// Everything behind two adjacent TaskFragments are occluded.
mBehindFullyOccludedContainer = true;
@@ -135,14 +126,10 @@ class EnsureActivitiesVisibleHelper {
if (adjacentTaskFragments == null) {
adjacentTaskFragments = new ArrayList<>();
}
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- final ArrayList<TaskFragment> adjacentTfs = adjacentTaskFragments;
- childTaskFragment.forOtherAdjacentTaskFragments(adjacentTf -> {
- adjacentTfs.add(adjacentTf);
- });
- } else {
- adjacentTaskFragments.add(childTaskFragment.getAdjacentTaskFragment());
- }
+ final ArrayList<TaskFragment> adjacentTfs = adjacentTaskFragments;
+ childTaskFragment.forOtherAdjacentTaskFragments(adjacentTf -> {
+ adjacentTfs.add(adjacentTf);
+ });
}
} else if (child.asActivityRecord() != null) {
setActivityVisibilityState(child.asActivityRecord(), starting, resumeTopActivity);
diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
index 2cac63c1e5e9..a937691e7998 100644
--- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
@@ -263,8 +263,8 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider {
boolean oldVisibility = mSource.isVisible();
super.updateVisibility();
if (Flags.refactorInsetsController()) {
- if (mSource.isVisible() && !oldVisibility && mImeRequester != null) {
- reportImeDrawnForOrganizerIfNeeded(mImeRequester);
+ if (mSource.isVisible() && !oldVisibility && mControlTarget != null) {
+ reportImeDrawnForOrganizerIfNeeded(mControlTarget);
}
}
onSourceChanged();
diff --git a/services/core/java/com/android/server/wm/InputManagerCallback.java b/services/core/java/com/android/server/wm/InputManagerCallback.java
index 7751ac3f9fc6..a4bc5cbcb5d3 100644
--- a/services/core/java/com/android/server/wm/InputManagerCallback.java
+++ b/services/core/java/com/android/server/wm/InputManagerCallback.java
@@ -343,6 +343,13 @@ final class InputManagerCallback implements InputManagerService.WindowManagerCal
}
}
+ @Override
+ public boolean isKeyguardLocked(int displayId) {
+ synchronized (mService.mGlobalLock) {
+ return mService.mAtmService.mKeyguardController.isKeyguardLocked(displayId);
+ }
+ }
+
/** Waits until the built-in input devices have been configured. */
public boolean waitForInputDevicesReady(long timeoutMillis) {
synchronized (mInputDevicesReadyMonitor) {
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index cf201c9f34f0..609302ce3f56 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -1732,26 +1732,14 @@ class RootWindowContainer extends WindowContainer<DisplayContent>
activityAssistInfos.clear();
activityAssistInfos.add(new ActivityAssistInfo(top));
// Check if the activity on the split screen.
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- top.getTask().forOtherAdjacentTasks(task -> {
- final ActivityRecord adjacentActivityRecord =
- task.getTopNonFinishingActivity();
- if (adjacentActivityRecord != null) {
- activityAssistInfos.add(
- new ActivityAssistInfo(adjacentActivityRecord));
- }
- });
- } else {
- final Task adjacentTask = top.getTask().getAdjacentTask();
- if (adjacentTask != null) {
- final ActivityRecord adjacentActivityRecord =
- adjacentTask.getTopNonFinishingActivity();
- if (adjacentActivityRecord != null) {
- activityAssistInfos.add(
- new ActivityAssistInfo(adjacentActivityRecord));
- }
+ top.getTask().forOtherAdjacentTasks(task -> {
+ final ActivityRecord adjacentActivityRecord =
+ task.getTopNonFinishingActivity();
+ if (adjacentActivityRecord != null) {
+ activityAssistInfos.add(
+ new ActivityAssistInfo(adjacentActivityRecord));
}
- }
+ });
if (rootTask == topFocusedRootTask) {
topVisibleActivities.addAll(0, activityAssistInfos);
} else {
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index d16c301cec40..e98b2b749af8 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -2461,21 +2461,6 @@ class Task extends TaskFragment {
return parentTask == null ? null : parentTask.getCreatedByOrganizerTask();
}
- /** @deprecated b/373709676 replace with {@link #forOtherAdjacentTasks(Consumer)} ()}. */
- @Deprecated
- @Nullable
- Task getAdjacentTask() {
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- throw new IllegalStateException("allowMultipleAdjacentTaskFragments is enabled. "
- + "Use #forOtherAdjacentTasks instead");
- }
- final Task taskWithAdjacent = getTaskWithAdjacent();
- if (taskWithAdjacent == null) {
- return null;
- }
- return taskWithAdjacent.getAdjacentTaskFragment().asTask();
- }
-
/** Finds the first Task parent (or itself) that has adjacent. */
@Nullable
Task getTaskWithAdjacent() {
@@ -2499,11 +2484,6 @@ class Task extends TaskFragment {
* Tasks. The invoke order is not guaranteed.
*/
void forOtherAdjacentTasks(@NonNull Consumer<Task> callback) {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- throw new IllegalStateException("allowMultipleAdjacentTaskFragments is not enabled. "
- + "Use #getAdjacentTask instead");
- }
-
final Task taskWithAdjacent = getTaskWithAdjacent();
if (taskWithAdjacent == null) {
return;
@@ -2521,10 +2501,6 @@ class Task extends TaskFragment {
* guaranteed.
*/
boolean forOtherAdjacentTasks(@NonNull Predicate<Task> callback) {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- throw new IllegalStateException("allowMultipleAdjacentTaskFragments is not enabled. "
- + "Use getAdjacentTask instead");
- }
final Task taskWithAdjacent = getTaskWithAdjacent();
if (taskWithAdjacent == null) {
return false;
@@ -3651,20 +3627,13 @@ class Task extends TaskFragment {
final TaskFragment taskFragment = wc.asTaskFragment();
if (taskFragment != null && taskFragment.isEmbedded()
&& taskFragment.hasAdjacentTaskFragment()) {
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- final int[] nextLayer = { layer };
- taskFragment.forOtherAdjacentTaskFragments(adjacentTf -> {
- if (adjacentTf.shouldBoostDimmer()) {
- adjacentTf.assignLayer(t, nextLayer[0]++);
- }
- });
- layer = nextLayer[0];
- } else {
- final TaskFragment adjacentTf = taskFragment.getAdjacentTaskFragment();
+ final int[] nextLayer = { layer };
+ taskFragment.forOtherAdjacentTaskFragments(adjacentTf -> {
if (adjacentTf.shouldBoostDimmer()) {
- adjacentTf.assignLayer(t, layer++);
+ adjacentTf.assignLayer(t, nextLayer[0]++);
}
- }
+ });
+ layer = nextLayer[0];
}
// Place the decor surface just above the owner TaskFragment.
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index 1966ecf57c73..fb7bab4b3e26 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -60,7 +60,6 @@ import com.android.internal.util.function.pooled.PooledLambda;
import com.android.internal.util.function.pooled.PooledPredicate;
import com.android.server.pm.UserManagerInternal;
import com.android.server.wm.LaunchParamsController.LaunchParams;
-import com.android.window.flags.Flags;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -1089,19 +1088,14 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> {
// Use launch-adjacent-flag-root if launching with launch-adjacent flag.
if ((launchFlags & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0
&& mLaunchAdjacentFlagRootTask != null) {
- final Task launchAdjacentRootAdjacentTask;
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- final Task[] tmpTask = new Task[1];
- mLaunchAdjacentFlagRootTask.forOtherAdjacentTasks(task -> {
- // TODO(b/382208145): enable FLAG_ACTIVITY_LAUNCH_ADJACENT for 3+.
- // Find the first adjacent for now.
- tmpTask[0] = task;
- return true;
- });
- launchAdjacentRootAdjacentTask = tmpTask[0];
- } else {
- launchAdjacentRootAdjacentTask = mLaunchAdjacentFlagRootTask.getAdjacentTask();
- }
+ final Task[] tmpTask = new Task[1];
+ mLaunchAdjacentFlagRootTask.forOtherAdjacentTasks(task -> {
+ // TODO(b/382208145): enable FLAG_ACTIVITY_LAUNCH_ADJACENT for 3+.
+ // Find the first adjacent for now.
+ tmpTask[0] = task;
+ return true;
+ });
+ final Task launchAdjacentRootAdjacentTask = tmpTask[0];
if (sourceTask != null && (sourceTask == candidateTask
|| sourceTask.topRunningActivity() == null)) {
// Do nothing when task that is getting opened is same as the source or when
@@ -1129,14 +1123,6 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> {
if (launchRootTask == null || sourceTask == null) {
return launchRootTask;
}
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- final Task adjacentRootTask = launchRootTask.getAdjacentTask();
- if (adjacentRootTask != null && (sourceTask == adjacentRootTask
- || sourceTask.isDescendantOf(adjacentRootTask))) {
- return adjacentRootTask;
- }
- return launchRootTask;
- }
final Task[] adjacentRootTask = new Task[1];
launchRootTask.forOtherAdjacentTasks(task -> {
if (sourceTask == task || sourceTask.isDescendantOf(task)) {
@@ -1163,24 +1149,16 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> {
return sourceTask.getCreatedByOrganizerTask();
}
// Check if the candidate is already positioned in the adjacent Task.
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- final Task[] adjacentRootTask = new Task[1];
- sourceTask.forOtherAdjacentTasks(task -> {
- if (candidateTask == task || candidateTask.isDescendantOf(task)) {
- adjacentRootTask[0] = task;
- return true;
- }
- return false;
- });
- if (adjacentRootTask[0] != null) {
- return adjacentRootTask[0];
- }
- } else {
- final Task adjacentTarget = taskWithAdjacent.getAdjacentTask();
- if (candidateTask == adjacentTarget
- || candidateTask.isDescendantOf(adjacentTarget)) {
- return adjacentTarget;
+ final Task[] adjacentRootTask = new Task[1];
+ sourceTask.forOtherAdjacentTasks(task -> {
+ if (candidateTask == task || candidateTask.isDescendantOf(task)) {
+ adjacentRootTask[0] = task;
+ return true;
}
+ return false;
+ });
+ if (adjacentRootTask[0] != null) {
+ return adjacentRootTask[0];
}
return sourceTask.getCreatedByOrganizerTask();
}
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 5183c6b57f15..2dabb253744a 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -235,11 +235,6 @@ class TaskFragment extends WindowContainer<WindowContainer> {
/** This task fragment will be removed when the cleanup of its children are done. */
private boolean mIsRemovalRequested;
- /** @deprecated b/373709676 replace with {@link #mAdjacentTaskFragments} */
- @Deprecated
- @Nullable
- private TaskFragment mAdjacentTaskFragment;
-
/**
* The TaskFragments that are adjacent to each other, including this TaskFragment.
* All TaskFragments in this set share the same set instance.
@@ -455,22 +450,6 @@ class TaskFragment extends WindowContainer<WindowContainer> {
return service.mWindowOrganizerController.getTaskFragment(token);
}
- /** @deprecated b/373709676 replace with {@link #setAdjacentTaskFragments}. */
- @Deprecated
- void setAdjacentTaskFragment(@NonNull TaskFragment taskFragment) {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- if (mAdjacentTaskFragment == taskFragment) {
- return;
- }
- resetAdjacentTaskFragment();
- mAdjacentTaskFragment = taskFragment;
- taskFragment.setAdjacentTaskFragment(this);
- return;
- }
-
- setAdjacentTaskFragments(new AdjacentSet(this, taskFragment));
- }
-
void setAdjacentTaskFragments(@NonNull AdjacentSet adjacentTaskFragments) {
adjacentTaskFragments.setAsAdjacent();
}
@@ -483,56 +462,18 @@ class TaskFragment extends WindowContainer<WindowContainer> {
return mCompanionTaskFragment;
}
- /** @deprecated b/373709676 replace with {@link #clearAdjacentTaskFragments()}. */
- @Deprecated
- private void resetAdjacentTaskFragment() {
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- throw new IllegalStateException("resetAdjacentTaskFragment shouldn't be called when"
- + " allowMultipleAdjacentTaskFragments is enabled. Use either"
- + " #clearAdjacentTaskFragments or #removeFromAdjacentTaskFragments");
- }
- // Reset the adjacent TaskFragment if its adjacent TaskFragment is also this TaskFragment.
- if (mAdjacentTaskFragment != null && mAdjacentTaskFragment.mAdjacentTaskFragment == this) {
- mAdjacentTaskFragment.mAdjacentTaskFragment = null;
- mAdjacentTaskFragment.mDelayLastActivityRemoval = false;
- }
- mAdjacentTaskFragment = null;
- mDelayLastActivityRemoval = false;
- }
-
void clearAdjacentTaskFragments() {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- resetAdjacentTaskFragment();
- return;
- }
-
if (mAdjacentTaskFragments != null) {
mAdjacentTaskFragments.clear();
}
}
void removeFromAdjacentTaskFragments() {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- resetAdjacentTaskFragment();
- return;
- }
-
if (mAdjacentTaskFragments != null) {
mAdjacentTaskFragments.remove(this);
}
}
- /** @deprecated b/373709676 replace with {@link #getAdjacentTaskFragments()}. */
- @Deprecated
- @Nullable
- TaskFragment getAdjacentTaskFragment() {
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- throw new IllegalStateException("allowMultipleAdjacentTaskFragments is enabled. "
- + "Use #getAdjacentTaskFragments instead");
- }
- return mAdjacentTaskFragment;
- }
-
@Nullable
AdjacentSet getAdjacentTaskFragments() {
return mAdjacentTaskFragments;
@@ -561,16 +502,10 @@ class TaskFragment extends WindowContainer<WindowContainer> {
}
boolean hasAdjacentTaskFragment() {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- return mAdjacentTaskFragment != null;
- }
return mAdjacentTaskFragments != null;
}
boolean isAdjacentTo(@NonNull TaskFragment other) {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- return mAdjacentTaskFragment == other;
- }
return other != this
&& mAdjacentTaskFragments != null
&& mAdjacentTaskFragments.contains(other);
@@ -1377,21 +1312,12 @@ class TaskFragment extends WindowContainer<WindowContainer> {
if (taskFragment.isAdjacentTo(this)) {
continue;
}
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- final boolean isOccluding = mTmpRect.intersect(taskFragment.getBounds())
- || taskFragment.forOtherAdjacentTaskFragments(adjacentTf -> {
- return mTmpRect.intersect(adjacentTf.getBounds());
- });
- if (isOccluding) {
- return TASK_FRAGMENT_VISIBILITY_INVISIBLE;
- }
- } else {
- final TaskFragment adjacentTaskFragment =
- taskFragment.mAdjacentTaskFragment;
- if (mTmpRect.intersect(taskFragment.getBounds())
- || mTmpRect.intersect(adjacentTaskFragment.getBounds())) {
- return TASK_FRAGMENT_VISIBILITY_INVISIBLE;
- }
+ final boolean isOccluding = mTmpRect.intersect(taskFragment.getBounds())
+ || taskFragment.forOtherAdjacentTaskFragments(adjacentTf -> {
+ return mTmpRect.intersect(adjacentTf.getBounds());
+ });
+ if (isOccluding) {
+ return TASK_FRAGMENT_VISIBILITY_INVISIBLE;
}
}
}
@@ -1427,37 +1353,22 @@ class TaskFragment extends WindowContainer<WindowContainer> {
// 2. Adjacent TaskFragments do not overlap, so that if this TaskFragment is behind
// any translucent TaskFragment in the adjacent set, then this TaskFragment is
// visible behind translucent.
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- final boolean hasTraversedAdj = otherTaskFrag.forOtherAdjacentTaskFragments(
- adjacentTaskFragments::contains);
- if (hasTraversedAdj) {
- final boolean isTranslucent =
- isBehindTransparentTaskFragment(otherTaskFrag, starting)
- || otherTaskFrag.forOtherAdjacentTaskFragments(
- (Predicate<TaskFragment>) tf ->
- isBehindTransparentTaskFragment(tf, starting));
- if (isTranslucent) {
- // Can be visible behind a translucent adjacent TaskFragments.
- gotTranslucentFullscreen = true;
- gotTranslucentAdjacent = true;
- continue;
- }
- // Can not be visible behind adjacent TaskFragments.
- return TASK_FRAGMENT_VISIBILITY_INVISIBLE;
- }
- } else {
- if (adjacentTaskFragments.contains(otherTaskFrag.mAdjacentTaskFragment)) {
- if (isBehindTransparentTaskFragment(otherTaskFrag, starting)
- || isBehindTransparentTaskFragment(
- otherTaskFrag.mAdjacentTaskFragment, starting)) {
- // Can be visible behind a translucent adjacent TaskFragments.
- gotTranslucentFullscreen = true;
- gotTranslucentAdjacent = true;
- continue;
- }
- // Can not be visible behind adjacent TaskFragments.
- return TASK_FRAGMENT_VISIBILITY_INVISIBLE;
+ final boolean hasTraversedAdj = otherTaskFrag.forOtherAdjacentTaskFragments(
+ adjacentTaskFragments::contains);
+ if (hasTraversedAdj) {
+ final boolean isTranslucent =
+ isBehindTransparentTaskFragment(otherTaskFrag, starting)
+ || otherTaskFrag.forOtherAdjacentTaskFragments(
+ (Predicate<TaskFragment>) tf ->
+ isBehindTransparentTaskFragment(tf, starting));
+ if (isTranslucent) {
+ // Can be visible behind a translucent adjacent TaskFragments.
+ gotTranslucentFullscreen = true;
+ gotTranslucentAdjacent = true;
+ continue;
}
+ // Can not be visible behind adjacent TaskFragments.
+ return TASK_FRAGMENT_VISIBILITY_INVISIBLE;
}
adjacentTaskFragments.add(otherTaskFrag);
}
@@ -3299,40 +3210,23 @@ class TaskFragment extends WindowContainer<WindowContainer> {
final ArrayList<WindowContainer> siblings = getParent().mChildren;
final int zOrder = siblings.indexOf(this);
-
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- if (siblings.indexOf(getAdjacentTaskFragment()) < zOrder) {
- // early return if this TF already has higher z-ordering.
- return false;
- }
- } else {
- final boolean hasAdjacentOnTop = forOtherAdjacentTaskFragments(
- tf -> siblings.indexOf(tf) > zOrder);
- if (!hasAdjacentOnTop) {
- // early return if this TF already has higher z-ordering.
- return false;
- }
+ final boolean hasAdjacentOnTop = forOtherAdjacentTaskFragments(
+ tf -> siblings.indexOf(tf) > zOrder);
+ if (!hasAdjacentOnTop) {
+ // early return if this TF already has higher z-ordering.
+ return false;
}
final ToBooleanFunction<WindowState> getDimBehindWindow =
(w) -> (w.mAttrs.flags & FLAG_DIM_BEHIND) != 0 && w.mActivityRecord != null
&& w.mActivityRecord.isEmbedded() && (w.mActivityRecord.isVisibleRequested()
|| w.mActivityRecord.isVisible());
-
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- final TaskFragment adjacentTf = getAdjacentTaskFragment();
- if (adjacentTf.forAllWindows(getDimBehindWindow, true)) {
- // early return if the adjacent Tf has a dimming window.
- return false;
- }
- } else {
- final boolean adjacentHasDimmingWindow = forOtherAdjacentTaskFragments(tf -> {
- return tf.forAllWindows(getDimBehindWindow, true);
- });
- if (adjacentHasDimmingWindow) {
- // early return if the adjacent Tf has a dimming window.
- return false;
- }
+ final boolean adjacentHasDimmingWindow = forOtherAdjacentTaskFragments(tf -> {
+ return tf.forAllWindows(getDimBehindWindow, true);
+ });
+ if (adjacentHasDimmingWindow) {
+ // early return if the adjacent Tf has a dimming window.
+ return false;
}
// boost if there's an Activity window that has FLAG_DIM_BEHIND flag.
@@ -3456,16 +3350,9 @@ class TaskFragment extends WindowContainer<WindowContainer> {
sb.append(" organizerProc=");
sb.append(mTaskFragmentOrganizerProcessName);
}
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- if (mAdjacentTaskFragments != null) {
- sb.append(" adjacent=");
- sb.append(mAdjacentTaskFragments);
- }
- } else {
- if (mAdjacentTaskFragment != null) {
- sb.append(" adjacent=");
- sb.append(mAdjacentTaskFragment);
- }
+ if (mAdjacentTaskFragments != null) {
+ sb.append(" adjacent=");
+ sb.append(mAdjacentTaskFragments);
}
sb.append('}');
return sb.toString();
@@ -3591,10 +3478,6 @@ class TaskFragment extends WindowContainer<WindowContainer> {
}
AdjacentSet(@NonNull ArraySet<TaskFragment> taskFragments) {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- throw new IllegalStateException("allowMultipleAdjacentTaskFragments must be"
- + " enabled to set more than two TaskFragments adjacent to each other.");
- }
final int size = taskFragments.size();
if (size < 2) {
throw new IllegalArgumentException("Adjacent TaskFragments must contain at least"
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index c78cdaa10df2..803c21ccab6e 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -2589,9 +2589,6 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
}
// When the TaskFragment has an adjacent TaskFragment, sibling behind them should be
// hidden unless any of them are translucent.
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- return taskFragment.getAdjacentTaskFragment().isTranslucentForTransition();
- }
return taskFragment.forOtherAdjacentTaskFragments(TaskFragment::isTranslucentForTransition);
}
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index 3a4d9d27f65a..e1553cd37d03 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -57,7 +57,8 @@ import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.ProtoLog;
import com.android.internal.util.ToBooleanFunction;
-import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils;
+import com.android.server.wallpaper.WallpaperCropper;
+import com.android.server.wallpaper.WallpaperDefaultDisplayInfo;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -71,7 +72,6 @@ import java.util.function.Consumer;
class WallpaperController {
private static final String TAG = TAG_WITH_CLASS_NAME ? "WallpaperController" : TAG_WM;
private WindowManagerService mService;
- private WallpaperCropUtils mWallpaperCropUtils = null;
private DisplayContent mDisplayContent;
// Larger index has higher z-order.
@@ -116,6 +116,10 @@ class WallpaperController {
private boolean mShouldOffsetWallpaperCenter;
+ // This is for WallpaperCropper, which has cropping logic for the default display only.
+ // TODO(b/400685784) make the WallpaperCropper operate on every display independently
+ private final WallpaperDefaultDisplayInfo mDefaultDisplayInfo;
+
private final ToBooleanFunction<WindowState> mFindWallpaperTargetFunction = w -> {
final ActivityRecord ar = w.mActivityRecord;
// The animating window can still be visible on screen if it is in transition, so we
@@ -198,12 +202,14 @@ class WallpaperController {
WallpaperController(WindowManagerService service, DisplayContent displayContent) {
mService = service;
mDisplayContent = displayContent;
+ WindowManager windowManager = service.mContext.getSystemService(WindowManager.class);
Resources resources = service.mContext.getResources();
mMinWallpaperScale =
resources.getFloat(com.android.internal.R.dimen.config_wallpaperMinScale);
mMaxWallpaperScale = resources.getFloat(R.dimen.config_wallpaperMaxScale);
mShouldOffsetWallpaperCenter = resources.getBoolean(
com.android.internal.R.bool.config_offsetWallpaperToCenterOfLargestDisplay);
+ mDefaultDisplayInfo = new WallpaperDefaultDisplayInfo(windowManager, resources);
}
void resetLargestDisplay(Display display) {
@@ -246,10 +252,6 @@ class WallpaperController {
return largestDisplaySize;
}
- void setWallpaperCropUtils(WallpaperCropUtils wallpaperCropUtils) {
- mWallpaperCropUtils = wallpaperCropUtils;
- }
-
WindowState getWallpaperTarget() {
return mWallpaperTarget;
}
@@ -352,16 +354,12 @@ class WallpaperController {
int offsetY;
if (multiCrop()) {
- if (mWallpaperCropUtils == null) {
- Slog.e(TAG, "Update wallpaper offsets before the system is ready. Aborting");
- return false;
- }
Point bitmapSize = new Point(
wallpaperWin.mRequestedWidth, wallpaperWin.mRequestedHeight);
SparseArray<Rect> cropHints = token.getCropHints();
wallpaperFrame = bitmapSize.x <= 0 || bitmapSize.y <= 0 ? wallpaperWin.getFrame()
- : mWallpaperCropUtils.getCrop(screenSize, bitmapSize, cropHints,
- wallpaperWin.isRtl());
+ : WallpaperCropper.getCrop(screenSize, mDefaultDisplayInfo, bitmapSize,
+ cropHints, wallpaperWin.isRtl());
int frameWidth = wallpaperFrame.width();
int frameHeight = wallpaperFrame.height();
float frameRatio = (float) frameWidth / frameHeight;
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 466ed7863c84..772a7fdfc684 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -2006,11 +2006,16 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
return getActivity(r -> !r.finishing, true /* traverseTopToBottom */);
}
- ActivityRecord getTopMostVisibleFreeformActivity() {
+ ActivityRecord getTopMostFreeformActivity() {
return getActivity(r -> r.isVisibleRequested() && r.inFreeformWindowingMode(),
true /* traverseTopToBottom */);
}
+ ActivityRecord getTopMostVisibleFreeformActivity() {
+ return getActivity(r -> r.isVisible() && r.inFreeformWindowingMode(),
+ true /* traverseTopToBottom */);
+ }
+
ActivityRecord getTopActivity(boolean includeFinishing, boolean includeOverlays) {
// Break down into 4 calls to avoid object creation due to capturing input params.
if (includeFinishing) {
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index 4b5a3a031931..5f2a2ad7f0eb 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -54,7 +54,6 @@ import android.window.ScreenCapture.ScreenshotHardwareBuffer;
import com.android.internal.policy.KeyInterceptionInfo;
import com.android.server.input.InputManagerService;
import com.android.server.policy.WindowManagerPolicy;
-import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils;
import com.android.server.wm.SensitiveContentPackages.PackageInfo;
import java.lang.annotation.Retention;
@@ -772,12 +771,6 @@ public abstract class WindowManagerInternal {
public abstract void setWallpaperCropHints(IBinder windowToken, SparseArray<Rect> cropHints);
/**
- * Transmits the {@link WallpaperCropUtils} instance to {@link WallpaperController}.
- * {@link WallpaperCropUtils} contains the helpers to properly position the wallpaper.
- */
- public abstract void setWallpaperCropUtils(WallpaperCropUtils wallpaperCropUtils);
-
- /**
* Returns {@code true} if a Window owned by {@code uid} has focus.
*/
public abstract boolean isUidFocused(int uid);
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 9fc0339c52a2..c078d67b6cc6 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -356,7 +356,6 @@ import com.android.server.policy.WindowManagerPolicy;
import com.android.server.policy.WindowManagerPolicy.ScreenOffListener;
import com.android.server.power.ShutdownThread;
import com.android.server.utils.PriorityDump;
-import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils;
import com.android.window.flags.Flags;
import dalvik.annotation.optimization.NeverCompile;
@@ -8100,12 +8099,6 @@ public class WindowManagerService extends IWindowManager.Stub
}
@Override
- public void setWallpaperCropUtils(WallpaperCropUtils wallpaperCropUtils) {
- mRoot.getDisplayContent(DEFAULT_DISPLAY).mWallpaperController
- .setWallpaperCropUtils(wallpaperCropUtils);
- }
-
- @Override
public boolean isUidFocused(int uid) {
synchronized (mGlobalLock) {
for (int i = mRoot.getChildCount() - 1; i >= 0; i--) {
@@ -9374,23 +9367,6 @@ public class WindowManagerService extends IWindowManager.Stub
return focusedActivity;
}
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- final TaskFragment adjacentTaskFragment = taskFragment.getAdjacentTaskFragment();
- final ActivityRecord adjacentTopActivity = adjacentTaskFragment.topRunningActivity();
- if (adjacentTopActivity == null) {
- // Return if no adjacent activity.
- return focusedActivity;
- }
-
- if (adjacentTopActivity.getLastWindowCreateTime()
- < focusedActivity.getLastWindowCreateTime()) {
- // Return if the current focus activity has more recently active window.
- return focusedActivity;
- }
-
- return adjacentTopActivity;
- }
-
// Find the adjacent activity with more recently active window.
final ActivityRecord[] mostRecentActiveActivity = { focusedActivity };
final long[] mostRecentActiveTime = { focusedActivity.getLastWindowCreateTime() };
@@ -9461,20 +9437,15 @@ public class WindowManagerService extends IWindowManager.Stub
// No adjacent window.
return false;
}
- final TaskFragment adjacentFragment;
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- if (fromFragment.getAdjacentTaskFragments().size() > 2) {
- throw new IllegalStateException("Not yet support 3+ adjacent for non-Task TFs");
- }
- final TaskFragment[] tmpAdjacent = new TaskFragment[1];
- fromFragment.forOtherAdjacentTaskFragments(adjacentTF -> {
- tmpAdjacent[0] = adjacentTF;
- return true;
- });
- adjacentFragment = tmpAdjacent[0];
- } else {
- adjacentFragment = fromFragment.getAdjacentTaskFragment();
+ if (fromFragment.getAdjacentTaskFragments().size() > 2) {
+ throw new IllegalStateException("Not yet support 3+ adjacent for non-Task TFs");
}
+ final TaskFragment[] tmpAdjacent = new TaskFragment[1];
+ fromFragment.forOtherAdjacentTaskFragments(adjacentTF -> {
+ tmpAdjacent[0] = adjacentTF;
+ return true;
+ });
+ final TaskFragment adjacentFragment = tmpAdjacent[0];
if (adjacentFragment.isIsolatedNav()) {
// Don't move the focus if the adjacent TF is isolated navigation.
return false;
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index ea1f35a130b0..a012ec137892 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -1671,13 +1671,9 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
}
if (!taskFragment.isAdjacentTo(secondaryTaskFragment)) {
// Only have lifecycle effect if the adjacent changed.
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- // Activity Embedding only set two TFs adjacent.
- taskFragment.setAdjacentTaskFragments(
- new TaskFragment.AdjacentSet(taskFragment, secondaryTaskFragment));
- } else {
- taskFragment.setAdjacentTaskFragment(secondaryTaskFragment);
- }
+ // Activity Embedding only set two TFs adjacent.
+ taskFragment.setAdjacentTaskFragments(
+ new TaskFragment.AdjacentSet(taskFragment, secondaryTaskFragment));
effects |= TRANSACT_EFFECTS_LIFECYCLE;
}
@@ -2220,30 +2216,6 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
}
private int setAdjacentRootsHierarchyOp(WindowContainerTransaction.HierarchyOp hop) {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- final WindowContainer wc1 = WindowContainer.fromBinder(hop.getContainer());
- if (wc1 == null || !wc1.isAttached()) {
- Slog.e(TAG, "Attempt to operate on unknown or detached container: " + wc1);
- return TRANSACT_EFFECTS_NONE;
- }
- final TaskFragment root1 = wc1.asTaskFragment();
- final WindowContainer wc2 = WindowContainer.fromBinder(hop.getAdjacentRoot());
- if (wc2 == null || !wc2.isAttached()) {
- Slog.e(TAG, "Attempt to operate on unknown or detached container: " + wc2);
- return TRANSACT_EFFECTS_NONE;
- }
- final TaskFragment root2 = wc2.asTaskFragment();
- if (!root1.mCreatedByOrganizer || !root2.mCreatedByOrganizer) {
- throw new IllegalArgumentException("setAdjacentRootsHierarchyOp: Not created by"
- + " organizer root1=" + root1 + " root2=" + root2);
- }
- if (root1.isAdjacentTo(root2)) {
- return TRANSACT_EFFECTS_NONE;
- }
- root1.setAdjacentTaskFragment(root2);
- return TRANSACT_EFFECTS_LIFECYCLE;
- }
-
final IBinder[] containers = hop.getContainers();
final ArraySet<TaskFragment> adjacentRoots = new ArraySet<>();
for (IBinder container : containers) {
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index 95776088aad8..66d04df8095b 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -193,10 +193,6 @@ cc_defaults {
"android.hardware.tv.input@1.0",
"android.hardware.tv.input-V2-ndk",
"android.hardware.vibrator-V3-ndk",
- "android.hardware.vibrator@1.0",
- "android.hardware.vibrator@1.1",
- "android.hardware.vibrator@1.2",
- "android.hardware.vibrator@1.3",
"android.hardware.vr@1.0",
"android.hidl.token@1.0-utils",
"android.frameworks.schedulerservice@1.0",
diff --git a/services/core/jni/com_android_server_display_DisplayControl.cpp b/services/core/jni/com_android_server_display_DisplayControl.cpp
index aeae13d83809..c674f9037aa4 100644
--- a/services/core/jni/com_android_server_display_DisplayControl.cpp
+++ b/services/core/jni/com_android_server_display_DisplayControl.cpp
@@ -24,12 +24,13 @@
namespace android {
static jobject nativeCreateVirtualDisplay(JNIEnv* env, jclass clazz, jstring nameObj,
- jboolean secure, jstring uniqueIdStr,
- jfloat requestedRefreshRate) {
+ jboolean secure, jboolean optimizeForPower,
+ jstring uniqueIdStr, jfloat requestedRefreshRate) {
const ScopedUtfChars name(env, nameObj);
const ScopedUtfChars uniqueId(env, uniqueIdStr);
sp<IBinder> token(SurfaceComposerClient::createVirtualDisplay(std::string(name.c_str()),
bool(secure),
+ bool(optimizeForPower),
std::string(uniqueId.c_str()),
requestedRefreshRate));
return javaObjectForIBinder(env, token);
@@ -182,7 +183,7 @@ static jobject nativeGetPhysicalDisplayToken(JNIEnv* env, jclass clazz, jlong ph
static const JNINativeMethod sDisplayMethods[] = {
// clang-format off
- {"nativeCreateVirtualDisplay", "(Ljava/lang/String;ZLjava/lang/String;F)Landroid/os/IBinder;",
+ {"nativeCreateVirtualDisplay", "(Ljava/lang/String;ZZLjava/lang/String;F)Landroid/os/IBinder;",
(void*)nativeCreateVirtualDisplay },
{"nativeDestroyVirtualDisplay", "(Landroid/os/IBinder;)V",
(void*)nativeDestroyVirtualDisplay },
diff --git a/services/core/jni/com_android_server_vibrator_VibratorController.cpp b/services/core/jni/com_android_server_vibrator_VibratorController.cpp
index 534dbb1f6cf1..11dbbdfb850e 100644
--- a/services/core/jni/com_android_server_vibrator_VibratorController.cpp
+++ b/services/core/jni/com_android_server_vibrator_VibratorController.cpp
@@ -19,7 +19,6 @@
#include <aidl/android/hardware/vibrator/IVibrator.h>
#include <android/binder_parcel.h>
#include <android/binder_parcel_jni.h>
-#include <android/hardware/vibrator/1.3/IVibrator.h>
#include <android/persistable_bundle_aidl.h>
#include <android_os_vibrator.h>
#include <nativehelper/JNIHelp.h>
@@ -32,8 +31,6 @@
#include "core_jni_helpers.h"
#include "jni.h"
-namespace V1_0 = android::hardware::vibrator::V1_0;
-namespace V1_3 = android::hardware::vibrator::V1_3;
namespace Aidl = aidl::android::hardware::vibrator;
using aidl::android::os::PersistableBundle;
@@ -80,31 +77,6 @@ static struct {
jfieldID timeMillis;
} sPwlePointClassInfo;
-static_assert(static_cast<uint8_t>(V1_0::EffectStrength::LIGHT) ==
- static_cast<uint8_t>(Aidl::EffectStrength::LIGHT));
-static_assert(static_cast<uint8_t>(V1_0::EffectStrength::MEDIUM) ==
- static_cast<uint8_t>(Aidl::EffectStrength::MEDIUM));
-static_assert(static_cast<uint8_t>(V1_0::EffectStrength::STRONG) ==
- static_cast<uint8_t>(Aidl::EffectStrength::STRONG));
-
-static_assert(static_cast<uint8_t>(V1_3::Effect::CLICK) ==
- static_cast<uint8_t>(Aidl::Effect::CLICK));
-static_assert(static_cast<uint8_t>(V1_3::Effect::DOUBLE_CLICK) ==
- static_cast<uint8_t>(Aidl::Effect::DOUBLE_CLICK));
-static_assert(static_cast<uint8_t>(V1_3::Effect::TICK) == static_cast<uint8_t>(Aidl::Effect::TICK));
-static_assert(static_cast<uint8_t>(V1_3::Effect::THUD) == static_cast<uint8_t>(Aidl::Effect::THUD));
-static_assert(static_cast<uint8_t>(V1_3::Effect::POP) == static_cast<uint8_t>(Aidl::Effect::POP));
-static_assert(static_cast<uint8_t>(V1_3::Effect::HEAVY_CLICK) ==
- static_cast<uint8_t>(Aidl::Effect::HEAVY_CLICK));
-static_assert(static_cast<uint8_t>(V1_3::Effect::RINGTONE_1) ==
- static_cast<uint8_t>(Aidl::Effect::RINGTONE_1));
-static_assert(static_cast<uint8_t>(V1_3::Effect::RINGTONE_2) ==
- static_cast<uint8_t>(Aidl::Effect::RINGTONE_2));
-static_assert(static_cast<uint8_t>(V1_3::Effect::RINGTONE_15) ==
- static_cast<uint8_t>(Aidl::Effect::RINGTONE_15));
-static_assert(static_cast<uint8_t>(V1_3::Effect::TEXTURE_TICK) ==
- static_cast<uint8_t>(Aidl::Effect::TEXTURE_TICK));
-
static std::shared_ptr<vibrator::HalController> findVibrator(int32_t vibratorId) {
vibrator::ManagerHalController* manager =
android_server_vibrator_VibratorManagerService_getManager();
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 215d6ca964eb..51ed6bb2aa40 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -245,6 +245,7 @@ import static android.app.admin.ProvisioningException.ERROR_REMOVE_NON_REQUIRED_
import static android.app.admin.ProvisioningException.ERROR_SETTING_PROFILE_OWNER_FAILED;
import static android.app.admin.ProvisioningException.ERROR_SET_DEVICE_OWNER_FAILED;
import static android.app.admin.ProvisioningException.ERROR_STARTING_PROFILE_FAILED;
+import static android.content.Context.RECEIVER_NOT_EXPORTED;
import static android.content.Intent.ACTION_MANAGED_PROFILE_AVAILABLE;
import static android.content.Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
@@ -486,6 +487,7 @@ import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.telephony.data.ApnSetting;
+import android.telephony.euicc.EuiccManager;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.ArrayMap;
@@ -643,6 +645,14 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
static final String ACTION_PROFILE_OFF_DEADLINE =
"com.android.server.ACTION_PROFILE_OFF_DEADLINE";
+ /** Broadcast action invoked when a managed eSIM is removed while deleting work profile. */
+ private static final String ACTION_ESIM_REMOVED_WITH_MANAGED_PROFILE =
+ "com.android.server.ACTION_ESIM_REMOVED_WITH_MANAGED_PROFILE";
+
+ /** Extra for the subscription ID of the managed eSIM removed while deleting work profile. */
+ private static final String EXTRA_REMOVED_ESIM_SUBSCRIPTION_ID =
+ "com.android.server.EXTRA_ESIM_REMOVED_WITH_MANAGED_PROFILE_SUBSCRIPTION_ID";
+
private static final String CALLED_FROM_PARENT = "calledFromParent";
private static final String NOT_CALLED_FROM_PARENT = "notCalledFromParent";
@@ -1266,6 +1276,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
removeCredentialManagementApp(intent.getData().getSchemeSpecificPart());
} else if (Intent.ACTION_MANAGED_PROFILE_ADDED.equals(action)) {
clearWipeProfileNotification();
+ } else if (Intent.ACTION_MANAGED_PROFILE_REMOVED.equals(action)) {
+ removeManagedEmbeddedSubscriptionsForUser(userHandle);
} else if (Intent.ACTION_DATE_CHANGED.equals(action)
|| Intent.ACTION_TIME_CHANGED.equals(action)) {
// Update freeze period record when clock naturally progresses to the next day
@@ -1298,6 +1310,13 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
triggerPolicyComplianceCheckIfNeeded(userHandle, suspended);
} else if (LOGIN_ACCOUNTS_CHANGED_ACTION.equals(action)) {
calculateHasIncompatibleAccounts();
+ } else if (ACTION_ESIM_REMOVED_WITH_MANAGED_PROFILE.equals(action)) {
+ int removedSubscriptionId = intent.getIntExtra(EXTRA_REMOVED_ESIM_SUBSCRIPTION_ID,
+ -1);
+ Slogf.i(LOG_TAG,
+ "Deleted subscription with ID %d because owning managed profile was "
+ + "removed",
+ removedSubscriptionId);
}
}
@@ -2219,9 +2238,14 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, filter, null, mHandler);
filter = new IntentFilter();
filter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED);
+ filter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED);
filter.addAction(Intent.ACTION_TIME_CHANGED);
filter.addAction(Intent.ACTION_DATE_CHANGED);
mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, filter, null, mHandler);
+ filter = new IntentFilter();
+ filter.addAction(ACTION_ESIM_REMOVED_WITH_MANAGED_PROFILE);
+ mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, filter, null,
+ mHandler, RECEIVER_NOT_EXPORTED);
LocalServices.addService(DevicePolicyManagerInternal.class, mLocalService);
@@ -3782,9 +3806,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
// Update user switcher message to activity manager.
ActivityManagerInternal activityManagerInternal =
mInjector.getActivityManagerInternal();
- activityManagerInternal.setSwitchingFromSystemUserMessage(
+ int deviceOwnerUserId = UserHandle.getUserId(deviceOwner.getUid());
+ activityManagerInternal.setSwitchingFromUserMessage(deviceOwnerUserId,
deviceOwner.startUserSessionMessage);
- activityManagerInternal.setSwitchingToSystemUserMessage(
+ activityManagerInternal.setSwitchingToUserMessage(deviceOwnerUserId,
deviceOwner.endUserSessionMessage);
}
@@ -3970,6 +3995,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
deletedUsers.remove(userInfo.id);
}
for (Integer userId : deletedUsers) {
+ removeManagedEmbeddedSubscriptionsForUser(userId);
removeUserData(userId);
mDevicePolicyEngine.handleUserRemoved(userId);
}
@@ -8099,6 +8125,45 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
mInjector.getNotificationManager().cancel(SystemMessage.NOTE_PROFILE_WIPED);
}
+ /**
+ * Remove eSIM subscriptions that are managed by any of the admin packages of the given
+ * userHandle.
+ */
+ private void removeManagedEmbeddedSubscriptionsForUser(int userHandle) {
+ if (!Flags.removeManagedEsimOnWorkProfileDeletion()) {
+ return;
+ }
+
+ Slogf.i(LOG_TAG,
+ "Managed profile with ID=%d deleted: going to remove managed embedded "
+ + "subscriptions", userHandle);
+ String profileOwnerPackage = mOwners.getProfileOwnerPackage(userHandle);
+ if (profileOwnerPackage == null) {
+ Slogf.wtf(LOG_TAG, "Profile owner package for managed profile is null");
+ return;
+ }
+ IntArray managedSubscriptionIds = getSubscriptionIdsInternal(profileOwnerPackage);
+ deleteEmbeddedSubscriptions(managedSubscriptionIds);
+ }
+
+ private void deleteEmbeddedSubscriptions(IntArray subscriptionIds) {
+ EuiccManager euiccManager = mContext.getSystemService(EuiccManager.class);
+ for (int subscriptionId : subscriptionIds.toArray()) {
+ Slogf.i(LOG_TAG, "Deleting embedded subscription with ID %d", subscriptionId);
+ euiccManager.deleteSubscription(subscriptionId,
+ createCallbackPendingIntentForRemovingManagedSubscription(
+ subscriptionId));
+ }
+ }
+
+ private PendingIntent createCallbackPendingIntentForRemovingManagedSubscription(
+ Integer subscriptionId) {
+ Intent intent = new Intent(ACTION_ESIM_REMOVED_WITH_MANAGED_PROFILE);
+ intent.putExtra(EXTRA_REMOVED_ESIM_SUBSCRIPTION_ID, subscriptionId);
+ return PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_IMMUTABLE);
+ }
+
+
@Override
public void setFactoryResetProtectionPolicy(ComponentName who, String callerPackageName,
@Nullable FactoryResetProtectionPolicy policy) {
@@ -19652,7 +19717,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
}
mInjector.getActivityManagerInternal()
- .setSwitchingFromSystemUserMessage(startUserSessionMessageString);
+ .setSwitchingFromUserMessage(caller.getUserId(), startUserSessionMessageString);
}
@Override
@@ -19677,7 +19742,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
}
mInjector.getActivityManagerInternal()
- .setSwitchingToSystemUserMessage(endUserSessionMessageString);
+ .setSwitchingToUserMessage(caller.getUserId(), endUserSessionMessageString);
}
@Override
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
index 8e749529978e..de78271acddc 100644
--- a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
@@ -20,6 +20,7 @@ import static android.hardware.SensorManager.SENSOR_DELAY_FASTEST;
import static android.hardware.devicestate.DeviceState.PROPERTY_POLICY_UNSUPPORTED_WHEN_POWER_SAVE_MODE;
import static android.hardware.devicestate.DeviceState.PROPERTY_POLICY_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL;
import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER;
+import static android.os.Trace.TRACE_TAG_SYSTEM_SERVER;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.TYPE_EXTERNAL;
@@ -324,6 +325,11 @@ public final class FoldableDeviceStateProvider implements DeviceStateProvider,
}
if (newState != INVALID_DEVICE_STATE_IDENTIFIER && newState != mLastReportedState) {
+ if (Trace.isTagEnabled(TRACE_TAG_SYSTEM_SERVER)) {
+ Trace.instant(TRACE_TAG_SYSTEM_SERVER,
+ "[Device state changed] Last hinge sensor event timestamp: "
+ + mLastHingeAngleSensorEvent.timestamp);
+ }
mLastReportedState = newState;
stateToReport = newState;
}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 860b6fb1dcd1..788b3b883160 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -366,6 +366,8 @@ public final class SystemServer implements Dumpable {
"com.android.clockwork.time.WearTimeService";
private static final String WEAR_SETTINGS_SERVICE_CLASS =
"com.android.clockwork.settings.WearSettingsService";
+ private static final String WEAR_GESTURE_SERVICE_CLASS =
+ "com.android.clockwork.gesture.WearGestureService";
private static final String WRIST_ORIENTATION_SERVICE_CLASS =
"com.android.clockwork.wristorientation.WristOrientationService";
private static final String IOT_SERVICE_CLASS =
@@ -2844,6 +2846,13 @@ public final class SystemServer implements Dumpable {
mSystemServiceManager.startService(WRIST_ORIENTATION_SERVICE_CLASS);
t.traceEnd();
}
+
+ if (android.server.Flags.wearGestureApi()
+ && SystemProperties.getBoolean("config.enable_gesture_api", false)) {
+ t.traceBegin("StartWearGestureService");
+ mSystemServiceManager.startService(WEAR_GESTURE_SERVICE_CLASS);
+ t.traceEnd();
+ }
}
if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_SLICES_DISABLED)) {
diff --git a/services/java/com/android/server/flags.aconfig b/services/java/com/android/server/flags.aconfig
index 86ccd878de7c..7a6bd75e5893 100644
--- a/services/java/com/android/server/flags.aconfig
+++ b/services/java/com/android/server/flags.aconfig
@@ -65,4 +65,12 @@ flag {
namespace: "package_manager_service"
description: "Remove AppIntegrityManagerService"
bug: "364200023"
+}
+
+flag {
+ name: "wear_gesture_api"
+ namespace: "wear_frameworks"
+ description: "Whether the Wear Gesture API is available."
+ bug: "396154116"
+ is_exported: true
} \ No newline at end of file
diff --git a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java
index 2dd16f68dc56..80a3a8788d80 100644
--- a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java
+++ b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java
@@ -109,6 +109,7 @@ import com.android.server.EventLogTags;
import com.android.server.LocalServices;
import com.android.server.backup.BackupAgentConnectionManager;
import com.android.server.backup.BackupRestoreTask;
+import com.android.server.backup.BackupRestoreTask.CancellationReason;
import com.android.server.backup.BackupWakeLock;
import com.android.server.backup.DataChangedJournal;
import com.android.server.backup.KeyValueBackupJob;
@@ -2412,7 +2413,7 @@ public class KeyValueBackupTaskTest {
KeyValueBackupTask task = spy(createKeyValueBackupTask(transportMock, PACKAGE_1));
doNothing().when(task).waitCancel();
- task.handleCancel(true);
+ task.handleCancel(CancellationReason.EXTERNAL);
InOrder inOrder = inOrder(task);
inOrder.verify(task).markCancel();
@@ -2420,12 +2421,14 @@ public class KeyValueBackupTaskTest {
}
@Test
- public void testHandleCancel_whenCancelAllFalse_throws() throws Exception {
+ public void testHandleCancel_timeout_throws() throws Exception {
TransportMock transportMock = setUpInitializedTransport(mTransport);
setUpAgentWithData(PACKAGE_1);
KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1);
- expectThrows(IllegalArgumentException.class, () -> task.handleCancel(false));
+ expectThrows(
+ IllegalArgumentException.class,
+ () -> task.handleCancel(CancellationReason.TIMEOUT));
}
/** Do not update backup token if no data was moved. */
diff --git a/services/tests/DynamicInstrumentationManagerServiceTests/OWNERS b/services/tests/DynamicInstrumentationManagerServiceTests/OWNERS
new file mode 100644
index 000000000000..2522426d93f8
--- /dev/null
+++ b/services/tests/DynamicInstrumentationManagerServiceTests/OWNERS
@@ -0,0 +1 @@
+include platform/packages/modules/UprobeStats:/OWNERS \ No newline at end of file
diff --git a/services/tests/InputMethodSystemServerTests/Android.bp b/services/tests/InputMethodSystemServerTests/Android.bp
index ae9a34efc222..c1d8382fcd0e 100644
--- a/services/tests/InputMethodSystemServerTests/Android.bp
+++ b/services/tests/InputMethodSystemServerTests/Android.bp
@@ -73,10 +73,7 @@ android_ravenwood_test {
static_libs: [
"androidx.annotation_annotation",
"androidx.test.rules",
- "framework",
- "ravenwood-runtime",
- "ravenwood-utils",
- "services",
+ "services.core",
],
libs: [
"android.test.base.stubs.system",
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/util/IgnoreableExpect.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/util/IgnoreableExpect.kt
index afb18f5be669..5c9ba401bf88 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/util/IgnoreableExpect.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/util/IgnoreableExpect.kt
@@ -32,7 +32,7 @@ internal class IgnoreableExpect : TestRule {
private var ignore = false
- override fun apply(base: Statement?, description: Description?): Statement {
+ override fun apply(base: Statement, description: Description): Statement {
return object : Statement() {
override fun evaluate() {
ignore = false
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 1f8ccde98d35..2770caa8aaa4 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -324,7 +324,8 @@ public class DisplayManagerServiceTest {
return new VirtualDisplayAdapter(syncRoot, context, handler, displayAdapterListener,
new VirtualDisplayAdapter.SurfaceControlDisplayFactory() {
@Override
- public IBinder createDisplay(String name, boolean secure, String uniqueId,
+ public IBinder createDisplay(String name, boolean secure,
+ boolean optimizeForPower, String uniqueId,
float requestedRefreshRate) {
return mMockDisplayToken;
}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt b/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt
index 206c90d0481a..6dc7361e5366 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt
@@ -47,7 +47,7 @@ class DisplayTopologyCoordinatorTest {
private val mockTopology = mock<DisplayTopology>()
private val mockTopologyCopy = mock<DisplayTopology>()
private val mockTopologyGraph = mock<DisplayTopologyGraph>()
- private val mockIsExtendedDisplayEnabled = mock<() -> Boolean>()
+ private val mockIsExtendedDisplayAllowed = mock<() -> Boolean>()
private val mockTopologySavedCallback = mock<() -> Unit>()
private val mockTopologyChangedCallback =
mock<(android.util.Pair<DisplayTopology, DisplayTopologyGraph>) -> Unit>()
@@ -73,10 +73,10 @@ class DisplayTopologyCoordinatorTest {
) =
mockTopologyStore
}
- whenever(mockIsExtendedDisplayEnabled()).thenReturn(true)
+ whenever(mockIsExtendedDisplayAllowed()).thenReturn(true)
whenever(mockTopology.copy()).thenReturn(mockTopologyCopy)
whenever(mockTopologyCopy.getGraph(any())).thenReturn(mockTopologyGraph)
- coordinator = DisplayTopologyCoordinator(injector, mockIsExtendedDisplayEnabled,
+ coordinator = DisplayTopologyCoordinator(injector, mockIsExtendedDisplayAllowed,
mockTopologyChangedCallback, topologyChangeExecutor, DisplayManagerService.SyncRoot(),
mockTopologySavedCallback)
}
@@ -195,7 +195,7 @@ class DisplayTopologyCoordinatorTest {
@Test
fun addDisplay_external_extendedDisplaysDisabled() {
- whenever(mockIsExtendedDisplayEnabled()).thenReturn(false)
+ whenever(mockIsExtendedDisplayAllowed()).thenReturn(false)
for (displayInfo in displayInfos) {
coordinator.onDisplayAdded(displayInfo)
@@ -208,7 +208,7 @@ class DisplayTopologyCoordinatorTest {
@Test
fun addDisplay_overlay_extendedDisplaysDisabled() {
displayInfos[0].type = Display.TYPE_OVERLAY
- whenever(mockIsExtendedDisplayEnabled()).thenReturn(false)
+ whenever(mockIsExtendedDisplayAllowed()).thenReturn(false)
for (displayInfo in displayInfos) {
coordinator.onDisplayAdded(displayInfo)
@@ -314,7 +314,7 @@ class DisplayTopologyCoordinatorTest {
@Test
fun updateDisplay_external_extendedDisplaysDisabled() {
- whenever(mockIsExtendedDisplayEnabled()).thenReturn(false)
+ whenever(mockIsExtendedDisplayAllowed()).thenReturn(false)
for (displayInfo in displayInfos) {
coordinator.onDisplayChanged(displayInfo)
@@ -328,7 +328,7 @@ class DisplayTopologyCoordinatorTest {
@Test
fun updateDisplay_overlay_extendedDisplaysDisabled() {
displayInfos[0].type = Display.TYPE_OVERLAY
- whenever(mockIsExtendedDisplayEnabled()).thenReturn(false)
+ whenever(mockIsExtendedDisplayAllowed()).thenReturn(false)
coordinator.onDisplayChanged(displayInfos[0])
diff --git a/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java
index 0bef3b89547f..10bea7d331cd 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java
@@ -416,7 +416,7 @@ public class VirtualDisplayAdapterTest {
final String uniqueId = "uniqueId";
final IBinder displayToken = new Binder();
when(mMockSufaceControlDisplayFactory.createDisplay(
- any(), anyBoolean(), eq(uniqueId), anyFloat()))
+ any(), anyBoolean(), anyBoolean(), eq(uniqueId), anyFloat()))
.thenReturn(displayToken);
// The display needs to be public, otherwise it will be considered never blank.
@@ -456,6 +456,49 @@ public class VirtualDisplayAdapterTest {
verify(mMockCallback).onPaused();
}
+ @EnableFlags(
+ android.companion.virtualdevice.flags.Flags.FLAG_CORRECT_VIRTUAL_DISPLAY_POWER_STATE)
+ @Test
+ public void createVirtualDisplayLocked_neverBlank_optimizesForPower() {
+ final String uniqueId = "uniqueId";
+ final IBinder displayToken = new Binder();
+ final String name = "name";
+ when(mVirtualDisplayConfigMock.getName()).thenReturn(name);
+ when(mMockSufaceControlDisplayFactory.createDisplay(
+ any(), anyBoolean(), anyBoolean(), eq(uniqueId), anyFloat()))
+ .thenReturn(displayToken);
+
+ // Use a private display to cause the display to be never blank.
+ mAdapter.createVirtualDisplayLocked(mMockCallback,
+ /* projection= */ null, /* ownerUid= */ 10, /* packageName= */ "testpackage",
+ uniqueId, /* surface= */ mSurfaceMock, 0, mVirtualDisplayConfigMock);
+
+ verify(mMockSufaceControlDisplayFactory).createDisplay(eq(name), eq(false), eq(true),
+ eq(uniqueId), anyFloat());
+ }
+
+ @EnableFlags(
+ android.companion.virtualdevice.flags.Flags.FLAG_CORRECT_VIRTUAL_DISPLAY_POWER_STATE)
+ @Test
+ public void createVirtualDisplayLocked_blankable_optimizesForPerformance() {
+ final String uniqueId = "uniqueId";
+ final IBinder displayToken = new Binder();
+ final String name = "name";
+ when(mVirtualDisplayConfigMock.getName()).thenReturn(name);
+ when(mMockSufaceControlDisplayFactory.createDisplay(
+ any(), anyBoolean(), anyBoolean(), eq(uniqueId), anyFloat()))
+ .thenReturn(displayToken);
+
+ // Use a public display to cause the display to be blankable
+ mAdapter.createVirtualDisplayLocked(mMockCallback,
+ /* projection= */ null, /* ownerUid= */ 10, /* packageName= */ "testpackage",
+ uniqueId, /* surface= */ mSurfaceMock, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,
+ mVirtualDisplayConfigMock);
+
+ verify(mMockSufaceControlDisplayFactory).createDisplay(eq(name), eq(false), eq(false),
+ eq(uniqueId), anyFloat());
+ }
+
private IVirtualDisplayCallback createCallback() {
return new IVirtualDisplayCallback.Stub() {
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
index 5d8f57866f7d..e094111c327a 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -746,6 +746,36 @@ public class MockingOomAdjusterTests {
@SuppressWarnings("GuardedBy")
@Test
@EnableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY)
+ public void testUpdateOomAdjFreezeState_bindingWithAllowFreeze() {
+ ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
+ MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
+ WindowProcessController wpc = app.getWindowProcessController();
+ doReturn(true).when(wpc).hasVisibleActivities();
+
+ final ProcessRecord app2 = spy(makeDefaultProcessRecord(MOCKAPP2_PID, MOCKAPP2_UID,
+ MOCKAPP2_PROCESSNAME, MOCKAPP2_PACKAGENAME, false));
+
+ // App with a visible activity binds to app2 without any special flag.
+ bindService(app2, app, null, null, 0, mock(IBinder.class));
+
+ final ProcessRecord app3 = spy(makeDefaultProcessRecord(MOCKAPP3_PID, MOCKAPP3_UID,
+ MOCKAPP3_PROCESSNAME, MOCKAPP3_PACKAGENAME, false));
+
+ // App with a visible activity binds to app3 with ALLOW_FREEZE.
+ bindService(app3, app, null, null, Context.BIND_ALLOW_FREEZE, mock(IBinder.class));
+
+ setProcessesToLru(app, app2, app3);
+
+ updateOomAdj(app);
+
+ assertCpuTime(app);
+ assertCpuTime(app2);
+ assertNoCpuTime(app3);
+ }
+
+ @SuppressWarnings("GuardedBy")
+ @Test
+ @EnableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY)
@DisableFlags(Flags.FLAG_PROTOTYPE_AGGRESSIVE_FREEZING)
public void testUpdateOomAdjFreezeState_bindingFromFgs() {
final ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/BackupAgentConnectionManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/BackupAgentConnectionManagerTest.java
index 8aaa72339c5b..33bd95ec9f5b 100644
--- a/services/tests/mockingservicestests/src/com/android/server/backup/BackupAgentConnectionManagerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/backup/BackupAgentConnectionManagerTest.java
@@ -51,6 +51,7 @@ import android.platform.test.flag.junit.SetFlagsRule;
import androidx.test.runner.AndroidJUnit4;
import com.android.server.LocalServices;
+import com.android.server.backup.BackupRestoreTask.CancellationReason;
import com.android.server.backup.internal.LifecycleOperationStorage;
import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
@@ -368,9 +369,12 @@ public class BackupAgentConnectionManagerTest {
mConnectionManager.agentDisconnected(TEST_PACKAGE);
mTestThread.join();
- verify(mUserBackupManagerService).handleCancel(eq(123), eq(true));
- verify(mUserBackupManagerService).handleCancel(eq(456), eq(true));
- verify(mUserBackupManagerService).handleCancel(eq(789), eq(true));
+ verify(mUserBackupManagerService)
+ .handleCancel(eq(123), eq(CancellationReason.AGENT_DISCONNECTED));
+ verify(mUserBackupManagerService)
+ .handleCancel(eq(456), eq(CancellationReason.AGENT_DISCONNECTED));
+ verify(mUserBackupManagerService)
+ .handleCancel(eq(789), eq(CancellationReason.AGENT_DISCONNECTED));
}
@Test
diff --git a/services/tests/mockingservicestests/src/com/android/server/power/ScreenUndimDetectorTest.java b/services/tests/mockingservicestests/src/com/android/server/power/ScreenUndimDetectorTest.java
index 8ce05e2fa115..c9f86b04be22 100644
--- a/services/tests/mockingservicestests/src/com/android/server/power/ScreenUndimDetectorTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/power/ScreenUndimDetectorTest.java
@@ -23,7 +23,6 @@ import static android.hardware.display.DisplayManagerInternal.DisplayPowerReques
import static android.provider.DeviceConfig.NAMESPACE_ATTENTION_MANAGER_SERVICE;
import static android.view.Display.DEFAULT_DISPLAY_GROUP;
-import static com.android.server.power.ScreenUndimDetector.DEFAULT_MAX_DURATION_BETWEEN_UNDIMS_MILLIS;
import static com.android.server.power.ScreenUndimDetector.KEY_KEEP_SCREEN_ON_ENABLED;
import static com.android.server.power.ScreenUndimDetector.KEY_MAX_DURATION_BETWEEN_UNDIMS_MILLIS;
import static com.android.server.power.ScreenUndimDetector.KEY_UNDIMS_REQUIRED;
@@ -49,6 +48,7 @@ import org.junit.runners.JUnit4;
import java.util.Arrays;
import java.util.List;
+import java.util.concurrent.TimeUnit;
/**
* Tests for {@link com.android.server.power.ScreenUndimDetector}
@@ -61,7 +61,8 @@ public class ScreenUndimDetectorTest {
POLICY_DIM,
POLICY_BRIGHT);
private static final int OTHER_DISPLAY_GROUP = DEFAULT_DISPLAY_GROUP + 1;
-
+ private static final long DEFAULT_MAX_DURATION_BETWEEN_UNDIMS_MILLIS =
+ TimeUnit.MINUTES.toMillis(5);
@ClassRule
public static final TestableContext sContext = new TestableContext(
InstrumentationRegistry.getInstrumentation().getTargetContext(), null);
@@ -88,7 +89,8 @@ public class ScreenUndimDetectorTest {
@Before
public void setup() {
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
-
+ DeviceConfig.setProperty(NAMESPACE_ATTENTION_MANAGER_SERVICE,
+ KEY_KEEP_SCREEN_ON_ENABLED, Boolean.TRUE.toString(), false /*makeDefault*/);
DeviceConfig.setProperty(NAMESPACE_ATTENTION_MANAGER_SERVICE,
KEY_UNDIMS_REQUIRED,
Integer.toString(1), false /*makeDefault*/);
@@ -108,10 +110,10 @@ public class ScreenUndimDetectorTest {
@Test
public void recordScreenPolicy_disabledByFlag_noop() {
+ setup();
DeviceConfig.setProperty(NAMESPACE_ATTENTION_MANAGER_SERVICE,
KEY_KEEP_SCREEN_ON_ENABLED, Boolean.FALSE.toString(), false /*makeDefault*/);
mScreenUndimDetector.readValuesFromDeviceConfig();
-
mScreenUndimDetector.recordScreenPolicy(DEFAULT_DISPLAY_GROUP, POLICY_DIM);
mScreenUndimDetector.recordScreenPolicy(DEFAULT_DISPLAY_GROUP, POLICY_BRIGHT);
diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java
index 49c37f163ff2..241ffdc19ce4 100644
--- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java
@@ -36,24 +36,29 @@ import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
+import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.platform.test.annotations.Presubmit;
import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
import android.view.Display;
import android.view.DisplayInfo;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
import androidx.test.runner.AndroidJUnit4;
import com.android.dx.mockito.inline.extended.ExtendedMockito;
import com.android.dx.mockito.inline.extended.StaticMockitoSession;
+import com.android.internal.R;
import org.junit.AfterClass;
import org.junit.Before;
@@ -70,10 +75,11 @@ import java.io.File;
import java.io.IOException;
import java.util.Comparator;
import java.util.List;
+import java.util.Set;
/**
* Unit tests for the most important helpers of {@link WallpaperCropper}, in particular
- * {@link WallpaperCropper#getCrop(Point, Point, SparseArray, boolean)}.
+ * {@link WallpaperCropper#getCrop(Point, WallpaperDefaultDisplayInfo, Point, SparseArray, boolean)}.
*/
@Presubmit
@RunWith(AndroidJUnit4.class)
@@ -83,6 +89,12 @@ public class WallpaperCropperTest {
@Mock
private WallpaperDisplayHelper mWallpaperDisplayHelper;
+
+ @Mock
+ private WindowManager mWindowManager;
+
+ @Mock
+ private Resources mResources;
private WallpaperCropper mWallpaperCropper;
private static final Point PORTRAIT_ONE = new Point(500, 800);
@@ -175,14 +187,21 @@ public class WallpaperCropperTest {
return tempDir;
}
- private void setUpWithDisplays(List<Point> displaySizes) {
+ private WallpaperDefaultDisplayInfo setUpWithDisplays(List<Point> displaySizes) {
mDisplaySizes = new SparseArray<>();
displaySizes.forEach(size -> {
mDisplaySizes.put(getOrientation(size), size);
Point rotated = new Point(size.y, size.x);
mDisplaySizes.put(getOrientation(rotated), rotated);
});
+ Set<WindowMetrics> windowMetrics = new ArraySet<>();
+ for (Point displaySize : displaySizes) {
+ windowMetrics.add(
+ new WindowMetrics(new Rect(0, 0, displaySize.x, displaySize.y),
+ new WindowInsets.Builder().build()));
+ }
when(mWallpaperDisplayHelper.getDefaultDisplaySizes()).thenReturn(mDisplaySizes);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt())).thenReturn(windowMetrics);
if (displaySizes.size() == 2) {
Point largestDisplay = displaySizes.stream().max(
Comparator.comparingInt(p -> p.x * p.y)).get();
@@ -192,11 +211,16 @@ public class WallpaperCropperTest {
mFolded = getOrientation(smallestDisplay);
mUnfoldedRotated = getRotatedOrientation(mUnfolded);
mFoldedRotated = getRotatedOrientation(mFolded);
+ // foldable
+ doReturn(new int[]{0}).when(mResources).getIntArray(R.array.config_foldedDeviceStates);
+ } else {
+ // no foldable
+ doReturn(new int[]{}).when(mResources).getIntArray(R.array.config_foldedDeviceStates);
}
- doAnswer(invocation -> getFoldedOrientation(invocation.getArgument(0)))
- .when(mWallpaperDisplayHelper).getFoldedOrientation(anyInt());
- doAnswer(invocation -> getUnfoldedOrientation(invocation.getArgument(0)))
- .when(mWallpaperDisplayHelper).getUnfoldedOrientation(anyInt());
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+ when(mWallpaperDisplayHelper.getDefaultDisplayInfo()).thenReturn(defaultDisplayInfo);
+ return defaultDisplayInfo;
}
private int getFoldedOrientation(int orientation) {
@@ -435,7 +459,7 @@ public class WallpaperCropperTest {
*/
@Test
public void testGetCrop_noSuggestedCrops() {
- setUpWithDisplays(STANDARD_DISPLAY);
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(STANDARD_DISPLAY);
Point bitmapSize = new Point(800, 1000);
Rect bitmapRect = new Rect(0, 0, bitmapSize.x, bitmapSize.y);
SparseArray<Rect> suggestedCrops = new SparseArray<>();
@@ -455,8 +479,9 @@ public class WallpaperCropperTest {
for (boolean rtl : List.of(false, true)) {
Rect expectedCrop = rtl ? rightOf(bitmapRect, expectedCropSize)
: leftOf(bitmapRect, expectedCropSize);
- assertThat(mWallpaperCropper.getCrop(
- displaySize, bitmapSize, suggestedCrops, rtl))
+ assertThat(
+ WallpaperCropper.getCrop(
+ displaySize, defaultDisplayInfo, bitmapSize, suggestedCrops, rtl))
.isEqualTo(expectedCrop);
}
}
@@ -469,7 +494,7 @@ public class WallpaperCropperTest {
*/
@Test
public void testGetCrop_hasSuggestedCrop() {
- setUpWithDisplays(STANDARD_DISPLAY);
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(STANDARD_DISPLAY);
Point bitmapSize = new Point(800, 1000);
SparseArray<Rect> suggestedCrops = new SparseArray<>();
suggestedCrops.put(ORIENTATION_PORTRAIT, new Rect(0, 0, 400, 800));
@@ -479,11 +504,13 @@ public class WallpaperCropperTest {
}
for (boolean rtl : List.of(false, true)) {
- assertThat(mWallpaperCropper.getCrop(
- new Point(300, 800), bitmapSize, suggestedCrops, rtl))
+ assertThat(
+ WallpaperCropper.getCrop(new Point(300, 800), defaultDisplayInfo, bitmapSize,
+ suggestedCrops, rtl))
.isEqualTo(suggestedCrops.get(ORIENTATION_PORTRAIT));
- assertThat(mWallpaperCropper.getCrop(
- new Point(500, 800), bitmapSize, suggestedCrops, rtl))
+ assertThat(
+ WallpaperCropper.getCrop(new Point(500, 800), defaultDisplayInfo, bitmapSize,
+ suggestedCrops, rtl))
.isEqualTo(new Rect(0, 0, 500, 800));
}
}
@@ -499,7 +526,7 @@ public class WallpaperCropperTest {
*/
@Test
public void testGetCrop_hasRotatedSuggestedCrop() {
- setUpWithDisplays(STANDARD_DISPLAY);
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(STANDARD_DISPLAY);
Point bitmapSize = new Point(2000, 1800);
Rect bitmapRect = new Rect(0, 0, bitmapSize.x, bitmapSize.y);
SparseArray<Rect> suggestedCrops = new SparseArray<>();
@@ -510,12 +537,14 @@ public class WallpaperCropperTest {
suggestedCrops.put(ORIENTATION_PORTRAIT, centerOf(bitmapRect, portrait));
suggestedCrops.put(ORIENTATION_SQUARE_LANDSCAPE, centerOf(bitmapRect, squareLandscape));
for (boolean rtl : List.of(false, true)) {
- assertThat(mWallpaperCropper.getCrop(
- landscape, bitmapSize, suggestedCrops, rtl))
+ assertThat(
+ WallpaperCropper.getCrop(landscape, defaultDisplayInfo, bitmapSize,
+ suggestedCrops, rtl))
.isEqualTo(centerOf(bitmapRect, landscape));
- assertThat(mWallpaperCropper.getCrop(
- squarePortrait, bitmapSize, suggestedCrops, rtl))
+ assertThat(
+ WallpaperCropper.getCrop(squarePortrait, defaultDisplayInfo, bitmapSize,
+ suggestedCrops, rtl))
.isEqualTo(centerOf(bitmapRect, squarePortrait));
}
}
@@ -532,7 +561,7 @@ public class WallpaperCropperTest {
@Test
public void testGetCrop_hasUnfoldedSuggestedCrop() {
for (List<Point> displaySizes : ALL_FOLDABLE_DISPLAYS) {
- setUpWithDisplays(displaySizes);
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(displaySizes);
Point bitmapSize = new Point(2000, 2400);
Rect bitmapRect = new Rect(0, 0, bitmapSize.x, bitmapSize.y);
@@ -569,8 +598,9 @@ public class WallpaperCropperTest {
expectedCrop.right = Math.min(
unfoldedCrop.right, unfoldedCrop.right + maxParallax);
}
- assertThat(mWallpaperCropper.getCrop(
- foldedDisplay, bitmapSize, suggestedCrops, rtl))
+ assertThat(
+ WallpaperCropper.getCrop(foldedDisplay, defaultDisplayInfo, bitmapSize,
+ suggestedCrops, rtl))
.isEqualTo(expectedCrop);
}
}
@@ -588,7 +618,7 @@ public class WallpaperCropperTest {
@Test
public void testGetCrop_hasFoldedSuggestedCrop() {
for (List<Point> displaySizes : ALL_FOLDABLE_DISPLAYS) {
- setUpWithDisplays(displaySizes);
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(displaySizes);
Point bitmapSize = new Point(2000, 2000);
Rect bitmapRect = new Rect(0, 0, 2000, 2000);
@@ -610,12 +640,14 @@ public class WallpaperCropperTest {
Point unfoldedDisplayTwo = mDisplaySizes.get(unfoldedTwo);
for (boolean rtl : List.of(false, true)) {
- assertThat(centerOf(mWallpaperCropper.getCrop(
- unfoldedDisplayOne, bitmapSize, suggestedCrops, rtl), foldedDisplayOne))
+ assertThat(centerOf(
+ WallpaperCropper.getCrop(unfoldedDisplayOne, defaultDisplayInfo, bitmapSize,
+ suggestedCrops, rtl), foldedDisplayOne))
.isEqualTo(foldedCropOne);
- assertThat(centerOf(mWallpaperCropper.getCrop(
- unfoldedDisplayTwo, bitmapSize, suggestedCrops, rtl), foldedDisplayTwo))
+ assertThat(centerOf(
+ WallpaperCropper.getCrop(unfoldedDisplayTwo, defaultDisplayInfo, bitmapSize,
+ suggestedCrops, rtl), foldedDisplayTwo))
.isEqualTo(foldedCropTwo);
}
}
@@ -633,7 +665,7 @@ public class WallpaperCropperTest {
@Test
public void testGetCrop_hasRotatedUnfoldedSuggestedCrop() {
for (List<Point> displaySizes : ALL_FOLDABLE_DISPLAYS) {
- setUpWithDisplays(displaySizes);
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(displaySizes);
Point bitmapSize = new Point(2000, 2000);
Rect bitmapRect = new Rect(0, 0, 2000, 2000);
Point largestDisplay = displaySizes.stream().max(
@@ -650,8 +682,9 @@ public class WallpaperCropperTest {
Point rotatedFoldedDisplay = mDisplaySizes.get(rotatedFolded);
for (boolean rtl : List.of(false, true)) {
- assertThat(mWallpaperCropper.getCrop(
- rotatedFoldedDisplay, bitmapSize, suggestedCrops, rtl))
+ assertThat(
+ WallpaperCropper.getCrop(rotatedFoldedDisplay, defaultDisplayInfo,
+ bitmapSize, suggestedCrops, rtl))
.isEqualTo(centerOf(rotatedUnfoldedCrop, rotatedFoldedDisplay));
}
}
@@ -670,7 +703,7 @@ public class WallpaperCropperTest {
@Test
public void testGetCrop_hasRotatedFoldedSuggestedCrop() {
for (List<Point> displaySizes : ALL_FOLDABLE_DISPLAYS) {
- setUpWithDisplays(displaySizes);
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(displaySizes);
Point bitmapSize = new Point(2000, 2000);
Rect bitmapRect = new Rect(0, 0, 2000, 2000);
@@ -689,8 +722,8 @@ public class WallpaperCropperTest {
Point rotatedUnfoldedDisplay = mDisplaySizes.get(rotatedUnfolded);
for (boolean rtl : List.of(false, true)) {
- Rect rotatedUnfoldedCrop = mWallpaperCropper.getCrop(
- rotatedUnfoldedDisplay, bitmapSize, suggestedCrops, rtl);
+ Rect rotatedUnfoldedCrop = WallpaperCropper.getCrop(rotatedUnfoldedDisplay,
+ defaultDisplayInfo, bitmapSize, suggestedCrops, rtl);
assertThat(centerOf(rotatedUnfoldedCrop, rotatedFoldedDisplay))
.isEqualTo(rotatedFoldedCrop);
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperDefaultDisplayInfoTest.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperDefaultDisplayInfoTest.java
new file mode 100644
index 000000000000..312db91afb12
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperDefaultDisplayInfoTest.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wallpaper;
+
+import static android.app.WallpaperManager.ORIENTATION_LANDSCAPE;
+import static android.app.WallpaperManager.ORIENTATION_PORTRAIT;
+import static android.app.WallpaperManager.ORIENTATION_SQUARE_LANDSCAPE;
+import static android.app.WallpaperManager.ORIENTATION_SQUARE_PORTRAIT;
+import static android.app.WallpaperManager.ORIENTATION_UNKNOWN;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.util.SparseArray;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.R;
+import com.android.server.wallpaper.WallpaperDefaultDisplayInfo.FoldableOrientations;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import java.util.Set;
+
+/** Unit tests for {@link WallpaperDefaultDisplayInfo}. */
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class WallpaperDefaultDisplayInfoTest {
+ @Mock
+ private WindowManager mWindowManager;
+
+ @Mock
+ private Resources mResources;
+
+ @Before
+ public void setUp() {
+ initMocks(this);
+ }
+
+ @Test
+ public void defaultDisplayInfo_foldable_shouldHaveExpectedContent() {
+ doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152);
+ Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424);
+ WindowMetrics innerDisplayMetrics =
+ new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ WindowMetrics outerDisplayMetrics =
+ new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics));
+
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ SparseArray<Point> displaySizes = new SparseArray<>();
+ displaySizes.put(ORIENTATION_PORTRAIT, new Point(1080, 2424));
+ displaySizes.put(ORIENTATION_LANDSCAPE, new Point(2424, 1080));
+ displaySizes.put(ORIENTATION_SQUARE_PORTRAIT, new Point(2076, 2152));
+ displaySizes.put(ORIENTATION_SQUARE_LANDSCAPE, new Point(2152, 2076));
+ assertThat(defaultDisplayInfo.defaultDisplaySizes.contentEquals(displaySizes)).isTrue();
+ assertThat(defaultDisplayInfo.isFoldable).isTrue();
+ assertThat(defaultDisplayInfo.isLargeScreen).isFalse();
+ assertThat(defaultDisplayInfo.foldableOrientations).containsExactly(
+ new FoldableOrientations(
+ /* foldedOrientation= */ ORIENTATION_PORTRAIT,
+ /* unfoldedOrientation= */ ORIENTATION_SQUARE_PORTRAIT),
+ new FoldableOrientations(
+ /* foldedOrientation= */ ORIENTATION_LANDSCAPE,
+ /* unfoldedOrientation= */ ORIENTATION_SQUARE_LANDSCAPE));
+ }
+
+ @Test
+ public void defaultDisplayInfo_tablet_shouldHaveExpectedContent() {
+ doReturn(new int[]{}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect displayBounds = new Rect(0, 0, 2560, 1600);
+ WindowMetrics displayMetrics =
+ new WindowMetrics(displayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ .thenReturn(Set.of(displayMetrics));
+
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ SparseArray<Point> displaySizes = new SparseArray<>();
+ displaySizes.put(ORIENTATION_PORTRAIT, new Point(1600, 2560));
+ displaySizes.put(ORIENTATION_LANDSCAPE, new Point(2560, 1600));
+ assertThat(defaultDisplayInfo.defaultDisplaySizes.contentEquals(displaySizes)).isTrue();
+ assertThat(defaultDisplayInfo.isFoldable).isFalse();
+ assertThat(defaultDisplayInfo.isLargeScreen).isTrue();
+ assertThat(defaultDisplayInfo.foldableOrientations).isEmpty();
+ }
+
+ @Test
+ public void defaultDisplayInfo_phone_shouldHaveExpectedContent() {
+ doReturn(new int[]{}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect displayBounds = new Rect(0, 0, 1280, 2856);
+ WindowMetrics displayMetrics =
+ new WindowMetrics(displayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 3f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ .thenReturn(Set.of(displayMetrics));
+
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ SparseArray<Point> displaySizes = new SparseArray<>();
+ displaySizes.put(ORIENTATION_PORTRAIT, new Point(1280, 2856));
+ displaySizes.put(ORIENTATION_LANDSCAPE, new Point(2856, 1280));
+ assertThat(defaultDisplayInfo.defaultDisplaySizes.contentEquals(displaySizes)).isTrue();
+ assertThat(defaultDisplayInfo.isFoldable).isFalse();
+ assertThat(defaultDisplayInfo.isLargeScreen).isFalse();
+ assertThat(defaultDisplayInfo.foldableOrientations).isEmpty();
+ }
+
+ @Test
+ public void defaultDisplayInfo_equals_sameContent_shouldEqual() {
+ doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152);
+ Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424);
+ WindowMetrics innerDisplayMetrics =
+ new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ WindowMetrics outerDisplayMetrics =
+ new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics));
+
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+ WallpaperDefaultDisplayInfo otherDefaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ assertThat(defaultDisplayInfo).isEqualTo(otherDefaultDisplayInfo);
+ }
+
+ @Test
+ public void defaultDisplayInfo_equals_differentBounds_shouldNotEqual() {
+ doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152);
+ Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424);
+ WindowMetrics innerDisplayMetrics =
+ new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ WindowMetrics outerDisplayMetrics =
+ new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ // For the first call
+ .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics))
+ // For the second+ call
+ .thenReturn(Set.of(innerDisplayMetrics));
+
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+ WallpaperDefaultDisplayInfo otherDefaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ assertThat(defaultDisplayInfo).isNotEqualTo(otherDefaultDisplayInfo);
+ }
+
+ @Test
+ public void defaultDisplayInfo_hashCode_sameContent_shouldEqual() {
+ doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152);
+ Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424);
+ WindowMetrics innerDisplayMetrics =
+ new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ WindowMetrics outerDisplayMetrics =
+ new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics));
+
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+ WallpaperDefaultDisplayInfo otherDefaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ assertThat(defaultDisplayInfo.hashCode()).isEqualTo(otherDefaultDisplayInfo.hashCode());
+ }
+
+ @Test
+ public void defaultDisplayInfo_hashCode_differentBounds_shouldNotEqual() {
+ doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152);
+ Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424);
+ WindowMetrics innerDisplayMetrics =
+ new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ WindowMetrics outerDisplayMetrics =
+ new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ // For the first call
+ .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics))
+ // For the second+ call
+ .thenReturn(Set.of(innerDisplayMetrics));
+
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+ WallpaperDefaultDisplayInfo otherDefaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ assertThat(defaultDisplayInfo.hashCode()).isNotEqualTo(otherDefaultDisplayInfo.hashCode());
+ }
+
+ @Test
+ public void getFoldedOrientation_foldable_shouldReturnExpectedOrientation() {
+ doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152);
+ Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424);
+ WindowMetrics innerDisplayMetrics =
+ new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ WindowMetrics outerDisplayMetrics =
+ new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics));
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_SQUARE_PORTRAIT))
+ .isEqualTo(ORIENTATION_PORTRAIT);
+ assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_SQUARE_LANDSCAPE))
+ .isEqualTo(ORIENTATION_LANDSCAPE);
+ // Use a folded orientation for a folded orientation should return unknown.
+ assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_PORTRAIT))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_LANDSCAPE))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ }
+
+ @Test
+ public void getUnfoldedOrientation_foldable_shouldReturnExpectedOrientation() {
+ doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152);
+ Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424);
+ WindowMetrics innerDisplayMetrics =
+ new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ WindowMetrics outerDisplayMetrics =
+ new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics));
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_PORTRAIT))
+ .isEqualTo(ORIENTATION_SQUARE_PORTRAIT);
+ assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_LANDSCAPE))
+ .isEqualTo(ORIENTATION_SQUARE_LANDSCAPE);
+ // Use an unfolded orientation for an unfolded orientation should return unknown.
+ assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_SQUARE_PORTRAIT))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_SQUARE_LANDSCAPE))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ }
+
+ @Test
+ public void getFoldedOrientation_nonFoldable_shouldReturnUnknown() {
+ doReturn(new int[]{}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect displayBounds = new Rect(0, 0, 2560, 1600);
+ WindowMetrics displayMetrics =
+ new WindowMetrics(displayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ .thenReturn(Set.of(displayMetrics));
+
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_SQUARE_PORTRAIT))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_SQUARE_LANDSCAPE))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_PORTRAIT))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_LANDSCAPE))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ }
+
+ @Test
+ public void getUnFoldedOrientation_nonFoldable_shouldReturnUnknown() {
+ doReturn(new int[]{}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect displayBounds = new Rect(0, 0, 2560, 1600);
+ WindowMetrics displayMetrics =
+ new WindowMetrics(displayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ .thenReturn(Set.of(displayMetrics));
+
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_SQUARE_PORTRAIT))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_SQUARE_LANDSCAPE))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_PORTRAIT))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_LANDSCAPE))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ }
+}
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
index 5165e34c7fcd..fc864dd230d9 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
@@ -22,6 +22,7 @@ import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.spy;
@@ -330,31 +331,24 @@ public class BatteryStatsHistoryTest {
return invocation.callRealMethod();
}).when(mHistory).readFragmentToParcel(any(), any());
- // Prepare history for iteration
- mHistory.iterate(0, MonotonicClock.UNDEFINED);
-
- Parcel parcel = mHistory.getNextParcel(0, Long.MAX_VALUE);
- assertThat(parcel).isNotNull();
- assertThat(mReadFiles).containsExactly("123.bh");
-
- // Skip to the end to force reading the next parcel
- parcel.setDataPosition(parcel.dataSize());
- mReadFiles.clear();
- parcel = mHistory.getNextParcel(0, Long.MAX_VALUE);
- assertThat(parcel).isNotNull();
- assertThat(mReadFiles).containsExactly("1000.bh");
-
- parcel.setDataPosition(parcel.dataSize());
- mReadFiles.clear();
- parcel = mHistory.getNextParcel(0, Long.MAX_VALUE);
- assertThat(parcel).isNotNull();
- assertThat(mReadFiles).containsExactly("2000.bh");
+ int eventsRead = 0;
+ BatteryStatsHistoryIterator iterator = mHistory.iterate(0, MonotonicClock.UNDEFINED);
+ while (iterator.hasNext()) {
+ HistoryItem item = iterator.next();
+ if (item.eventCode == HistoryItem.EVENT_JOB_START) {
+ eventsRead++;
+ assertThat(mReadFiles).containsExactly("123.bh");
+ } else if (item.eventCode == HistoryItem.EVENT_JOB_FINISH) {
+ eventsRead++;
+ assertThat(mReadFiles).containsExactly("123.bh", "1000.bh");
+ } else if (item.eventCode == HistoryItem.EVENT_ALARM) {
+ eventsRead++;
+ assertThat(mReadFiles).containsExactly("123.bh", "1000.bh", "2000.bh");
+ }
+ }
- parcel.setDataPosition(parcel.dataSize());
- mReadFiles.clear();
- parcel = mHistory.getNextParcel(0, Long.MAX_VALUE);
- assertThat(parcel).isNull();
- assertThat(mReadFiles).isEmpty();
+ assertThat(eventsRead).isEqualTo(3);
+ assertThat(mReadFiles).containsExactly("123.bh", "1000.bh", "2000.bh", "3000.bh");
}
@Test
@@ -372,25 +366,19 @@ public class BatteryStatsHistoryTest {
return invocation.callRealMethod();
}).when(mHistory).readFragmentToParcel(any(), any());
- // Prepare history for iteration
- mHistory.iterate(1000, 3000);
-
- Parcel parcel = mHistory.getNextParcel(1000, 3000);
- assertThat(parcel).isNotNull();
- assertThat(mReadFiles).containsExactly("1000.bh");
-
- // Skip to the end to force reading the next parcel
- parcel.setDataPosition(parcel.dataSize());
- mReadFiles.clear();
- parcel = mHistory.getNextParcel(1000, 3000);
- assertThat(parcel).isNotNull();
- assertThat(mReadFiles).containsExactly("2000.bh");
+ BatteryStatsHistoryIterator iterator = mHistory.iterate(1000, 3000);
+ while (iterator.hasNext()) {
+ HistoryItem item = iterator.next();
+ if (item.eventCode == HistoryItem.EVENT_JOB_START) {
+ fail("Event outside the range");
+ } else if (item.eventCode == HistoryItem.EVENT_JOB_FINISH) {
+ assertThat(mReadFiles).containsExactly("1000.bh");
+ } else if (item.eventCode == HistoryItem.EVENT_ALARM) {
+ fail("Event outside the range");
+ }
+ }
- parcel.setDataPosition(parcel.dataSize());
- mReadFiles.clear();
- parcel = mHistory.getNextParcel(1000, 3000);
- assertThat(parcel).isNull();
- assertThat(mReadFiles).isEmpty();
+ assertThat(mReadFiles).containsExactly("1000.bh", "2000.bh");
}
private void prepareMultiFileHistory() {
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
index ea83825cd810..ea25e7992dd9 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
@@ -43,6 +43,7 @@ import android.view.accessibility.AccessibilityManager;
import com.android.internal.accessibility.util.AccessibilityUtils;
import com.android.server.accessibility.AccessibilityTraceManager;
+import com.android.server.accessibility.BaseEventStreamTransformation;
import org.junit.After;
import org.junit.Before;
@@ -70,6 +71,19 @@ public class AutoclickControllerTest {
@Mock private WindowManager mMockWindowManager;
private AutoclickController mController;
+ private static class MotionEventCaptor extends BaseEventStreamTransformation {
+ public MotionEvent downEvent;
+
+ @Override
+ public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ downEvent = event;
+ break;
+ }
+ }
+ }
+
@Before
public void setUp() {
mTestableLooper = TestableLooper.get(this);
@@ -660,6 +674,153 @@ public class AutoclickControllerTest {
assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting()).isEqualTo(-1);
}
+ @Test
+ @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
+ public void onMotionEvent_flagOn_lazyInitAutoclickScrollPanel() {
+ assertThat(mController.mAutoclickScrollPanel).isNull();
+
+ injectFakeMouseActionHoverMoveEvent();
+
+ assertThat(mController.mAutoclickScrollPanel).isNotNull();
+ }
+
+ @Test
+ @DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
+ public void onMotionEvent_flagOff_notInitAutoclickScrollPanel() {
+ assertThat(mController.mAutoclickScrollPanel).isNull();
+
+ injectFakeMouseActionHoverMoveEvent();
+
+ assertThat(mController.mAutoclickScrollPanel).isNull();
+ }
+
+ @Test
+ @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
+ public void onDestroy_flagOn_hideAutoclickScrollPanel() {
+ injectFakeMouseActionHoverMoveEvent();
+ AutoclickScrollPanel mockAutoclickScrollPanel = mock(AutoclickScrollPanel.class);
+ mController.mAutoclickScrollPanel = mockAutoclickScrollPanel;
+
+ mController.onDestroy();
+
+ verify(mockAutoclickScrollPanel).hide();
+ }
+
+ @Test
+ @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
+ public void changeFromScrollToOtherClickType_hidesScrollPanel() {
+ injectFakeMouseActionHoverMoveEvent();
+
+ // Set active click type to SCROLL.
+ mController.clickPanelController.handleAutoclickTypeChange(
+ AutoclickTypePanel.AUTOCLICK_TYPE_SCROLL);
+
+ // Show the scroll panel.
+ mController.mAutoclickScrollPanel.show();
+ assertThat(mController.mAutoclickScrollPanel.isVisible()).isTrue();
+
+ // Change click type to LEFT_CLICK.
+ mController.clickPanelController.handleAutoclickTypeChange(
+ AutoclickTypePanel.AUTOCLICK_TYPE_LEFT_CLICK);
+
+ // Verify scroll panel is hidden.
+ assertThat(mController.mAutoclickScrollPanel.isVisible()).isFalse();
+ }
+
+ @Test
+ public void sendClick_clickType_leftClick() {
+ MotionEventCaptor motionEventCaptor = new MotionEventCaptor();
+ mController.setNext(motionEventCaptor);
+
+ injectFakeMouseActionHoverMoveEvent();
+ // Set delay to zero so click is scheduled to run immediately.
+ mController.mClickScheduler.updateDelay(0);
+
+ // Send hover move event.
+ MotionEvent hoverMove = MotionEvent.obtain(
+ /* downTime= */ 0,
+ /* eventTime= */ 100,
+ /* action= */ MotionEvent.ACTION_HOVER_MOVE,
+ /* x= */ 30f,
+ /* y= */ 0f,
+ /* metaState= */ 0);
+ hoverMove.setSource(InputDevice.SOURCE_MOUSE);
+ mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0);
+ mTestableLooper.processAllMessages();
+
+ // Verify left click sent.
+ assertThat(motionEventCaptor.downEvent).isNotNull();
+ assertThat(motionEventCaptor.downEvent.getButtonState()).isEqualTo(
+ MotionEvent.BUTTON_PRIMARY);
+ }
+
+ @Test
+ public void sendClick_clickType_rightClick() {
+ MotionEventCaptor motionEventCaptor = new MotionEventCaptor();
+ mController.setNext(motionEventCaptor);
+
+ injectFakeMouseActionHoverMoveEvent();
+ // Set delay to zero so click is scheduled to run immediately.
+ mController.mClickScheduler.updateDelay(0);
+
+ // Set click type to right click.
+ mController.clickPanelController.handleAutoclickTypeChange(
+ AutoclickTypePanel.AUTOCLICK_TYPE_RIGHT_CLICK);
+
+ // Send hover move event.
+ MotionEvent hoverMove = MotionEvent.obtain(
+ /* downTime= */ 0,
+ /* eventTime= */ 100,
+ /* action= */ MotionEvent.ACTION_HOVER_MOVE,
+ /* x= */ 30f,
+ /* y= */ 0f,
+ /* metaState= */ 0);
+ hoverMove.setSource(InputDevice.SOURCE_MOUSE);
+ mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0);
+ mTestableLooper.processAllMessages();
+
+ // Verify right click sent.
+ assertThat(motionEventCaptor.downEvent).isNotNull();
+ assertThat(motionEventCaptor.downEvent.getButtonState()).isEqualTo(
+ MotionEvent.BUTTON_SECONDARY);
+ }
+
+ @Test
+ @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
+ public void hoverOnAutoclickPanel_rightClickType_forceTriggerLeftClick() {
+ MotionEventCaptor motionEventCaptor = new MotionEventCaptor();
+ mController.setNext(motionEventCaptor);
+
+ injectFakeMouseActionHoverMoveEvent();
+ // Set delay to zero so click is scheduled to run immediately.
+ mController.mClickScheduler.updateDelay(0);
+
+ // Set click type to right click.
+ mController.clickPanelController.handleAutoclickTypeChange(
+ AutoclickTypePanel.AUTOCLICK_TYPE_RIGHT_CLICK);
+ // Set mouse to hover panel.
+ AutoclickTypePanel mockAutoclickTypePanel = mock(AutoclickTypePanel.class);
+ when(mockAutoclickTypePanel.isHovered()).thenReturn(true);
+ mController.mAutoclickTypePanel = mockAutoclickTypePanel;
+
+ // Send hover move event.
+ MotionEvent hoverMove = MotionEvent.obtain(
+ /* downTime= */ 0,
+ /* eventTime= */ 100,
+ /* action= */ MotionEvent.ACTION_HOVER_MOVE,
+ /* x= */ 30f,
+ /* y= */ 0f,
+ /* metaState= */ 0);
+ hoverMove.setSource(InputDevice.SOURCE_MOUSE);
+ mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0);
+ mTestableLooper.processAllMessages();
+
+ // Verify left click is sent due to the mouse hovering the panel.
+ assertThat(motionEventCaptor.downEvent).isNotNull();
+ assertThat(motionEventCaptor.downEvent.getButtonState()).isEqualTo(
+ MotionEvent.BUTTON_PRIMARY);
+ }
+
private void injectFakeMouseActionHoverMoveEvent() {
MotionEvent event = getFakeMotionHoverMoveEvent();
event.setSource(InputDevice.SOURCE_MOUSE);
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java
new file mode 100644
index 000000000000..f445b50c7d9c
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.accessibility.autoclick;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.times;
+
+import android.content.Context;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableContext;
+import android.testing.TestableLooper;
+import android.view.WindowManager;
+
+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;
+
+/** Test cases for {@link AutoclickScrollPanel}. */
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class AutoclickScrollPanelTest {
+ @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+ @Rule
+ public TestableContext mTestableContext =
+ new TestableContext(getInstrumentation().getContext());
+
+ @Mock private WindowManager mMockWindowManager;
+ private AutoclickScrollPanel mScrollPanel;
+
+ @Before
+ public void setUp() {
+ mTestableContext.addMockSystemService(Context.WINDOW_SERVICE, mMockWindowManager);
+ mScrollPanel = new AutoclickScrollPanel(mTestableContext, mMockWindowManager);
+ }
+
+ @Test
+ public void show_addsViewToWindowManager() {
+ mScrollPanel.show();
+
+ // Verify view is added to window manager.
+ verify(mMockWindowManager).addView(any(), any(WindowManager.LayoutParams.class));
+
+ // Verify isVisible reflects correct state.
+ assertThat(mScrollPanel.isVisible()).isTrue();
+ }
+
+ @Test
+ public void show_alreadyVisible_doesNotAddAgain() {
+ // Show twice.
+ mScrollPanel.show();
+ mScrollPanel.show();
+
+ // Verify addView was only called once.
+ verify(mMockWindowManager, times(1)).addView(any(), any());
+ }
+
+ @Test
+ public void hide_removesViewFromWindowManager() {
+ // First show the panel.
+ mScrollPanel.show();
+ // Then hide it.
+ mScrollPanel.hide();
+ // Verify view is removed from window manager.
+ verify(mMockWindowManager).removeView(any());
+ // Verify scroll panel is hidden.
+ assertThat(mScrollPanel.isVisible()).isFalse();
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java
index 1af59daa9c78..5922b12edc1e 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java
@@ -37,6 +37,7 @@ import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -46,6 +47,7 @@ import android.content.Context;
import android.graphics.PointF;
import android.os.Looper;
import android.os.SystemClock;
+import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.testing.DexmakerShareClassLoaderRule;
@@ -504,6 +506,36 @@ public class TouchExplorerTest {
assertThat(sentRawEvent.getDisplayId()).isEqualTo(rawDisplayId);
}
+ @Test
+ @DisableFlags(Flags.FLAG_POINTER_UP_MOTION_EVENT_IN_TOUCH_EXPLORATION)
+ public void handleMotionEventStateTouchExploring_pointerUp_doesNotSendToManager() {
+ mTouchExplorer.getState().setServiceDetectsGestures(true);
+ mTouchExplorer.getState().clear();
+
+ mLastEvent = pointerDownEvent();
+ mTouchExplorer.getState().startTouchExploring();
+ MotionEvent event = fromTouchscreen(pointerUpEvent());
+
+ mTouchExplorer.onMotionEvent(event, event, /*policyFlags=*/0);
+
+ verify(mMockAms, never()).sendMotionEventToListeningServices(event);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_POINTER_UP_MOTION_EVENT_IN_TOUCH_EXPLORATION)
+ public void handleMotionEventStateTouchExploring_pointerUp_sendsToManager() {
+ mTouchExplorer.getState().setServiceDetectsGestures(true);
+ mTouchExplorer.getState().clear();
+
+ mLastEvent = pointerDownEvent();
+ mTouchExplorer.getState().startTouchExploring();
+ MotionEvent event = fromTouchscreen(pointerUpEvent());
+
+ mTouchExplorer.onMotionEvent(event, event, /*policyFlags=*/0);
+
+ verify(mMockAms).sendMotionEventToListeningServices(event);
+ }
+
/**
* Used to play back event data of a gesture by parsing the log into MotionEvents and sending
* them to TouchExplorer.
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchStateTest.java b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchStateTest.java
new file mode 100644
index 000000000000..3e7d9fd05327
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchStateTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.accessibility.gestures;
+
+import static android.view.accessibility.AccessibilityEvent.TYPE_TOUCH_INTERACTION_END;
+
+import static com.android.server.accessibility.gestures.TouchState.STATE_CLEAR;
+import static com.android.server.accessibility.gestures.TouchState.STATE_TOUCH_EXPLORING;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.view.Display;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.accessibility.AccessibilityManagerService;
+import com.android.server.accessibility.Flags;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class TouchStateTest {
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ private TouchState mTouchState;
+ @Mock private AccessibilityManagerService mMockAms;
+
+ @Before
+ public void setup() {
+ mTouchState = new TouchState(Display.DEFAULT_DISPLAY, mMockAms);
+ }
+
+ @EnableFlags(Flags.FLAG_POINTER_UP_MOTION_EVENT_IN_TOUCH_EXPLORATION)
+ @Test
+ public void injectedEvent_interactionEnd_pointerDown_startsTouchExploring() {
+ mTouchState.mReceivedPointerTracker.mReceivedPointersDown = 1;
+ mTouchState.onInjectedAccessibilityEvent(TYPE_TOUCH_INTERACTION_END);
+ assertThat(mTouchState.getState()).isEqualTo(STATE_TOUCH_EXPLORING);
+ }
+
+ @EnableFlags(Flags.FLAG_POINTER_UP_MOTION_EVENT_IN_TOUCH_EXPLORATION)
+ @Test
+ public void injectedEvent_interactionEnd_pointerUp_clears() {
+ mTouchState.mReceivedPointerTracker.mReceivedPointersDown = 0;
+ mTouchState.onInjectedAccessibilityEvent(TYPE_TOUCH_INTERACTION_END);
+ assertThat(mTouchState.getState()).isEqualTo(STATE_CLEAR);
+ }
+
+ @DisableFlags(Flags.FLAG_POINTER_UP_MOTION_EVENT_IN_TOUCH_EXPLORATION)
+ @Test
+ public void injectedEvent_interactionEnd_clears() {
+ mTouchState.onInjectedAccessibilityEvent(TYPE_TOUCH_INTERACTION_END);
+ assertThat(mTouchState.getState()).isEqualTo(STATE_CLEAR);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpXmlPersistenceTest.java b/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpXmlPersistenceTest.java
index ae973be17904..56f802b278c6 100644
--- a/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpXmlPersistenceTest.java
+++ b/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpXmlPersistenceTest.java
@@ -89,8 +89,7 @@ public class DiscreteAppOpXmlPersistenceTest {
int attributionChainId = AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE;
mDiscreteRegistry.recordDiscreteAccess(uid, packageName, deviceId, op, null, opFlags,
- uidState, accessTime, duration, attributionFlags, attributionChainId,
- DiscreteOpsXmlRegistry.ACCESS_TYPE_FINISH_OP);
+ uidState, accessTime, duration, attributionFlags, attributionChainId);
// Verify in-memory object is correct
fetchDiscreteOpsAndValidate(uid, packageName, op, deviceId, null, accessTime,
@@ -121,8 +120,7 @@ public class DiscreteAppOpXmlPersistenceTest {
int attributionChainId = 10;
mDiscreteRegistry.recordDiscreteAccess(uid, packageName, deviceId, op, null, opFlags,
- uidState, accessTime, duration, attributionFlags, attributionChainId,
- DiscreteOpsXmlRegistry.ACCESS_TYPE_START_OP);
+ uidState, accessTime, duration, attributionFlags, attributionChainId);
fetchDiscreteOpsAndValidate(uid, packageName, op, deviceId, null, accessTime,
duration, uidState, opFlags, attributionFlags, attributionChainId);
diff --git a/services/tests/servicestests/src/com/android/server/appop/DiscreteOpsMigrationAndRollbackTest.java b/services/tests/servicestests/src/com/android/server/appop/DiscreteOpsMigrationAndRollbackTest.java
index 8eea1c73d4f2..6c66f149baa7 100644
--- a/services/tests/servicestests/src/com/android/server/appop/DiscreteOpsMigrationAndRollbackTest.java
+++ b/services/tests/servicestests/src/com/android/server/appop/DiscreteOpsMigrationAndRollbackTest.java
@@ -70,7 +70,7 @@ public class DiscreteOpsMigrationAndRollbackTest {
opEvent.getDeviceId(), opEvent.getOpCode(), opEvent.getAttributionTag(),
opEvent.getOpFlags(), opEvent.getUidState(), opEvent.getAccessTime(),
opEvent.getDuration(), opEvent.getAttributionFlags(),
- (int) opEvent.getChainId(), DiscreteOpsRegistry.ACCESS_TYPE_NOTE_OP);
+ (int) opEvent.getChainId());
}
xmlRegistry.writeAndClearOldAccessHistory();
assertThat(xmlRegistry.readLargestChainIdFromDiskLocked()).isEqualTo(RECORD_COUNT);
@@ -104,7 +104,7 @@ public class DiscreteOpsMigrationAndRollbackTest {
opEvent.getDeviceId(), opEvent.getOpCode(), opEvent.getAttributionTag(),
opEvent.getOpFlags(), opEvent.getUidState(), opEvent.getAccessTime(),
opEvent.getDuration(), opEvent.getAttributionFlags(),
- (int) opEvent.getChainId(), DiscreteOpsRegistry.ACCESS_TYPE_NOTE_OP);
+ (int) opEvent.getChainId());
}
// flush records from cache to the database.
sqlRegistry.shutdown();
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index 30aa8cebdff6..01bcc2584fe1 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -143,6 +143,7 @@ import android.provider.DeviceConfig;
import android.provider.Settings;
import android.security.KeyChain;
import android.security.keystore.AttestationUtils;
+import android.telephony.SubscriptionInfo;
import android.telephony.TelephonyManager;
import android.telephony.data.ApnSetting;
import android.test.MoreAsserts;
@@ -8715,6 +8716,47 @@ public class DevicePolicyManagerTest extends DpmTestBase {
}
}
+ @RequiresFlagsEnabled(Flags.FLAG_REMOVE_MANAGED_ESIM_ON_WORK_PROFILE_DELETION)
+ @Test
+ public void testManagedProfileDeleted_managedEmbeddedSubscriptionDeleted() throws Exception {
+ // Setup PO mode.
+ setupProfileOwner();
+ // Mock SubscriptionManager to return a subscription managed by the profile owner package.
+ int managedSubscriptionId = 42;
+ SubscriptionInfo managedSubscription = new SubscriptionInfo.Builder().setCardId(1).setId(
+ managedSubscriptionId).setGroupOwner(admin1.getPackageName()).build();
+ when(getServices().subscriptionManager.getAvailableSubscriptionInfoList()).thenReturn(
+ List.of(managedSubscription));
+
+ // Send a ACTION_MANAGED_PROFILE_REMOVED broadcast to emulate a managed profile being
+ // removed.
+ sendBroadcastWithUser(dpms, Intent.ACTION_MANAGED_PROFILE_REMOVED, CALLER_USER_HANDLE);
+
+ // Verify that EuiccManager was called to delete the subscription.
+ verify(getServices().euiccManager).deleteSubscription(eq(managedSubscriptionId), any());
+ }
+
+ @RequiresFlagsDisabled(Flags.FLAG_REMOVE_MANAGED_ESIM_ON_WORK_PROFILE_DELETION)
+ @Test
+ public void testManagedProfileDeleted_flagDisabled_managedEmbeddedSubscriptionDeleted()
+ throws Exception {
+ // Set up PO mode.
+ setupProfileOwner();
+ // Mock SubscriptionManager to return a subscription managed by the profile owner package.
+ int managedSubscriptionId = 42;
+ SubscriptionInfo managedSubscription = new SubscriptionInfo.Builder().setCardId(1).setId(
+ managedSubscriptionId).setGroupOwner(admin1.getPackageName()).build();
+ when(getServices().subscriptionManager.getAvailableSubscriptionInfoList()).thenReturn(
+ List.of(managedSubscription));
+
+ // Send a ACTION_MANAGED_PROFILE_REMOVED broadcast to emulate a managed profile being
+ // removed.
+ sendBroadcastWithUser(dpms, Intent.ACTION_MANAGED_PROFILE_REMOVED, CALLER_USER_HANDLE);
+
+ // Verify that EuiccManager was not called to delete the subscription.
+ verifyZeroInteractions(getServices().euiccManager);
+ }
+
private void setupVpnAuthorization(String userVpnPackage, int userVpnUid) {
final AppOpsManager.PackageOps vpnOp = new AppOpsManager.PackageOps(userVpnPackage,
userVpnUid, List.of(new AppOpsManager.OpEntry(
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
index 00b0c558b4e3..479af73bae3c 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
@@ -253,6 +253,8 @@ public class DpmMockContext extends MockContext {
return mMockSystemServices.subscriptionManager;
case Context.USB_SERVICE:
return mMockSystemServices.usbManager;
+ case Context.EUICC_SERVICE:
+ return mMockSystemServices.euiccManager;
}
throw new UnsupportedOperationException();
}
@@ -487,6 +489,14 @@ public class DpmMockContext extends MockContext {
}
@Override
+ public Intent registerReceiverAsUser(BroadcastReceiver receiver, UserHandle user,
+ IntentFilter filter, String broadcastPermission, Handler scheduler, int flags) {
+ mMockSystemServices.registerReceiver(receiver, filter, scheduler);
+ return spiedContext.registerReceiverAsUser(receiver, user, filter, broadcastPermission,
+ scheduler, flags);
+ }
+
+ @Override
public void unregisterReceiver(BroadcastReceiver receiver) {
mMockSystemServices.unregisterReceiver(receiver);
spiedContext.unregisterReceiver(receiver);
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java b/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java
index 3e4448c1dafa..d01fa91e22c2 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java
@@ -68,6 +68,7 @@ import android.provider.Settings;
import android.security.KeyChain;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
+import android.telephony.euicc.EuiccManager;
import android.test.mock.MockContentProvider;
import android.test.mock.MockContentResolver;
import android.util.ArrayMap;
@@ -151,6 +152,7 @@ public class MockSystemServices {
public final File dataDir;
public final PolicyPathProvider pathProvider;
public final SupervisionManagerInternal supervisionManagerInternal;
+ public final EuiccManager euiccManager;
private final Map<String, PackageState> mTestPackageStates = new ArrayMap<>();
@@ -206,6 +208,7 @@ public class MockSystemServices {
roleManagerForMock = mock(RoleManagerForMock.class);
subscriptionManager = mock(SubscriptionManager.class);
supervisionManagerInternal = mock(SupervisionManagerInternal.class);
+ euiccManager = mock(EuiccManager.class);
// Package manager is huge, so we use a partial mock instead.
packageManager = spy(realContext.getPackageManager());
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
index b0ffebb973a1..aa1d5835bfc8 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -2295,6 +2295,38 @@ public class HdmiCecLocalDeviceTvTest {
.hasSize(1);
}
+ @Test
+ public void onOneTouchPlay_wakeUp_exist_device() {
+ HdmiCecMessage requestActiveSource =
+ HdmiCecMessageBuilder.buildRequestActiveSource(ADDR_TV);
+
+ // Go to standby to trigger RequestActiveSourceAction for playback_1
+ mHdmiControlService.onStandby(STANDBY_SCREEN_OFF);
+ mTestLooper.dispatchAll();
+
+ mNativeWrapper.setPollAddressResponse(ADDR_PLAYBACK_1, SendMessageResult.SUCCESS);
+ mHdmiControlService.onWakeUp(WAKE_UP_SCREEN_ON);
+ mTestLooper.dispatchAll();
+
+ // Skip the LauncherX API timeout.
+ mTestLooper.moveTimeForward(TIMEOUT_WAIT_FOR_TV_ASSERT_ACTIVE_SOURCE_MS);
+ mTestLooper.dispatchAll();
+
+ assertThat(mNativeWrapper.getResultMessages()).contains(requestActiveSource);
+ mNativeWrapper.clearResultMessages();
+
+ // turn off TV and wake up with one touch play
+ mHdmiControlService.onStandby(STANDBY_SCREEN_OFF);
+ mTestLooper.dispatchAll();
+
+ // FakePowerManagerWrapper#wakeUp() doesn't broadcast Intent.ACTION_SCREEN_ON
+ // manually trigger onWakeUp to mock OTP
+ mHdmiControlService.onWakeUp(WAKE_UP_SCREEN_ON);
+ mTestLooper.dispatchAll();
+
+ assertThat(mNativeWrapper.getResultMessages()).doesNotContain(requestActiveSource);
+ }
+
@Test
public void handleReportAudioStatus_SamOnAvrStandby_startSystemAudioActionFromTv() {
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
index 2da2f50447c7..e836780b3f71 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
@@ -16,6 +16,7 @@
package com.android.server.locksettings;
+import static android.content.pm.UserInfo.FLAG_FOR_TESTING;
import static android.content.pm.UserInfo.FLAG_FULL;
import static android.content.pm.UserInfo.FLAG_MAIN;
import static android.content.pm.UserInfo.FLAG_PRIMARY;
@@ -44,6 +45,8 @@ import static org.mockito.Mockito.when;
import android.app.PropertyInvalidatedCache;
import android.app.admin.PasswordMetrics;
+import android.content.ComponentName;
+import android.content.pm.UserInfo;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
@@ -357,6 +360,45 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests {
}
@Test
+ public void testEscrowDataRetainedWhenManagedUserVerifiesCredential() throws RemoteException {
+ when(mDeviceStateCache.isUserOrganizationManaged(anyInt())).thenReturn(true);
+
+ LockscreenCredential password = newPassword("password");
+ initSpAndSetCredential(PRIMARY_USER_ID, password);
+
+ mService.verifyCredential(password, PRIMARY_USER_ID, 0 /* flags */);
+
+ assertTrue("Escrow data was destroyed", mSpManager.hasEscrowData(PRIMARY_USER_ID));
+ }
+
+ @Test
+ public void testEscrowDataRetainedWhenUnmanagedTestUserVerifiesCredential()
+ throws RemoteException {
+ when(mDeviceStateCache.isUserOrganizationManaged(anyInt())).thenReturn(false);
+ UserInfo userInfo = mUserManagerInternal.getUserInfo(PRIMARY_USER_ID);
+ userInfo.flags |= FLAG_FOR_TESTING;
+
+ LockscreenCredential password = newPassword("password");
+ initSpAndSetCredential(PRIMARY_USER_ID, password);
+
+ mService.verifyCredential(password, PRIMARY_USER_ID, 0 /* flags */);
+
+ assertTrue("Escrow data was destroyed", mSpManager.hasEscrowData(PRIMARY_USER_ID));
+ }
+
+ @Test
+ public void testEscrowDataDeletedWhenUnmanagedUserVerifiesCredential() throws RemoteException {
+ when(mDeviceStateCache.isUserOrganizationManaged(anyInt())).thenReturn(false);
+
+ LockscreenCredential password = newPassword("password");
+ initSpAndSetCredential(PRIMARY_USER_ID, password);
+
+ mService.verifyCredential(password, PRIMARY_USER_ID, 0 /* flags */);
+
+ assertFalse("Escrow data wasn't destroyed", mSpManager.hasAnyEscrowData(PRIMARY_USER_ID));
+ }
+
+ @Test
public void testTokenBasedClearPassword() throws RemoteException {
LockscreenCredential password = newPassword("password");
LockscreenCredential pattern = newPattern("123654");
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
index f9946604ad5d..b842d3a42f26 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
@@ -389,6 +389,44 @@ public final class UserManagerTest {
}
@Test
+ public void testSupervisingProfile() throws Exception {
+ assumeTrue("Device doesn't support supervising profiles ",
+ mUserManager.isUserTypeEnabled(UserManager.USER_TYPE_PROFILE_SUPERVISING));
+
+ final UserTypeDetails userTypeDetails =
+ UserTypeFactory.getUserTypes().get(UserManager.USER_TYPE_PROFILE_SUPERVISING);
+ assertWithMessage("No supervising user type on device").that(userTypeDetails).isNotNull();
+
+
+ // Create supervising profile if it doesn't exist
+ UserInfo supervisingUser = getSupervisingProfile();
+ if (supervisingUser == null) {
+ supervisingUser = createUser("Supervising",
+ UserManager.USER_TYPE_PROFILE_SUPERVISING, /*flags*/ 0);
+ }
+ assertWithMessage("Couldn't create supervising profile").that(supervisingUser).isNotNull();
+ UserHandle supervisingHandle = supervisingUser.getUserHandle();
+
+ // Test that only one supervising profile can be created
+ final UserInfo secondSupervisingProfile =
+ createUser("Supervising", UserManager.USER_TYPE_PROFILE_SUPERVISING,
+ /*flags*/ 0);
+ assertThat(secondSupervisingProfile).isNull();
+
+ // Verify that the supervising profile doesn't have a parent
+ assertThat(mUserManager.getProfileParent(supervisingHandle.getIdentifier())).isNull();
+
+ // Make sure that the supervising profile can be started in the background, and that it
+ // is visible
+ final boolean isStarted = mActivityManager.startProfile(supervisingHandle);
+ assertWithMessage("Unable to start supervising profile").that(isStarted).isTrue();
+ final UserManager umSupervising = (UserManager) mContext.createPackageContextAsUser(
+ "android", 0, supervisingHandle).getSystemService(Context.USER_SERVICE);
+ assertWithMessage("Supervising profile not visible").that(
+ umSupervising.isUserVisible()).isTrue();
+ }
+
+ @Test
public void testGetProfileAccessibilityString_throwsExceptionForNonProfileUser() {
UserInfo user1 = createUser("Guest 1", UserInfo.FLAG_GUEST);
assertThat(user1).isNotNull();
@@ -2198,4 +2236,13 @@ public final class UserManagerTest {
assertEquals(actual.getLevel(), expected.getLevel());
}
+ @Nullable
+ private UserInfo getSupervisingProfile() {
+ for (UserInfo user : mUserManager.getUsers()) {
+ if (user.userType.equals(UserManager.USER_TYPE_PROFILE_SUPERVISING)) {
+ return user;
+ }
+ }
+ return null;
+ }
}
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
index 04335df8c454..2baf0c141c89 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
@@ -247,6 +247,7 @@ public class VibrationThreadTest {
assertThat(mVibratorProviders.get(VIBRATOR_ID).getAmplitudes()).isEmpty();
}
+ @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING)
@Test
public void vibrate_singleVibratorWaveform_runsVibrationAndChangesAmplitudes() {
mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
@@ -269,7 +270,10 @@ public class VibrationThreadTest {
}
@Test
- @EnableFlags(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
+ @EnableFlags({
+ Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED,
+ Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING,
+ })
public void vibrate_singleWaveformWithAdaptiveHapticsScaling_scalesAmplitudesProperly() {
// No user settings scale.
setUserSetting(Settings.System.RING_VIBRATION_INTENSITY,
@@ -296,7 +300,10 @@ public class VibrationThreadTest {
}
@Test
- @EnableFlags(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
+ @EnableFlags({
+ Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED,
+ Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING,
+ })
public void vibrate_withVibrationParamsRequestStalling_timeoutRequestAndApplyNoScaling() {
// No user settings scale.
setUserSetting(Settings.System.RING_VIBRATION_INTENSITY,
@@ -357,6 +364,7 @@ public class VibrationThreadTest {
}
}
+ @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING)
@Test
public void vibrate_singleVibratorRepeatingShortAlwaysOnWaveform_turnsVibratorOnForLonger()
throws Exception {
@@ -380,6 +388,7 @@ public class VibrationThreadTest {
.containsExactly(expectedOneShot(5000)).inOrder();
}
+ @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING)
@Test
public void vibrate_singleVibratorPatternWithZeroDurationSteps_skipsZeroDurationSteps() {
mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
@@ -399,6 +408,7 @@ public class VibrationThreadTest {
.containsExactlyElementsIn(expectedOneShots(100L, 150L)).inOrder();
}
+ @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING)
@Test
public void vibrate_singleVibratorPatternWithZeroDurationAndAmplitude_skipsZeroDurationSteps() {
mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
@@ -537,6 +547,7 @@ public class VibrationThreadTest {
assertThat(fakeVibrator.getEffectSegments(vibration.id)).hasSize(10);
}
+ @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING)
@Test
public void vibrate_singleVibratorRepeatingLongAlwaysOnWaveform_turnsVibratorOnForACycle()
throws Exception {
@@ -700,6 +711,7 @@ public class VibrationThreadTest {
.containsExactly(expectedPrebaked(VibrationEffect.EFFECT_THUD)).inOrder();
}
+ @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING)
@Test
public void vibrate_singleVibratorPrebakedAndUnsupportedEffectWithFallback_runsFallback() {
mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
@@ -1247,6 +1259,7 @@ public class VibrationThreadTest {
.containsExactly(expected).inOrder();
}
+ @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING)
@Test
public void vibrate_multipleStereo_runsVibrationOnRightVibrators() {
mockVibrators(1, 2, 3, 4);
@@ -1479,6 +1492,7 @@ public class VibrationThreadTest {
assertThat(mVibratorProviders.get(1).getAmplitudes()).isEmpty();
}
+ @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING)
@Test
public void vibrate_multipleWaveforms_playsWaveformsInParallel() throws Exception {
mockVibrators(1, 2, 3);
@@ -1544,8 +1558,8 @@ public class VibrationThreadTest {
VibrationEffect.createOneShot(
expectedDuration, VibrationEffect.DEFAULT_AMPLITUDE)));
- startThreadAndDispatcher(vibration);
long startTime = SystemClock.elapsedRealtime();
+ startThreadAndDispatcher(vibration);
vibration.waitForEnd();
long vibrationEndTime = SystemClock.elapsedRealtime();
@@ -1554,15 +1568,13 @@ public class VibrationThreadTest {
long completionTime = SystemClock.elapsedRealtime();
verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong());
- // Vibration ends after duration, thread completed after ramp down
- assertThat(vibrationEndTime - startTime).isAtLeast(expectedDuration);
+ // Vibration ends before ramp down, thread completed after ramp down
assertThat(vibrationEndTime - startTime).isLessThan(expectedDuration + rampDownDuration);
assertThat(completionTime - startTime).isAtLeast(expectedDuration + rampDownDuration);
}
@Test
- public void vibrate_withVibratorCallbackDelayShorterThanTimeout_vibrationFinishedAfterDelay()
- throws Exception {
+ public void vibrate_withVibratorCallbackDelayShorterThanTimeout_vibrationFinishedAfterDelay() {
long expectedDuration = 10;
long callbackDelay = VibrationStepConductor.CALLBACKS_EXTRA_TIMEOUT / 2;
@@ -1577,10 +1589,8 @@ public class VibrationThreadTest {
long startTime = SystemClock.elapsedRealtime();
startThreadAndDispatcher(vibration);
- vibration.waitForEnd();
- long vibrationEndTime = SystemClock.elapsedRealtime();
-
waitForCompletion(TEST_TIMEOUT_MILLIS);
+ long vibrationEndTime = SystemClock.elapsedRealtime();
verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong());
assertThat(vibrationEndTime - startTime).isAtLeast(expectedDuration + callbackDelay);
@@ -1588,8 +1598,7 @@ public class VibrationThreadTest {
@LargeTest
@Test
- public void vibrate_withVibratorCallbackDelayLongerThanTimeout_vibrationFinishedAfterTimeout()
- throws Exception {
+ public void vibrate_withVibratorCallbackDelayLongerThanTimeout_vibrationFinishedAfterTimeout() {
long expectedDuration = 10;
long callbackTimeout = VibrationStepConductor.CALLBACKS_EXTRA_TIMEOUT;
long callbackDelay = callbackTimeout * 2;
@@ -1602,21 +1611,17 @@ public class VibrationThreadTest {
VibrationEffect.createOneShot(
expectedDuration, VibrationEffect.DEFAULT_AMPLITUDE)));
- startThreadAndDispatcher(vibration);
long startTime = SystemClock.elapsedRealtime();
-
- vibration.waitForEnd();
- long vibrationEndTime = SystemClock.elapsedRealtime();
+ startThreadAndDispatcher(vibration);
waitForCompletion(callbackDelay + TEST_TIMEOUT_MILLIS);
- long completionTime = SystemClock.elapsedRealtime();
+ long vibrationEndTime = SystemClock.elapsedRealtime();
verify(mControllerCallbacks, never())
.onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong());
// Vibration ends and thread completes after timeout, before the HAL callback
assertThat(vibrationEndTime - startTime).isAtLeast(expectedDuration + callbackTimeout);
assertThat(vibrationEndTime - startTime).isLessThan(expectedDuration + callbackDelay);
- assertThat(completionTime - startTime).isLessThan(expectedDuration + callbackDelay);
}
@LargeTest
@@ -1637,8 +1642,8 @@ public class VibrationThreadTest {
Arrays.fill(amplitudes, VibrationEffect.DEFAULT_AMPLITUDE);
VibrationEffect effect = VibrationEffect.createWaveform(timings, amplitudes, -1);
- startThreadAndDispatcher(effect);
long startTime = SystemClock.elapsedRealtime();
+ startThreadAndDispatcher(effect);
waitForCompletion(totalDuration + TEST_TIMEOUT_MILLIS);
long delay = Math.abs(SystemClock.elapsedRealtime() - startTime - totalDuration);
@@ -1806,6 +1811,7 @@ public class VibrationThreadTest {
assertThat(mControllers.get(VIBRATOR_ID).isVibrating()).isFalse();
}
+ @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING)
@Test
public void vibrate_waveformWithRampDown_addsRampDownAfterVibrationCompleted() {
when(mVibrationConfigMock.getRampDownDurationMs()).thenReturn(15);
@@ -1833,6 +1839,7 @@ public class VibrationThreadTest {
}
}
+ @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING)
@Test
public void vibrate_waveformWithRampDown_triggersCallbackWhenOriginalVibrationEnds()
throws Exception {
@@ -1863,6 +1870,7 @@ public class VibrationThreadTest {
verify(mManagerHooks).onVibrationThreadReleased(vibration.id);
}
+ @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING)
@Test
public void vibrate_waveformCancelledWithRampDown_addsRampDownAfterVibrationCancelled()
throws Exception {
@@ -2057,6 +2065,7 @@ public class VibrationThreadTest {
.containsExactly(expectedPrebaked(EFFECT_CLICK)).inOrder();
}
+ @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING)
@Test
public void vibrate_multipleVibratorsSequentialInSession_runsInOrderWithoutDelaysAndNoOffs() {
mockVibrators(1, 2, 3);
diff --git a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
index 3ca019728c2b..fcdf88f16550 100644
--- a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
@@ -605,29 +605,6 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase {
}
@Test
- public void testKeyGestureAccessibilityShortcutChord() {
- Assert.assertTrue(
- sendKeyGestureEventStart(
- KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD));
- mPhoneWindowManager.moveTimeForward(5000);
- Assert.assertTrue(
- sendKeyGestureEventCancel(
- KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD));
- mPhoneWindowManager.assertAccessibilityKeychordCalled();
- }
-
- @Test
- public void testKeyGestureAccessibilityShortcutChordCancelled() {
- Assert.assertTrue(
- sendKeyGestureEventStart(
- KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD));
- Assert.assertTrue(
- sendKeyGestureEventCancel(
- KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD));
- mPhoneWindowManager.assertAccessibilityKeychordNotCalled();
- }
-
- @Test
public void testKeyGestureRingerToggleChord() {
mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_MUTE);
Assert.assertTrue(
@@ -670,29 +647,6 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase {
}
@Test
- public void testKeyGestureAccessibilityTvShortcutChord() {
- Assert.assertTrue(
- sendKeyGestureEventStart(
- KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD));
- mPhoneWindowManager.moveTimeForward(5000);
- Assert.assertTrue(
- sendKeyGestureEventCancel(
- KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD));
- mPhoneWindowManager.assertAccessibilityKeychordCalled();
- }
-
- @Test
- public void testKeyGestureAccessibilityTvShortcutChordCancelled() {
- Assert.assertTrue(
- sendKeyGestureEventStart(
- KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD));
- Assert.assertTrue(
- sendKeyGestureEventCancel(
- KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD));
- mPhoneWindowManager.assertAccessibilityKeychordNotCalled();
- }
-
- @Test
public void testKeyGestureTvTriggerBugReport() {
Assert.assertTrue(
sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT));
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index f88492477487..e56fd3c6272d 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -750,11 +750,6 @@ class TestPhoneWindowManager {
verify(mAccessibilityShortcutController).performAccessibilityShortcut();
}
- void assertAccessibilityKeychordNotCalled() {
- mTestLooper.dispatchAll();
- verify(mAccessibilityShortcutController, never()).performAccessibilityShortcut();
- }
-
void assertCloseAllDialogs() {
verify(mContext).closeSystemDialogs();
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java
index 948371f74a9c..ad706e879b72 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java
@@ -63,11 +63,23 @@ public class ActivitySnapshotControllerTests extends TaskSnapshotPersisterTestBa
super(0.8f /* highResScale */, 0.5f /* lowResScale */);
}
+ private class TestActivitySnapshotController extends ActivitySnapshotController {
+ TestActivitySnapshotController(WindowManagerService service,
+ SnapshotPersistQueue persistQueue) {
+ super(service, persistQueue);
+ }
+ @Override
+ BaseAppSnapshotPersister.PersistInfoProvider createPersistInfoProvider(
+ WindowManagerService service) {
+ return mPersister.mPersistInfoProvider;
+ }
+ }
@Override
@Before
public void setUp() {
super.setUp();
- mActivitySnapshotController = new ActivitySnapshotController(mWm, mSnapshotPersistQueue);
+ mActivitySnapshotController = new TestActivitySnapshotController(
+ mWm, mSnapshotPersistQueue);
spyOn(mActivitySnapshotController);
doReturn(false).when(mActivitySnapshotController).shouldDisableSnapshots();
mActivitySnapshotController.resetTmpFields();
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index e3e9cc426bb3..08b0077c49b3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -797,7 +797,7 @@ public class ActivityStarterTests extends WindowTestsBase {
// Create adjacent tasks and put one activity under it
final Task parent = new TaskBuilder(mSupervisor).build();
final Task adjacentParent = new TaskBuilder(mSupervisor).build();
- parent.setAdjacentTaskFragment(adjacentParent);
+ parent.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(parent, adjacentParent));
final ActivityRecord activity = new ActivityBuilder(mAtm)
.setParentTask(parent)
.setCreateTask(true).build();
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
index a9be47d71213..86d901b640ff 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
@@ -488,14 +488,13 @@ public class ActivityTaskSupervisorTests extends WindowTestsBase {
WINDOWING_MODE_MULTI_WINDOW, /* opaque */ true, /* filling */ false);
final TaskFragment tf2 = createChildTaskFragment(/* parent */ rootTask,
WINDOWING_MODE_MULTI_WINDOW, /* opaque */ true, /* filling */ false);
- tf1.setAdjacentTaskFragment(tf2);
+ tf1.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(tf1, tf2));
assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(rootTask)).isTrue();
}
@Test
- @EnableFlags({Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
- Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS})
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
public void testOpaque_rootTask_nonFillingOpaqueAdjacentChildren_multipleAdjacent_isOpaque() {
final Task rootTask = new TaskBuilder(mSupervisor).setOnTop(true).build();
final TaskFragment tf1 = createChildTaskFragment(/* parent */ rootTask,
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
index 8fe08553db95..cb98b9a490d8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
@@ -243,6 +243,11 @@ class AppCompatActivityRobot {
.getAspectRatioOverrides()).getUserMinAspectRatio();
}
+ void setShouldRefreshActivityForCameraCompat(boolean enabled) {
+ doReturn(enabled).when(mActivityStack.top().mAppCompatController.getCameraOverrides())
+ .shouldRefreshActivityForCameraCompat();
+ }
+
void setIgnoreOrientationRequest(boolean enabled) {
mDisplayContent.setIgnoreOrientationRequest(enabled);
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java
index 05f6ed644632..7ef85262dfc2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java
@@ -64,6 +64,19 @@ class AppCompatConfigurationRobot {
.isCameraCompatTreatmentEnabledAtBuildTime();
}
+ void setCameraCompatAspectRatio(float aspectRatio) {
+ doReturn(aspectRatio).when(mAppCompatConfiguration).getCameraCompatAspectRatio();
+ }
+
+ void enableCameraCompatRefresh(boolean enabled) {
+ doReturn(enabled).when(mAppCompatConfiguration).isCameraCompatRefreshEnabled();
+ }
+
+ void enableCameraCompatRefreshCycleThroughStop(boolean enabled) {
+ doReturn(enabled).when(mAppCompatConfiguration)
+ .isCameraCompatRefreshCycleThroughStopEnabled();
+ }
+
void enableUserAppAspectRatioFullscreen(boolean enabled) {
doReturn(enabled).when(mAppCompatConfiguration).isUserAppAspectRatioFullscreenEnabled();
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
index bdee3c323549..dd3e9fcbbdaf 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
@@ -343,8 +343,7 @@ public class BackNavigationControllerTests extends WindowTestsBase {
// Adjacent + no companion => unable to predict
// TF1 | TF2
- tf1.setAdjacentTaskFragment(tf2);
- tf2.setAdjacentTaskFragment(tf1);
+ tf1.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(tf1, tf2));
predictable = BackNavigationController.getAnimatablePrevActivities(task, topAr,
outPrevActivities);
assertTrue(outPrevActivities.isEmpty());
@@ -393,8 +392,7 @@ public class BackNavigationControllerTests extends WindowTestsBase {
// Adjacent => predict for previous activity.
// TF2 | TF3
// TF1
- tf2.setAdjacentTaskFragment(tf3);
- tf3.setAdjacentTaskFragment(tf2);
+ tf2.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(tf2, tf3));
predictable = BackNavigationController.getAnimatablePrevActivities(task, topAr,
outPrevActivities);
assertTrue(outPrevActivities.contains(prevAr));
@@ -657,8 +655,7 @@ public class BackNavigationControllerTests extends WindowTestsBase {
final TaskFragment secondaryTf = createTaskFragmentWithEmbeddedActivity(task, organizer);
final ActivityRecord primaryActivity = primaryTf.getTopMostActivity();
final ActivityRecord secondaryActivity = secondaryTf.getTopMostActivity();
- primaryTf.setAdjacentTaskFragment(secondaryTf);
- secondaryTf.setAdjacentTaskFragment(primaryTf);
+ primaryTf.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(primaryTf, secondaryTf));
final WindowState primaryWindow = mock(WindowState.class);
final WindowState secondaryWindow = mock(WindowState.class);
diff --git a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
index f5bec04a98d5..6f959812d742 100644
--- a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
@@ -21,13 +21,13 @@ import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_LANDSCAPE_
import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE;
import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE;
import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_PORTRAIT;
+import static android.app.CameraCompatTaskInfo.FreeformCameraCompatMode;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE;
import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_SIMULATE_REQUESTED_ORIENTATION;
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT;
-import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_FULL_USER;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
@@ -40,18 +40,15 @@ import static android.view.Surface.ROTATION_90;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
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.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING;
import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
@@ -59,13 +56,11 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.annotation.NonNull;
-import android.app.CameraCompatTaskInfo;
import android.app.IApplicationThread;
import android.app.WindowConfiguration.WindowingMode;
import android.app.servertransaction.RefreshCallbackItem;
import android.app.servertransaction.ResumeActivityItem;
import android.compat.testing.PlatformCompatChangeRule;
-import android.content.ComponentName;
import android.content.pm.ActivityInfo.ScreenOrientation;
import android.content.res.CompatibilityInfo;
import android.content.res.Configuration;
@@ -73,17 +68,16 @@ import android.content.res.Configuration.Orientation;
import android.graphics.Rect;
import android.hardware.camera2.CameraManager;
import android.os.Handler;
+import android.os.RemoteException;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
-import android.view.DisplayInfo;
import android.view.Surface;
import androidx.test.filters.SmallTest;
import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
-import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
@@ -91,6 +85,7 @@ import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import java.util.concurrent.Executor;
+import java.util.function.Consumer;
/**
* Tests for {@link CameraCompatFreeformPolicy}.
@@ -109,30 +104,18 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
private static final String TEST_PACKAGE_1 = "com.android.frameworks.wmtests";
private static final String TEST_PACKAGE_2 = "com.test.package.two";
private static final String CAMERA_ID_1 = "camera-1";
- private AppCompatConfiguration mAppCompatConfiguration;
-
- private CameraManager.AvailabilityCallback mCameraAvailabilityCallback;
- private CameraCompatFreeformPolicy mCameraCompatFreeformPolicy;
- private ActivityRecord mActivity;
-
- // TODO(b/384465100): use a robot structure.
- @Before
- public void setUp() throws Exception {
- setupAppCompatConfiguration();
- setupCameraManager();
- setupHandler();
- doReturn(true).when(() -> DesktopModeHelper.canEnterDesktopMode(any()));
- }
@Test
@DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testFeatureDisabled_cameraCompatFreeformPolicyNotCreated() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertNull(mCameraCompatFreeformPolicy);
+ robot.checkCameraCompatPolicyNotCreated();
+ });
}
@Test
@@ -140,31 +123,37 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT})
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_SIMULATE_REQUESTED_ORIENTATION})
public void testIsCameraRunningAndWindowingModeEligible_disabledViaOverride_returnsFalse() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertFalse(mCameraCompatFreeformPolicy.isCameraRunningAndWindowingModeEligible(mActivity));
+ robot.checkIsCameraRunningAndWindowingModeEligible(false);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testIsCameraRunningAndWindowingModeEligible_cameraNotRunning_returnsFalse() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- assertFalse(mCameraCompatFreeformPolicy.isCameraRunningAndWindowingModeEligible(mActivity));
+ robot.checkIsCameraRunningAndWindowingModeEligible(false);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testIsCameraRunningAndWindowingModeEligible_notFreeformWindowing_returnsFalse() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertFalse(mCameraCompatFreeformPolicy.isCameraRunningAndWindowingModeEligible(mActivity));
+ robot.checkIsCameraRunningAndWindowingModeEligible(false);
+ });
}
@Test
@@ -172,64 +161,76 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
@DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testIsCameraRunningAndWindowingModeEligible_optInFreeformCameraRunning_true() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertTrue(mCameraCompatFreeformPolicy.isCameraRunningAndWindowingModeEligible(mActivity));
+ robot.checkIsCameraRunningAndWindowingModeEligible(true);
+ });
}
@Test
@EnableFlags({FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING,
FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT})
public void testIsCameraRunningAndWindowingModeEligible_freeformCameraRunning_true() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertTrue(mCameraCompatFreeformPolicy.isCameraRunningAndWindowingModeEligible(mActivity));
+ robot.checkIsCameraRunningAndWindowingModeEligible(true);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT)
public void testIsFreeformLetterboxingForCameraAllowed_optInMechanism_notOptedIn_retFalse() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertFalse(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity));
+ robot.checkIsFreeformLetterboxingForCameraAllowed(false);
+ });
}
@Test
@EnableFlags({FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING,
FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT})
public void testIsFreeformLetterboxingForCameraAllowed_notOptedOut_returnsTrue() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertTrue(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity));
+ robot.checkIsFreeformLetterboxingForCameraAllowed(true);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testIsFreeformLetterboxingForCameraAllowed_cameraNotRunning_returnsFalse() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- assertFalse(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity));
+ robot.checkIsFreeformLetterboxingForCameraAllowed(false);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testIsFreeformLetterboxingForCameraAllowed_notFreeformWindowing_returnsFalse() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertFalse(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity));
+ robot.checkIsFreeformLetterboxingForCameraAllowed(false);
+ });
}
@Test
@@ -237,519 +238,603 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
@DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testIsFreeformLetterboxingForCameraAllowed_optInFreeformCameraRunning_true() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertTrue(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity));
+ robot.checkIsFreeformLetterboxingForCameraAllowed(true);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testFullscreen_doesNotActivateCameraCompatMode() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN);
- doReturn(false).when(mActivity).inFreeformWindowingMode();
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN);
+ robot.setInFreeformWindowingMode(false);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertNotInCameraCompatMode();
+ robot.assertNotInCameraCompatMode();
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testOrientationUnspecified_doesNotActivateCameraCompatMode() {
- configureActivity(SCREEN_ORIENTATION_UNSPECIFIED);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_UNSPECIFIED);
- assertNotInCameraCompatMode();
+ robot.assertNotInCameraCompatMode();
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testNoCameraConnection_doesNotActivateCameraCompatMode() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- assertNotInCameraCompatMode();
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+ robot.assertNotInCameraCompatMode();
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
- public void testCameraConnected_deviceInPortrait_portraitCameraCompatMode() throws Exception {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- setDisplayRotation(ROTATION_0);
+ public void testCameraConnected_deviceInPortrait_portraitCameraCompatMode() {
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ robot.activity().rotateDisplayForTopActivity(ROTATION_0);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_PORTRAIT);
- assertActivityRefreshRequested(/* refreshRequested */ false);
+ robot.assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_PORTRAIT);
+ robot.assertActivityRefreshRequested(/* refreshRequested */ false);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
- public void testCameraConnected_deviceInLandscape_portraitCameraCompatMode() throws Exception {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- setDisplayRotation(ROTATION_270);
+ public void testCameraConnected_deviceInLandscape_portraitCameraCompatMode() {
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ robot.activity().rotateDisplayForTopActivity(ROTATION_270);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE);
- assertActivityRefreshRequested(/* refreshRequested */ false);
+ robot.assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE);
+ robot.assertActivityRefreshRequested(/* refreshRequested */ false);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
- public void testCameraConnected_deviceInPortrait_landscapeCameraCompatMode() throws Exception {
- configureActivity(SCREEN_ORIENTATION_LANDSCAPE);
- setDisplayRotation(ROTATION_0);
+ public void testCameraConnected_deviceInPortrait_landscapeCameraCompatMode() {
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_LANDSCAPE);
+ robot.activity().rotateDisplayForTopActivity(ROTATION_0);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_PORTRAIT);
- assertActivityRefreshRequested(/* refreshRequested */ false);
+ robot.assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_PORTRAIT);
+ robot.assertActivityRefreshRequested(/* refreshRequested */ false);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
- public void testCameraConnected_deviceInLandscape_landscapeCameraCompatMode() throws Exception {
- configureActivity(SCREEN_ORIENTATION_LANDSCAPE);
- setDisplayRotation(ROTATION_270);
+ public void testCameraConnected_deviceInLandscape_landscapeCameraCompatMode() {
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_LANDSCAPE);
+ robot.activity().rotateDisplayForTopActivity(ROTATION_270);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_LANDSCAPE);
- assertActivityRefreshRequested(/* refreshRequested */ false);
+ robot.assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_LANDSCAPE);
+ robot.assertActivityRefreshRequested(/* refreshRequested */ false);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
- public void testCameraReconnected_cameraCompatModeAndRefresh() throws Exception {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- setDisplayRotation(ROTATION_270);
+ public void testCameraReconnected_cameraCompatModeAndRefresh() {
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ robot.activity().rotateDisplayForTopActivity(ROTATION_270);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- callOnActivityConfigurationChanging(mActivity, /* letterboxNew= */ true,
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.callOnActivityConfigurationChanging(/* letterboxNew= */ true,
/* lastLetterbox= */ false);
- assertActivityRefreshRequested(/* refreshRequested */ true);
- onCameraClosed(CAMERA_ID_1);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- // Activity is letterboxed from the previous configuration change.
- callOnActivityConfigurationChanging(mActivity, /* letterboxNew= */ true,
- /* lastLetterbox= */ true);
+ robot.assertActivityRefreshRequested(/* refreshRequested */ true);
+ robot.onCameraClosed(CAMERA_ID_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ // Activity is letterboxed from the previous configuration change.
+ robot.callOnActivityConfigurationChanging(/* letterboxNew= */ true,
+ /* lastLetterbox= */ true);
- assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE);
- assertActivityRefreshRequested(/* refreshRequested */ true);
+ robot.assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE);
+ robot.assertActivityRefreshRequested(/* refreshRequested */ true);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testCameraOpenedForDifferentPackage_notInCameraCompatMode() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_2);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_2);
- assertNotInCameraCompatMode();
+ robot.assertNotInCameraCompatMode();
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT)
public void testShouldApplyCameraCompatFreeformTreatment_overrideNotEnabled_returnsFalse() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertFalse(mCameraCompatFreeformPolicy.isTreatmentEnabledForActivity(mActivity,
- /* checkOrientation */ true));
+ robot.checkIsCameraCompatTreatmentActiveForTopActivity(false);
+ });
}
@Test
@EnableFlags({FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING,
FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT})
public void testShouldApplyCameraCompatFreeformTreatment_notOptedOut_returnsTrue() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertTrue(mCameraCompatFreeformPolicy.isTreatmentEnabledForActivity(mActivity,
- /* checkOrientation */ true));
+ robot.checkIsCameraCompatTreatmentActiveForTopActivity(true);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges(OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT)
public void testShouldApplyCameraCompatFreeformTreatment_enabledByOverride_returnsTrue() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertTrue(mActivity.info
- .isChangeEnabled(OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT));
- assertTrue(mCameraCompatFreeformPolicy.isTreatmentEnabledForActivity(mActivity,
- /* checkOrientation */ true));
+ robot.checkIsCameraCompatTreatmentActiveForTopActivity(true);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testShouldRefreshActivity_appBoundsChanged_returnsTrue() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- Configuration oldConfiguration = createConfiguration(/* letterbox= */ false);
- Configuration newConfiguration = createConfiguration(/* letterbox= */ true);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- assertTrue(mCameraCompatFreeformPolicy.shouldRefreshActivity(mActivity, newConfiguration,
- oldConfiguration));
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ robot.checkShouldRefreshActivity(/* expected= */ true,
+ robot.createConfiguration(/* letterbox= */ true, /* rotation= */ 0),
+ robot.createConfiguration(/* letterbox= */ false, /* rotation= */ 0));
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testShouldRefreshActivity_displayRotationChanged_returnsTrue() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- Configuration oldConfiguration = createConfiguration(/* letterbox= */ true);
- Configuration newConfiguration = createConfiguration(/* letterbox= */ true);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- oldConfiguration.windowConfiguration.setDisplayRotation(0);
- newConfiguration.windowConfiguration.setDisplayRotation(90);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertTrue(mCameraCompatFreeformPolicy.shouldRefreshActivity(mActivity, newConfiguration,
- oldConfiguration));
+ robot.checkShouldRefreshActivity(/* expected= */ true,
+ robot.createConfiguration(/* letterbox= */ true, /* rotation= */ 90),
+ robot.createConfiguration(/* letterbox= */ true, /* rotation= */ 0));
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testShouldRefreshActivity_appBoundsNorDisplayChanged_returnsFalse() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- Configuration oldConfiguration = createConfiguration(/* letterbox= */ true);
- Configuration newConfiguration = createConfiguration(/* letterbox= */ true);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- oldConfiguration.windowConfiguration.setDisplayRotation(0);
- newConfiguration.windowConfiguration.setDisplayRotation(0);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertFalse(mCameraCompatFreeformPolicy.shouldRefreshActivity(mActivity, newConfiguration,
- oldConfiguration));
+ robot.checkShouldRefreshActivity(/* expected= */ false,
+ robot.createConfiguration(/* letterbox= */ true, /* rotation= */ 0),
+ robot.createConfiguration(/* letterbox= */ true, /* rotation= */ 0));
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
- public void testOnActivityConfigurationChanging_refreshDisabledViaFlag_noRefresh()
- throws Exception {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
-
- doReturn(false).when(mActivity.mAppCompatController.getCameraOverrides())
- .shouldRefreshActivityForCameraCompat();
+ public void testOnActivityConfigurationChanging_refreshDisabledViaFlag_noRefresh() {
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ robot.activity().setShouldRefreshActivityForCameraCompat(false);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- callOnActivityConfigurationChanging(mActivity);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.callOnActivityConfigurationChanging();
- assertActivityRefreshRequested(/* refreshRequested */ false);
+ robot.assertActivityRefreshRequested(/* refreshRequested */ false);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
- public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() throws Exception {
- when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
- .thenReturn(false);
+ public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() {
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ robot.conf().enableCameraCompatRefreshCycleThroughStop(false);
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.callOnActivityConfigurationChanging();
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- callOnActivityConfigurationChanging(mActivity);
-
- assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false);
+ robot.assertActivityRefreshRequested(/* refreshRequested */ true,
+ /* cycleThroughStop */ false);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
- public void testOnActivityConfigurationChanging_cycleThroughStopDisabledForApp()
- throws Exception {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- doReturn(true).when(mActivity.mAppCompatController.getCameraOverrides())
- .shouldRefreshActivityViaPauseForCameraCompat();
+ public void testOnActivityConfigurationChanging_cycleThroughStopDisabledForApp() {
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ robot.setShouldRefreshActivityViaPause(true);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- callOnActivityConfigurationChanging(mActivity);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.callOnActivityConfigurationChanging();
- assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false);
+ robot.assertActivityRefreshRequested(/* refreshRequested */ true,
+ /* cycleThroughStop */ false);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testGetCameraCompatAspectRatio_activityNotInCameraCompat_returnsDefaultAspRatio() {
- configureActivity(SCREEN_ORIENTATION_FULL_USER);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_FULL_USER);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- callOnActivityConfigurationChanging(mActivity);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.callOnActivityConfigurationChanging();
- assertEquals(MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO,
- mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(mActivity),
- /* delta= */ 0.001);
+ robot.checkCameraCompatAspectRatioEquals(MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testGetCameraCompatAspectRatio_activityInCameraCompat_returnsConfigAspectRatio() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- final float configAspectRatio = 1.5f;
- mWm.mAppCompatConfiguration.setCameraCompatAspectRatio(configAspectRatio);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ final float configAspectRatio = 1.5f;
+ robot.conf().setCameraCompatAspectRatio(configAspectRatio);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- callOnActivityConfigurationChanging(mActivity);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.callOnActivityConfigurationChanging();
- assertEquals(configAspectRatio,
- mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(mActivity),
- /* delta= */ 0.001);
+ robot.checkCameraCompatAspectRatioEquals(configAspectRatio);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testGetCameraCompatAspectRatio_inCameraCompatPerAppOverride_returnDefAspectRatio() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- final float configAspectRatio = 1.5f;
- mWm.mAppCompatConfiguration.setCameraCompatAspectRatio(configAspectRatio);
- doReturn(true).when(mActivity.mAppCompatController.getCameraOverrides())
- .isOverrideMinAspectRatioForCameraEnabled();
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ robot.conf().setCameraCompatAspectRatio(1.5f);
+ robot.setOverrideMinAspectRatioEnabled(true);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- callOnActivityConfigurationChanging(mActivity);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.callOnActivityConfigurationChanging();
- assertEquals(MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO,
- mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(mActivity),
- /* delta= */ 0.001);
+ robot.checkCameraCompatAspectRatioEquals(MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
- public void testOnCameraOpened_portraitActivity_sandboxesDisplayRotationAndUpdatesApp() throws
- Exception {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- setDisplayRotation(ROTATION_270);
+ public void testOnCameraOpened_portraitActivity_sandboxesDisplayRotationAndUpdatesApp() {
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ robot.activity().rotateDisplayForTopActivity(ROTATION_270);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- // This is a portrait rotation for a device with portrait natural orientation (most common,
- // currently the only one supported).
- assertCompatibilityInfoSentWithDisplayRotation(ROTATION_0);
+ // This is a portrait rotation for a device with portrait natural orientation (most
+ // common, currently the only one supported).
+ robot.assertCompatibilityInfoSentWithDisplayRotation(ROTATION_0);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
- public void testOnCameraOpened_landscapeActivity_sandboxesDisplayRotationAndUpdatesApp() throws
- Exception {
- configureActivity(SCREEN_ORIENTATION_LANDSCAPE);
- setDisplayRotation(ROTATION_0);
-
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
-
- // This is a landscape rotation for a device with portrait natural orientation (most common,
- // currently the only one supported).
- assertCompatibilityInfoSentWithDisplayRotation(ROTATION_90);
- }
-
- private void setupAppCompatConfiguration() {
- mAppCompatConfiguration = mDisplayContent.mWmService.mAppCompatConfiguration;
- spyOn(mAppCompatConfiguration);
- when(mAppCompatConfiguration.isCameraCompatTreatmentEnabled()).thenReturn(true);
- when(mAppCompatConfiguration.isCameraCompatTreatmentEnabledAtBuildTime()).thenReturn(true);
- when(mAppCompatConfiguration.isCameraCompatRefreshEnabled()).thenReturn(true);
- when(mAppCompatConfiguration.isCameraCompatSplitScreenAspectRatioEnabled())
- .thenReturn(false);
- when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
- .thenReturn(true);
- }
-
- private void setupCameraManager() {
- final CameraManager mockCameraManager = mock(CameraManager.class);
- doAnswer(invocation -> {
- mCameraAvailabilityCallback = invocation.getArgument(1);
- return null;
- }).when(mockCameraManager).registerAvailabilityCallback(
- any(Executor.class), any(CameraManager.AvailabilityCallback.class));
-
- when(mContext.getSystemService(CameraManager.class)).thenReturn(mockCameraManager);
- }
-
- private void setupHandler() {
- final Handler handler = mDisplayContent.mWmService.mH;
- spyOn(handler);
-
- when(handler.postDelayed(any(Runnable.class), anyLong())).thenAnswer(
- invocation -> {
- ((Runnable) invocation.getArgument(0)).run();
- return null;
- });
- }
-
- private void configureActivity(@ScreenOrientation int activityOrientation) {
- configureActivity(activityOrientation, WINDOWING_MODE_FREEFORM);
- }
-
- private void configureActivity(@ScreenOrientation int activityOrientation,
- @WindowingMode int windowingMode) {
- configureActivityAndDisplay(activityOrientation, ORIENTATION_PORTRAIT, windowingMode);
- }
-
- private void configureActivityAndDisplay(@ScreenOrientation int activityOrientation,
- @Orientation int naturalOrientation, @WindowingMode int windowingMode) {
- setupDisplayContent(naturalOrientation);
- final Task task = setupTask(windowingMode);
- setupActivity(task, activityOrientation, windowingMode);
- setupMockApplicationThread();
-
- mCameraCompatFreeformPolicy = mDisplayContent.mAppCompatCameraPolicy
- .mCameraCompatFreeformPolicy;
- }
-
- private void setupDisplayContent(@Orientation int naturalOrientation) {
- // Create a new DisplayContent so that the flag values create the camera freeform policy.
- mDisplayContent = new TestDisplayContent.Builder(mAtm, mDisplayContent.getSurfaceWidth(),
- mDisplayContent.getSurfaceHeight()).build();
- mDisplayContent.setIgnoreOrientationRequest(true);
- setDisplayRotation(ROTATION_90);
- doReturn(naturalOrientation).when(mDisplayContent).getNaturalOrientation();
- }
-
- private Task setupTask(@WindowingMode int windowingMode) {
- final TaskDisplayArea tda = mDisplayContent.getDefaultTaskDisplayArea();
- spyOn(tda);
- doReturn(true).when(tda).supportsNonResizableMultiWindow();
-
- final Task task = new TaskBuilder(mSupervisor)
- .setDisplay(mDisplayContent)
- .setWindowingMode(windowingMode)
- .build();
- task.setBounds(0, 0, 1000, 500);
- return task;
- }
-
- private void setupActivity(@NonNull Task task, @ScreenOrientation int activityOrientation,
- @WindowingMode int windowingMode) {
- mActivity = new ActivityBuilder(mAtm)
- // Set the component to be that of the test class in order to enable compat changes
- .setComponent(ComponentName.createRelative(mContext,
- com.android.server.wm.CameraCompatFreeformPolicyTests.class.getName()))
- .setScreenOrientation(activityOrientation)
- .setResizeMode(RESIZE_MODE_RESIZEABLE)
- .setCreateTask(true)
- .setOnTop(true)
- .setTask(task)
- .build();
- mActivity.mAppCompatController.getSizeCompatModePolicy().clearSizeCompatMode();
-
- spyOn(mActivity.mAppCompatController.getCameraOverrides());
- spyOn(mActivity.info);
-
- doReturn(mActivity).when(mDisplayContent).topRunningActivity(anyBoolean());
- doReturn(windowingMode == WINDOWING_MODE_FREEFORM).when(mActivity)
- .inFreeformWindowingMode();
- }
-
- private void onCameraOpened(@NonNull String cameraId, @NonNull String packageName) {
- mCameraAvailabilityCallback.onCameraOpened(cameraId, packageName);
- waitHandlerIdle(mDisplayContent.mWmService.mH);
- }
-
- private void onCameraClosed(@NonNull String cameraId) {
- mCameraAvailabilityCallback.onCameraClosed(cameraId);
- waitHandlerIdle(mDisplayContent.mWmService.mH);
- }
-
- private void assertInCameraCompatMode(@CameraCompatTaskInfo.FreeformCameraCompatMode int mode) {
- assertEquals(mode, mCameraCompatFreeformPolicy.getCameraCompatMode(mActivity));
- }
-
- private void assertNotInCameraCompatMode() {
- assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_NONE);
- }
-
- private void assertActivityRefreshRequested(boolean refreshRequested) throws Exception {
- assertActivityRefreshRequested(refreshRequested, /* cycleThroughStop*/ true);
- }
-
- private void assertActivityRefreshRequested(boolean refreshRequested,
- boolean cycleThroughStop) throws Exception {
- verify(mActivity.mAppCompatController.getCameraOverrides(),
- times(refreshRequested ? 1 : 0)).setIsRefreshRequested(true);
-
- final RefreshCallbackItem refreshCallbackItem =
- new RefreshCallbackItem(mActivity.token, cycleThroughStop ? ON_STOP : ON_PAUSE);
- final ResumeActivityItem resumeActivityItem = new ResumeActivityItem(mActivity.token,
- /* isForward */ false, /* shouldSendCompatFakeFocus */ false);
-
- verify(mActivity.mAtmService.getLifecycleManager(), times(refreshRequested ? 1 : 0))
- .scheduleTransactionItems(mActivity.app.getThread(),
- refreshCallbackItem, resumeActivityItem);
- }
-
- private void callOnActivityConfigurationChanging(ActivityRecord activity) {
- callOnActivityConfigurationChanging(activity, /* letterboxNew= */ true,
- /* lastLetterbox= */false);
- }
-
- private void callOnActivityConfigurationChanging(ActivityRecord activity, boolean letterboxNew,
- boolean lastLetterbox) {
- mDisplayContent.mAppCompatCameraPolicy.mActivityRefresher
- .onActivityConfigurationChanging(activity,
- /* newConfig */ createConfiguration(letterboxNew),
- /* lastReportedConfig */ createConfiguration(lastLetterbox));
- }
-
- private Configuration createConfiguration(boolean letterbox) {
- final Configuration configuration = new Configuration();
- Rect bounds = letterbox ? new Rect(/*left*/ 300, /*top*/ 0, /*right*/ 700, /*bottom*/ 600)
- : new Rect(/*left*/ 0, /*top*/ 0, /*right*/ 1000, /*bottom*/ 600);
- configuration.windowConfiguration.setAppBounds(bounds);
- return configuration;
- }
-
- private void setDisplayRotation(@Surface.Rotation int displayRotation) {
- doAnswer(invocation -> {
- DisplayInfo displayInfo = new DisplayInfo();
- mDisplayContent.getDisplay().getDisplayInfo(displayInfo);
- displayInfo.rotation = displayRotation;
- // Set height so that the natural orientation (rotation is 0) is portrait. This is the
- // case for most standard phones and tablets.
- // TODO(b/365725400): handle landscape natural orientation.
- displayInfo.logicalHeight = displayRotation % 180 == 0 ? 800 : 600;
- displayInfo.logicalWidth = displayRotation % 180 == 0 ? 600 : 800;
- return displayInfo;
- }).when(mDisplayContent.mWmService.mDisplayManagerInternal)
- .getDisplayInfo(anyInt());
- }
-
- private void setupMockApplicationThread() {
- IApplicationThread mockApplicationThread = mock(IApplicationThread.class);
- spyOn(mActivity.app);
- doReturn(mockApplicationThread).when(mActivity.app).getThread();
- }
-
- private void assertCompatibilityInfoSentWithDisplayRotation(@Surface.Rotation int
- expectedRotation) throws Exception {
- final ArgumentCaptor<CompatibilityInfo> compatibilityInfoArgumentCaptor =
- ArgumentCaptor.forClass(CompatibilityInfo.class);
- verify(mActivity.app.getThread()).updatePackageCompatibilityInfo(eq(mActivity.packageName),
- compatibilityInfoArgumentCaptor.capture());
-
- final CompatibilityInfo compatInfo = compatibilityInfoArgumentCaptor.getValue();
- assertTrue(compatInfo.isOverrideDisplayRotationRequired());
- assertEquals(expectedRotation, compatInfo.applicationDisplayRotation);
+ public void testOnCameraOpened_landscapeActivity_sandboxesDisplayRotationAndUpdatesApp() {
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_LANDSCAPE);
+ robot.activity().rotateDisplayForTopActivity(ROTATION_0);
+
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ // This is a landscape rotation for a device with portrait natural orientation (most
+ // common, currently the only one supported).
+ robot.assertCompatibilityInfoSentWithDisplayRotation(ROTATION_90);
+ });
+ }
+
+ /**
+ * Runs a test scenario providing a Robot.
+ */
+ void runTestScenario(@NonNull Consumer<CameraCompatFreeformPolicyRobotTests> consumer) {
+ final CameraCompatFreeformPolicyRobotTests robot =
+ new CameraCompatFreeformPolicyRobotTests(mWm, mAtm, mSupervisor, this);
+ consumer.accept(robot);
+ }
+
+ private static class CameraCompatFreeformPolicyRobotTests extends AppCompatRobotBase {
+ private final WindowTestsBase mWindowTestsBase;
+
+ private CameraManager.AvailabilityCallback mCameraAvailabilityCallback;
+
+ CameraCompatFreeformPolicyRobotTests(@NonNull WindowManagerService wm,
+ @NonNull ActivityTaskManagerService atm,
+ @NonNull ActivityTaskSupervisor supervisor,
+ @NonNull WindowTestsBase windowTestsBase) {
+ super(wm, atm, supervisor);
+ mWindowTestsBase = windowTestsBase;
+ setupCameraManager();
+ setupAppCompatConfiguration();
+ }
+
+ @Override
+ void onPostDisplayContentCreation(@NonNull DisplayContent displayContent) {
+ super.onPostDisplayContentCreation(displayContent);
+ spyOn(displayContent.mAppCompatCameraPolicy);
+ if (displayContent.mAppCompatCameraPolicy.mCameraCompatFreeformPolicy != null) {
+ spyOn(displayContent.mAppCompatCameraPolicy.mCameraCompatFreeformPolicy);
+ }
+ }
+
+ @Override
+ void onPostActivityCreation(@NonNull ActivityRecord activity) {
+ super.onPostActivityCreation(activity);
+ setupCameraManager();
+ setupHandler();
+ setupMockApplicationThread();
+ }
+
+ private void setupMockApplicationThread() {
+ IApplicationThread mockApplicationThread = mock(IApplicationThread.class);
+ spyOn(activity().top().app);
+ doReturn(mockApplicationThread).when(activity().top().app).getThread();
+ }
+
+ private Configuration createConfiguration(boolean letterbox, int rotation) {
+ final Configuration configuration = createConfiguration(letterbox);
+ configuration.windowConfiguration.setDisplayRotation(rotation);
+ return configuration;
+ }
+
+ private Configuration createConfiguration(boolean letterbox) {
+ final Configuration configuration = new Configuration();
+ Rect bounds = letterbox ? new Rect(/*left*/ 300, /*top*/ 0, /*right*/ 700, /*bottom*/
+ 600)
+ : new Rect(/*left*/ 0, /*top*/ 0, /*right*/ 1000, /*bottom*/ 600);
+ configuration.windowConfiguration.setAppBounds(bounds);
+ return configuration;
+ }
+
+ private void setupAppCompatConfiguration() {
+ applyOnConf((c) -> {
+ c.enableCameraCompatTreatment(true);
+ c.enableCameraCompatTreatmentAtBuildTime(true);
+ c.enableCameraCompatRefresh(true);
+ c.enableCameraCompatRefreshCycleThroughStop(true);
+ c.enableCameraCompatSplitScreenAspectRatio(false);
+ });
+ }
+
+ private void setupCameraManager() {
+ final CameraManager mockCameraManager = mock(CameraManager.class);
+ doAnswer(invocation -> {
+ mCameraAvailabilityCallback = invocation.getArgument(1);
+ return null;
+ }).when(mockCameraManager).registerAvailabilityCallback(
+ any(Executor.class), any(CameraManager.AvailabilityCallback.class));
+
+ doReturn(mockCameraManager).when(mWindowTestsBase.mWm.mContext).getSystemService(
+ CameraManager.class);
+ }
+
+ private void setupHandler() {
+ final Handler handler = activity().top().mWmService.mH;
+ spyOn(handler);
+
+ doAnswer(invocation -> {
+ ((Runnable) invocation.getArgument(0)).run();
+ return null;
+ }).when(handler).postDelayed(any(Runnable.class), anyLong());
+ }
+
+ private void configureActivity(@ScreenOrientation int activityOrientation) {
+ configureActivity(activityOrientation, WINDOWING_MODE_FREEFORM);
+ }
+
+ private void configureActivity(@ScreenOrientation int activityOrientation,
+ @WindowingMode int windowingMode) {
+ configureActivityAndDisplay(activityOrientation, ORIENTATION_PORTRAIT, windowingMode);
+ }
+
+ private void configureActivityAndDisplay(@ScreenOrientation int activityOrientation,
+ @Orientation int naturalOrientation, @WindowingMode int windowingMode) {
+ applyOnActivity(a -> {
+ dw().allowEnterDesktopMode(true);
+ a.createActivityWithComponentInNewTaskAndDisplay();
+ a.setIgnoreOrientationRequest(true);
+ a.rotateDisplayForTopActivity(ROTATION_90);
+ a.configureTopActivity(/* minAspect */ -1, /* maxAspect */ -1,
+ activityOrientation, /* isUnresizable */ false);
+ a.top().setWindowingMode(windowingMode);
+ a.displayContent().setWindowingMode(windowingMode);
+ a.setDisplayNaturalOrientation(naturalOrientation);
+ spyOn(a.top().mAppCompatController.getCameraOverrides());
+ spyOn(a.top().info);
+ doReturn(a.displayContent().getDisplayInfo()).when(
+ a.displayContent().mWmService.mDisplayManagerInternal).getDisplayInfo(
+ a.displayContent().mDisplayId);
+ });
+ }
+
+ private void onCameraOpened(@NonNull String cameraId, @NonNull String packageName) {
+ mCameraAvailabilityCallback.onCameraOpened(cameraId, packageName);
+ waitHandlerIdle();
+ }
+
+ private void onCameraClosed(@NonNull String cameraId) {
+ mCameraAvailabilityCallback.onCameraClosed(cameraId);
+ }
+
+ private void waitHandlerIdle() {
+ mWindowTestsBase.waitHandlerIdle(activity().displayContent().mWmService.mH);
+ }
+
+ void setInFreeformWindowingMode(boolean inFreeform) {
+ doReturn(inFreeform).when(activity().top()).inFreeformWindowingMode();
+ }
+
+ void setShouldRefreshActivityViaPause(boolean enabled) {
+ doReturn(enabled).when(activity().top().mAppCompatController.getCameraOverrides())
+ .shouldRefreshActivityViaPauseForCameraCompat();
+ }
+
+ void checkShouldRefreshActivity(boolean expected, Configuration newConfig,
+ Configuration oldConfig) {
+ assertEquals(expected, cameraCompatFreeformPolicy().shouldRefreshActivity(
+ activity().top(), newConfig, oldConfig));
+ }
+
+ void checkCameraCompatPolicyNotCreated() {
+ assertNull(cameraCompatFreeformPolicy());
+ }
+
+ void checkIsCameraRunningAndWindowingModeEligible(boolean expected) {
+ assertEquals(expected, cameraCompatFreeformPolicy()
+ .isCameraRunningAndWindowingModeEligible(activity().top()));
+ }
+
+ void checkIsFreeformLetterboxingForCameraAllowed(boolean expected) {
+ assertEquals(expected, cameraCompatFreeformPolicy()
+ .isFreeformLetterboxingForCameraAllowed(activity().top()));
+ }
+
+ void checkCameraCompatAspectRatioEquals(float aspectRatio) {
+ assertEquals(aspectRatio,
+ cameraCompatFreeformPolicy().getCameraCompatAspectRatio(activity().top()),
+ /* delta= */ 0.001);
+ }
+
+ private void assertInCameraCompatMode(@FreeformCameraCompatMode int mode) {
+ assertEquals(mode, cameraCompatFreeformPolicy().getCameraCompatMode(activity().top()));
+ }
+
+ private void assertNotInCameraCompatMode() {
+ assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_NONE);
+ }
+
+ private void assertActivityRefreshRequested(boolean refreshRequested) {
+ assertActivityRefreshRequested(refreshRequested, /* cycleThroughStop*/ true);
+ }
+
+ private void assertActivityRefreshRequested(boolean refreshRequested,
+ boolean cycleThroughStop) {
+ verify(activity().top().mAppCompatController.getCameraOverrides(),
+ times(refreshRequested ? 1 : 0)).setIsRefreshRequested(true);
+
+ final RefreshCallbackItem refreshCallbackItem =
+ new RefreshCallbackItem(activity().top().token,
+ cycleThroughStop ? ON_STOP : ON_PAUSE);
+ final ResumeActivityItem resumeActivityItem = new ResumeActivityItem(
+ activity().top().token,
+ /* isForward */ false, /* shouldSendCompatFakeFocus */ false);
+ try {
+ verify(activity().top().mAtmService.getLifecycleManager(),
+ times(refreshRequested ? 1 : 0))
+ .scheduleTransactionItems(activity().top().app.getThread(),
+ refreshCallbackItem, resumeActivityItem);
+ } catch (RemoteException e) {
+ fail(e.getMessage());
+ }
+ }
+
+ private void callOnActivityConfigurationChanging() {
+ callOnActivityConfigurationChanging(/* letterboxNew= */ true,
+ /* lastLetterbox= */false);
+ }
+
+ private void callOnActivityConfigurationChanging(boolean letterboxNew,
+ boolean lastLetterbox) {
+ activity().displayContent().mAppCompatCameraPolicy.mActivityRefresher
+ .onActivityConfigurationChanging(activity().top(),
+ /* newConfig */ createConfiguration(letterboxNew),
+ /* lastReportedConfig */ createConfiguration(lastLetterbox));
+ }
+
+ void checkIsCameraCompatTreatmentActiveForTopActivity(boolean active) {
+ assertEquals(active,
+ cameraCompatFreeformPolicy().isTreatmentEnabledForActivity(activity().top(),
+ /* checkOrientation */ true));
+ }
+
+ void setOverrideMinAspectRatioEnabled(boolean enabled) {
+ doReturn(enabled).when(activity().top().mAppCompatController.getCameraOverrides())
+ .isOverrideMinAspectRatioForCameraEnabled();
+ }
+
+ void assertCompatibilityInfoSentWithDisplayRotation(@Surface.Rotation int
+ expectedRotation) {
+ final ArgumentCaptor<CompatibilityInfo> compatibilityInfoArgumentCaptor =
+ ArgumentCaptor.forClass(CompatibilityInfo.class);
+ try {
+ verify(activity().top().app.getThread()).updatePackageCompatibilityInfo(
+ eq(activity().top().packageName),
+ compatibilityInfoArgumentCaptor.capture());
+ } catch (RemoteException e) {
+ fail(e.getMessage());
+ }
+
+ final CompatibilityInfo compatInfo = compatibilityInfoArgumentCaptor.getValue();
+ assertTrue(compatInfo.isOverrideDisplayRotationRequired());
+ assertEquals(expectedRotation, compatInfo.applicationDisplayRotation);
+ }
+
+ CameraCompatFreeformPolicy cameraCompatFreeformPolicy() {
+ return activity().displayContent().mAppCompatCameraPolicy.mCameraCompatFreeformPolicy;
+ }
}
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
index bc37496d14a7..e87e107cd793 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
@@ -21,6 +21,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_LARGE_VALUE;
import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_MEDIUM_VALUE;
import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_SMALL_VALUE;
@@ -157,7 +158,7 @@ public class DesktopModeLaunchParamsModifierTests extends
@Test
@EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
Flags.FLAG_DISABLE_DESKTOP_LAUNCH_PARAMS_OUTSIDE_DESKTOP_BUG_FIX})
- public void testReturnsContinueIfVisibleFreeformTaskExists() {
+ public void testReturnsContinueIfFreeformTaskExists() {
setupDesktopModeLaunchParamsModifier();
when(mTarget.isEnteringDesktopMode(any(), any(), any())).thenCallRealMethod();
@@ -165,7 +166,7 @@ public class DesktopModeLaunchParamsModifierTests extends
final Task existingFreeformTask = new TaskBuilder(mSupervisor).setCreateActivity(true)
.setWindowingMode(WINDOWING_MODE_FREEFORM).build();
doReturn(existingFreeformTask.getRootActivity()).when(dc)
- .getTopMostVisibleFreeformActivity();
+ .getTopMostFreeformActivity();
final Task launchingTask = new TaskBuilder(mSupervisor).build();
launchingTask.onDisplayChanged(dc);
@@ -269,6 +270,38 @@ public class DesktopModeLaunchParamsModifierTests extends
}
@Test
+ @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+ Flags.FLAG_INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES})
+ public void testInheritTaskBoundsFromExistingInstanceIfClosing() {
+ setupDesktopModeLaunchParamsModifier();
+
+ final String packageName = "com.same.package";
+ // Setup existing task.
+ final DisplayContent dc = spy(createNewDisplay());
+ final Task existingFreeformTask = new TaskBuilder(mSupervisor).setCreateActivity(true)
+ .setWindowingMode(WINDOWING_MODE_FREEFORM).setPackage(packageName).build();
+ existingFreeformTask.setBounds(
+ /* left */ 0,
+ /* top */ 0,
+ /* right */ 500,
+ /* bottom */ 500);
+ doReturn(existingFreeformTask.getRootActivity()).when(dc)
+ .getTopMostVisibleFreeformActivity();
+ // Set up new instance of already existing task. By default multi instance is not supported
+ // so first instance will close.
+ final Task launchingTask = new TaskBuilder(mSupervisor).setPackage(packageName)
+ .setCreateActivity(true).build();
+ launchingTask.onDisplayChanged(dc);
+ launchingTask.intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
+
+ // New instance should inherit task bounds of old instance.
+ assertEquals(RESULT_DONE,
+ new CalculateRequestBuilder().setTask(launchingTask)
+ .setActivity(launchingTask.getRootActivity()).calculate());
+ assertEquals(existingFreeformTask.getBounds(), mResult.mBounds);
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
@DisableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
public void testUsesDesiredBoundsIfEmptyLayoutAndActivityOptionsBounds() {
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayCompatTests.java
new file mode 100644
index 000000000000..1445a6982c60
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayCompatTests.java
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+
+package com.android.server.wm;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static com.android.window.flags.Flags.FLAG_ENABLE_RESTART_MENU_FOR_CONNECTED_DISPLAYS;
+
+import static junit.framework.Assert.assertFalse;
+
+import static org.junit.Assert.assertTrue;
+
+import android.compat.testing.PlatformCompatChangeRule;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.annotations.Presubmit;
+import android.view.DisplayInfo;
+
+import androidx.test.filters.MediumTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+
+/**
+ * Build/Install/Run:
+ * atest WmTests:DisplayCompatTests
+ */
+@MediumTest
+@Presubmit
+@RunWith(WindowTestRunner.class)
+public class DisplayCompatTests extends WindowTestsBase {
+
+ @Rule
+ public TestRule compatChangeRule = new PlatformCompatChangeRule();
+
+ @EnableFlags(FLAG_ENABLE_RESTART_MENU_FOR_CONNECTED_DISPLAYS)
+ @Test
+ public void testFixedMiscConfigurationWhenMovingToDisplay() {
+ // Create an app on the default display, at which point the restart menu isn't enabled.
+ final Task task = createTask(mDefaultDisplay);
+ final ActivityRecord activity = createActivityRecord(task);
+ assertFalse(task.getTaskInfo().appCompatTaskInfo.isRestartMenuEnabledForDisplayMove());
+
+ // Move the app to a secondary display, and the restart menu must get enabled.
+ final DisplayInfo displayInfo = new DisplayInfo();
+ displayInfo.copyFrom(mDisplayInfo);
+ displayInfo.displayId = DEFAULT_DISPLAY + 1;
+ final DisplayContent secondaryDisplay = createNewDisplay(displayInfo);
+ task.reparent(secondaryDisplay.getDefaultTaskDisplayArea(), true);
+ assertTrue(task.getTaskInfo().appCompatTaskInfo.isRestartMenuEnabledForDisplayMove());
+
+ // Once the app gets restarted, the restart menu must be gone.
+ activity.restartProcessIfVisible();
+ assertFalse(task.getTaskInfo().appCompatTaskInfo.isRestartMenuEnabledForDisplayMove());
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
index 71e34ef220d3..3c6a89842af9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
@@ -93,7 +93,7 @@ public class InsetsPolicyTest extends WindowTestsBase {
final Task task1 = createTask(mDisplayContent);
final Task task2 = createTask(mDisplayContent);
- task1.setAdjacentTaskFragment(task2);
+ task1.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(task1, task2));
final WindowState win = createAppWindow(task1, WINDOWING_MODE_MULTI_WINDOW, "app");
final InsetsSourceControl[] controls = addWindowAndGetControlsForDispatch(win);
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 33a48aadbd70..e2c4a1d2dfea 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -572,8 +572,8 @@ public class SizeCompatTests extends WindowTestsBase {
new TestDisplayContent.Builder(mAtm, 1000, 2000).build();
final InputDevice device = new InputDevice.Builder()
.setAssociatedDisplayId(newDisplay.mDisplayId)
- .setSources(InputDevice.SOURCE_TOUCHSCREEN | InputDevice.SOURCE_TRACKBALL
- | InputDevice.KEYBOARD_TYPE_ALPHABETIC)
+ .setKeyboardType(InputDevice.KEYBOARD_TYPE_ALPHABETIC)
+ .setSources(InputDevice.SOURCE_TOUCHSCREEN | InputDevice.SOURCE_TRACKBALL)
.build();
final InputDevice[] devices = {device};
doReturn(true).when(newDisplay.mWmService.mInputManager)
@@ -596,6 +596,7 @@ public class SizeCompatTests extends WindowTestsBase {
assertEquals(originalTouchscreen, newConfiguration.touchscreen);
assertEquals(originalNavigation, newConfiguration.navigation);
assertEquals(originalKeyboard, newConfiguration.keyboard);
+ // TODO(b/399749909): assert keyboardHidden, hardkeyboardHidden, and navigationHidden too.
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
index 3776b03695d5..b558fad84efa 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
@@ -78,6 +78,7 @@ import android.view.SurfaceControl;
import com.android.dx.mockito.inline.extended.StaticMockitoSession;
import com.android.internal.os.BackgroundThread;
+import com.android.internal.protolog.PerfettoProtoLogImpl;
import com.android.internal.protolog.ProtoLog;
import com.android.internal.protolog.WmProtoLogGroups;
import com.android.server.AnimationThread;
@@ -187,7 +188,10 @@ public class SystemServicesTestRule implements TestRule {
}
private void setUp() {
- ProtoLog.init(WmProtoLogGroups.values());
+ if (ProtoLog.getSingleInstance() == null) {
+ ProtoLog.init(WmProtoLogGroups.values());
+ PerfettoProtoLogImpl.waitForInitialization();
+ }
if (mOnBeforeServicesCreated != null) {
mOnBeforeServicesCreated.run();
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
index 986532ce5897..ec83c50e95aa 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
@@ -87,7 +87,8 @@ public class TaskDisplayAreaTests extends WindowTestsBase {
mDisplayContent, WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD);
adjacentRootTask.mCreatedByOrganizer = true;
final TaskDisplayArea taskDisplayArea = rootTask.getDisplayArea();
- adjacentRootTask.setAdjacentTaskFragment(rootTask);
+ adjacentRootTask.setAdjacentTaskFragments(
+ new TaskFragment.AdjacentSet(adjacentRootTask, rootTask));
taskDisplayArea.setLaunchAdjacentFlagRootTask(adjacentRootTask);
Task actualRootTask = taskDisplayArea.getLaunchRootTask(
@@ -113,7 +114,8 @@ public class TaskDisplayAreaTests extends WindowTestsBase {
final Task adjacentRootTask = createTask(
mDisplayContent, WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD);
adjacentRootTask.mCreatedByOrganizer = true;
- adjacentRootTask.setAdjacentTaskFragment(rootTask);
+ adjacentRootTask.setAdjacentTaskFragments(
+ new TaskFragment.AdjacentSet(adjacentRootTask, rootTask));
taskDisplayArea.setLaunchRootTask(rootTask,
new int[]{WINDOWING_MODE_MULTI_WINDOW}, new int[]{ACTIVITY_TYPE_STANDARD});
@@ -135,7 +137,8 @@ public class TaskDisplayAreaTests extends WindowTestsBase {
adjacentRootTask.mCreatedByOrganizer = true;
createActivityRecord(adjacentRootTask);
final TaskDisplayArea taskDisplayArea = rootTask.getDisplayArea();
- adjacentRootTask.setAdjacentTaskFragment(rootTask);
+ adjacentRootTask.setAdjacentTaskFragments(
+ new TaskFragment.AdjacentSet(adjacentRootTask, rootTask));
taskDisplayArea.setLaunchAdjacentFlagRootTask(adjacentRootTask);
final Task actualRootTask = taskDisplayArea.getLaunchRootTask(
@@ -821,7 +824,8 @@ public class TaskDisplayAreaTests extends WindowTestsBase {
adjacentRootTask.mCreatedByOrganizer = true;
final Task candidateTask = createTaskInRootTask(rootTask, 0 /* userId*/);
final TaskDisplayArea taskDisplayArea = rootTask.getDisplayArea();
- adjacentRootTask.setAdjacentTaskFragment(rootTask);
+ adjacentRootTask.setAdjacentTaskFragments(
+ new TaskFragment.AdjacentSet(adjacentRootTask, rootTask));
// Verify the launch root with candidate task
Task actualRootTask = taskDisplayArea.getLaunchRootTask(WINDOWING_MODE_UNDEFINED,
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
index ab76ae8e378a..76660bdc7355 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -784,7 +784,8 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
.setFragmentToken(fragmentToken2)
.build();
mWindowOrganizerController.mLaunchTaskFragments.put(fragmentToken2, taskFragment2);
- mTaskFragment.setAdjacentTaskFragment(taskFragment2);
+ mTaskFragment.setAdjacentTaskFragments(
+ new TaskFragment.AdjacentSet(mTaskFragment, taskFragment2));
mTransaction.clearAdjacentTaskFragments(mFragmentToken);
mOrganizer.applyTransaction(mTransaction, TASK_FRAGMENT_TRANSIT_CHANGE,
@@ -1267,7 +1268,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
}
@Test
- public void testTaskFragmentInPip_setAdjacentTaskFragment() {
+ public void testTaskFragmentInPip_setAdjacentTaskFragments() {
setupTaskFragmentInPip();
spyOn(mWindowOrganizerController);
@@ -1279,7 +1280,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
verify(mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer),
eq(mErrorToken), eq(mTaskFragment), eq(OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS),
any(IllegalArgumentException.class));
- verify(mTaskFragment, never()).setAdjacentTaskFragment(any());
+ verify(mTaskFragment, never()).setAdjacentTaskFragments(any());
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index cc2a76dcc9f2..7c1d7fec819b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -67,7 +67,6 @@ import android.content.res.Configuration;
import android.graphics.Color;
import android.graphics.Rect;
import android.os.Binder;
-import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.view.SurfaceControl;
import android.view.View;
@@ -363,7 +362,7 @@ public class TaskFragmentTest extends WindowTestsBase {
doReturn(true).when(primaryActivity).supportsPictureInPicture();
doReturn(false).when(secondaryActivity).supportsPictureInPicture();
- primaryTf.setAdjacentTaskFragment(secondaryTf);
+ primaryTf.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(primaryTf, secondaryTf));
primaryActivity.setState(RESUMED, "test");
secondaryActivity.setState(RESUMED, "test");
@@ -390,7 +389,8 @@ public class TaskFragmentTest extends WindowTestsBase {
task.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
taskFragment0.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
taskFragment0.setBounds(taskFragmentBounds);
- taskFragment0.setAdjacentTaskFragment(taskFragment1);
+ taskFragment0.setAdjacentTaskFragments(
+ new TaskFragment.AdjacentSet(taskFragment0, taskFragment1));
taskFragment0.setCompanionTaskFragment(taskFragment1);
taskFragment0.setAnimationParams(new TaskFragmentAnimationParams.Builder()
.setAnimationBackgroundColor(Color.GREEN)
@@ -779,7 +779,7 @@ public class TaskFragmentTest extends WindowTestsBase {
.setOrganizer(mOrganizer)
.setFragmentToken(new Binder())
.build();
- tf0.setAdjacentTaskFragment(tf1);
+ tf0.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(tf0, tf1));
tf0.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
tf1.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
task.setBounds(0, 0, 1200, 1000);
@@ -834,7 +834,7 @@ public class TaskFragmentTest extends WindowTestsBase {
final Task task = createTask(mDisplayContent);
final TaskFragment tf0 = createTaskFragmentWithActivity(task);
final TaskFragment tf1 = createTaskFragmentWithActivity(task);
- tf0.setAdjacentTaskFragment(tf1);
+ tf0.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(tf0, tf1));
tf0.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
tf1.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
task.setBounds(0, 0, 1200, 1000);
@@ -982,7 +982,8 @@ public class TaskFragmentTest extends WindowTestsBase {
.setOrganizer(mOrganizer)
.setFragmentToken(new Binder())
.build();
- taskFragmentLeft.setAdjacentTaskFragment(taskFragmentRight);
+ taskFragmentLeft.setAdjacentTaskFragments(
+ new TaskFragment.AdjacentSet(taskFragmentLeft, taskFragmentRight));
taskFragmentLeft.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
taskFragmentRight.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
task.setBounds(0, 0, 1200, 1000);
@@ -1051,8 +1052,8 @@ public class TaskFragmentTest extends WindowTestsBase {
.setParentTask(task)
.createActivityCount(1)
.build();
- taskFragmentRight.setAdjacentTaskFragment(taskFragmentLeft);
- taskFragmentLeft.setAdjacentTaskFragment(taskFragmentRight);
+ taskFragmentRight.setAdjacentTaskFragments(
+ new TaskFragment.AdjacentSet(taskFragmentLeft, taskFragmentRight));
final ActivityRecord appLeftTop = taskFragmentLeft.getTopMostActivity();
final ActivityRecord appRightTop = taskFragmentRight.getTopMostActivity();
@@ -1103,7 +1104,6 @@ public class TaskFragmentTest extends WindowTestsBase {
Math.min(outConfig.screenWidthDp, outConfig.screenHeightDp));
}
- @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS)
@Test
public void testAdjacentSetForTaskFragments() {
final Task task = createTask(mDisplayContent);
@@ -1119,7 +1119,6 @@ public class TaskFragmentTest extends WindowTestsBase {
() -> new TaskFragment.AdjacentSet(tf0, tf1, tf2));
}
- @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS)
@Test
public void testSetAdjacentTaskFragments() {
final Task task0 = createTask(mDisplayContent);
@@ -1148,7 +1147,6 @@ public class TaskFragmentTest extends WindowTestsBase {
assertFalse(task2.hasAdjacentTaskFragment());
}
- @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS)
@Test
public void testClearAdjacentTaskFragments() {
final Task task0 = createTask(mDisplayContent);
@@ -1167,7 +1165,6 @@ public class TaskFragmentTest extends WindowTestsBase {
assertFalse(task2.hasAdjacentTaskFragment());
}
- @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS)
@Test
public void testRemoveFromAdjacentTaskFragments() {
final Task task0 = createTask(mDisplayContent);
@@ -1190,7 +1187,6 @@ public class TaskFragmentTest extends WindowTestsBase {
assertFalse(task1.isAdjacentTo(task1));
}
- @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS)
@Test
public void testRemoveFromAdjacentTaskFragmentsWhenRemove() {
final Task task0 = createTask(mDisplayContent);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java
index 51ea498811fc..f22ecb5eb3f9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java
@@ -68,11 +68,8 @@ public class TaskSnapshotLowResDisabledTest extends TaskSnapshotPersisterTestBas
public void testPersistAndLoadSnapshot() {
mPersister.persistSnapshot(1, mTestUserId, createSnapshot());
mSnapshotPersistQueue.waitForQueueEmpty();
- final File[] files = new File[]{
- new File(FILES_DIR.getPath() + "/snapshots/1.proto"),
- new File(FILES_DIR.getPath() + "/snapshots/1.jpg")};
- final File[] nonExistsFiles = new File[]{
- new File(FILES_DIR.getPath() + "/snapshots/1_reduced.jpg")};
+ final File[] files = convertFilePath("1.proto", "1.jpg");
+ final File[] nonExistsFiles = convertFilePath("1_reduced.proto");
assertTrueForFiles(files, File::exists, " must exist");
assertTrueForFiles(nonExistsFiles, file -> !file.exists(), " must not exist");
final TaskSnapshot snapshot = mLoader.loadTask(1, mTestUserId, false /* isLowResolution */);
@@ -92,14 +89,9 @@ public class TaskSnapshotLowResDisabledTest extends TaskSnapshotPersisterTestBas
taskIds.add(1);
mPersister.removeObsoleteFiles(taskIds, new int[]{mTestUserId});
mSnapshotPersistQueue.waitForQueueEmpty();
- final File[] existsFiles = new File[]{
- new File(FILES_DIR.getPath() + "/snapshots/1.proto"),
- new File(FILES_DIR.getPath() + "/snapshots/1.jpg")};
- final File[] nonExistsFiles = new File[]{
- new File(FILES_DIR.getPath() + "/snapshots/1_reduced.jpg"),
- new File(FILES_DIR.getPath() + "/snapshots/2.proto"),
- new File(FILES_DIR.getPath() + "/snapshots/2.jpg"),
- new File(FILES_DIR.getPath() + "/snapshots/2_reduced.jpg")};
+ final File[] existsFiles = convertFilePath("1.proto", "1.jpg");
+ final File[] nonExistsFiles = convertFilePath("1_reduced.proto", "2.proto", "2.jpg",
+ "2_reduced.jpg");
assertTrueForFiles(existsFiles, File::exists, " must exist");
assertTrueForFiles(nonExistsFiles, file -> !file.exists(), " must not exist");
}
@@ -112,14 +104,8 @@ public class TaskSnapshotLowResDisabledTest extends TaskSnapshotPersisterTestBas
mPersister.removeObsoleteFiles(taskIds, new int[]{mTestUserId});
mPersister.persistSnapshot(2, mTestUserId, createSnapshot());
mSnapshotPersistQueue.waitForQueueEmpty();
- final File[] existsFiles = new File[]{
- new File(FILES_DIR.getPath() + "/snapshots/1.proto"),
- new File(FILES_DIR.getPath() + "/snapshots/1.jpg"),
- new File(FILES_DIR.getPath() + "/snapshots/2.proto"),
- new File(FILES_DIR.getPath() + "/snapshots/2.jpg")};
- final File[] nonExistsFiles = new File[]{
- new File(FILES_DIR.getPath() + "/snapshots/1_reduced.jpg"),
- new File(FILES_DIR.getPath() + "/snapshots/2_reduced.jpg")};
+ final File[] existsFiles = convertFilePath("1.proto", "1.jpg", "2.proto", "2.jpg");
+ final File[] nonExistsFiles = convertFilePath("1_reduced.jpg", "2_reduced.jpg");
assertTrueForFiles(existsFiles, File::exists, " must exist");
assertTrueForFiles(nonExistsFiles, file -> !file.exists(), " must not exist");
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterLoaderTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterLoaderTest.java
index 4b54e4464ca7..af06c14516a1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterLoaderTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterLoaderTest.java
@@ -75,9 +75,7 @@ public class TaskSnapshotPersisterLoaderTest extends TaskSnapshotPersisterTestBa
public void testPersistAndLoadSnapshot() {
mPersister.persistSnapshot(1, mTestUserId, createSnapshot());
mSnapshotPersistQueue.waitForQueueEmpty();
- final File[] files = new File[]{new File(FILES_DIR.getPath() + "/snapshots/1.proto"),
- new File(FILES_DIR.getPath() + "/snapshots/1.jpg"),
- new File(FILES_DIR.getPath() + "/snapshots/1_reduced.jpg")};
+ final File[] files = convertFilePath("1.proto", "1.jpg", "1_reduced.jpg");
assertTrueForFiles(files, File::exists, " must exist");
final TaskSnapshot snapshot = mLoader.loadTask(1, mTestUserId, false /* isLowResolution */);
assertNotNull(snapshot);
@@ -140,13 +138,8 @@ public class TaskSnapshotPersisterLoaderTest extends TaskSnapshotPersisterTestBa
mSnapshotPersistQueue.waitForQueueEmpty();
// Make sure 1,2 were purged but removeObsoleteFiles wasn't.
- final File[] existsFiles = new File[]{
- new File(FILES_DIR.getPath() + "/snapshots/3.proto"),
- new File(FILES_DIR.getPath() + "/snapshots/4.proto")};
- final File[] nonExistsFiles = new File[]{
- new File(FILES_DIR.getPath() + "/snapshots/100.proto"),
- new File(FILES_DIR.getPath() + "/snapshots/1.proto"),
- new File(FILES_DIR.getPath() + "/snapshots/1.proto")};
+ final File[] existsFiles = convertFilePath("3.proto", "4.proto");
+ final File[] nonExistsFiles = convertFilePath("100.proto", "1.proto", "2.proto");
assertTrueForFiles(existsFiles, File::exists, " must exist");
assertTrueForFiles(nonExistsFiles, file -> !file.exists(), " must not exist");
}
@@ -427,14 +420,8 @@ public class TaskSnapshotPersisterLoaderTest extends TaskSnapshotPersisterTestBa
taskIds.add(1);
mPersister.removeObsoleteFiles(taskIds, new int[]{mTestUserId});
mSnapshotPersistQueue.waitForQueueEmpty();
- final File[] existsFiles = new File[]{
- new File(FILES_DIR.getPath() + "/snapshots/1.proto"),
- new File(FILES_DIR.getPath() + "/snapshots/1.jpg"),
- new File(FILES_DIR.getPath() + "/snapshots/1_reduced.jpg")};
- final File[] nonExistsFiles = new File[]{
- new File(FILES_DIR.getPath() + "/snapshots/2.proto"),
- new File(FILES_DIR.getPath() + "/snapshots/2.jpg"),
- new File(FILES_DIR.getPath() + "/snapshots/2_reduced.jpg")};
+ final File[] existsFiles = convertFilePath("1.proto", "1.jpg", "1_reduced.jpg");
+ final File[] nonExistsFiles = convertFilePath("2.proto", "2.jpg", "2_reduced.jpg");
assertTrueForFiles(existsFiles, File::exists, " must exist");
assertTrueForFiles(nonExistsFiles, file -> !file.exists(), " must not exist");
}
@@ -447,13 +434,8 @@ public class TaskSnapshotPersisterLoaderTest extends TaskSnapshotPersisterTestBa
mPersister.removeObsoleteFiles(taskIds, new int[]{mTestUserId});
mPersister.persistSnapshot(2, mTestUserId, createSnapshot());
mSnapshotPersistQueue.waitForQueueEmpty();
- final File[] existsFiles = new File[]{
- new File(FILES_DIR.getPath() + "/snapshots/1.proto"),
- new File(FILES_DIR.getPath() + "/snapshots/1.jpg"),
- new File(FILES_DIR.getPath() + "/snapshots/1_reduced.jpg"),
- new File(FILES_DIR.getPath() + "/snapshots/2.proto"),
- new File(FILES_DIR.getPath() + "/snapshots/2.jpg"),
- new File(FILES_DIR.getPath() + "/snapshots/2_reduced.jpg")};
+ final File[] existsFiles = convertFilePath("1.proto", "1.jpg", "1_reduced.jpg", "2.proto",
+ "2.jpg", "2_reduced.jpg");
assertTrueForFiles(existsFiles, File::exists, " must exist");
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java
index 1e16c97de647..b2c195e8ebaa 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java
@@ -28,6 +28,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
+import android.annotation.NonNull;
import android.content.ComponentName;
import android.content.ContextWrapper;
import android.content.res.Resources;
@@ -46,6 +47,7 @@ import android.window.TaskSnapshot;
import com.android.server.LocalServices;
import com.android.server.pm.UserManagerInternal;
import com.android.server.wm.BaseAppSnapshotPersister.PersistInfoProvider;
+import com.android.window.flags.Flags;
import org.junit.After;
import org.junit.AfterClass;
@@ -129,12 +131,33 @@ class TaskSnapshotPersisterTestBase extends WindowTestsBase {
return;
}
for (File file : files) {
- if (!file.isDirectory()) {
- file.delete();
+ if (file.isDirectory()) {
+ final File[] subFiles = file.listFiles();
+ if (subFiles == null) {
+ continue;
+ }
+ for (File subFile : subFiles) {
+ subFile.delete();
+ }
}
+ file.delete();
}
}
+ File[] convertFilePath(@NonNull String... fileNames) {
+ final File[] files = new File[fileNames.length];
+ final String path;
+ if (Flags.scrambleSnapshotFileName()) {
+ path = mPersister.mPersistInfoProvider.getDirectory(mTestUserId).getPath();
+ } else {
+ path = FILES_DIR.getPath() + "/snapshots/";
+ }
+ for (int i = 0; i < fileNames.length; i++) {
+ files[i] = new File(path + fileNames[i]);
+ }
+ return files;
+ }
+
TaskSnapshot createSnapshot() {
return new TaskSnapshotBuilder().setTopActivityComponent(getUniqueComponentName()).build();
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
index 724d7e7c111c..044aacc4b988 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
@@ -1750,8 +1750,7 @@ public class TaskTests extends WindowTestsBase {
primary.mVisibleRequested = true;
secondary.mVisibleRequested = true;
- primary.setAdjacentTaskFragment(secondary);
- secondary.setAdjacentTaskFragment(primary);
+ primary.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(primary, secondary));
primary.setEmbeddedDimArea(EMBEDDED_DIM_AREA_PARENT_TASK);
doReturn(true).when(primary).shouldBoostDimmer();
task.assignChildLayers(t);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
index 7030d986494f..ae6144713a1d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
@@ -929,7 +929,6 @@ public class WindowOrganizerTests extends WindowTestsBase {
assertEquals(dc.getDefaultTaskDisplayArea().mLaunchAdjacentFlagRootTask, null);
}
- @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS)
@Test
public void testSetAdjacentLaunchRootSet() {
final DisplayContent dc = mWm.mRoot.getDisplayContent(Display.DEFAULT_DISPLAY);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index 57ab13ffee89..471b065aebb4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -122,7 +122,6 @@ import android.window.WindowContainerTransaction;
import com.android.internal.policy.AttributeCache;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.test.FakeSettingsProvider;
-import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils;
import com.android.server.wm.DisplayWindowSettings.SettingsProvider.SettingsEntry;
import org.junit.After;
@@ -289,18 +288,6 @@ public class WindowTestsBase extends SystemServiceTestsBase {
mAtm.mWindowManager.mAppCompatConfiguration
.setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(false);
- // Setup WallpaperController crop utils with a simple center-align strategy
- WallpaperCropUtils cropUtils = (displaySize, bitmapSize, suggestedCrops, rtl) -> {
- Rect crop = new Rect(0, 0, displaySize.x, displaySize.y);
- crop.scale(Math.min(
- ((float) bitmapSize.x) / displaySize.x,
- ((float) bitmapSize.y) / displaySize.y));
- crop.offset((bitmapSize.x - crop.width()) / 2, (bitmapSize.y - crop.height()) / 2);
- return crop;
- };
- mDisplayContent.mWallpaperController.setWallpaperCropUtils(cropUtils);
- mDefaultDisplay.mWallpaperController.setWallpaperCropUtils(cropUtils);
-
checkDeviceSpecificOverridesNotApplied();
}
@@ -1890,7 +1877,7 @@ public class WindowTestsBase extends SystemServiceTestsBase {
mSecondary = mService.mTaskOrganizerController.createRootTask(
display, WINDOWING_MODE_MULTI_WINDOW, null);
- mPrimary.setAdjacentTaskFragment(mSecondary);
+ mPrimary.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(mPrimary, mSecondary));
display.getDefaultTaskDisplayArea().setLaunchAdjacentFlagRootTask(mSecondary);
final Rect primaryBounds = new Rect();
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
index 89b98509de34..a727df7a7efb 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
@@ -43,6 +43,8 @@ import android.media.AudioManagerInternal;
import android.media.permission.Identity;
import android.os.Binder;
import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
import android.os.IBinder;
import android.os.IRemoteCallback;
import android.os.ParcelFileDescriptor;
@@ -837,6 +839,13 @@ final class HotwordDetectionConnection {
private final int mBindingFlags;
private final int mInstanceNumber;
+ private static final HandlerThread mHandler;
+
+ static {
+ mHandler = new HandlerThread("Sandbox detection connector");
+ mHandler.start();
+ }
+
private boolean mRespectServiceConnectionStatusChanged = true;
private boolean mIsBound = false;
private boolean mIsLoggedFirstConnect = false;
@@ -883,6 +892,11 @@ final class HotwordDetectionConnection {
}
}
+ @Override // from ServiceConnector.Impl
+ protected Handler getJobHandler() {
+ return mHandler.getThreadHandler();
+ }
+
@Override
protected long getAutoDisconnectTimeoutMs() {
return -1;
@@ -1151,14 +1165,12 @@ final class HotwordDetectionConnection {
}
private void updateServiceIdentity(ServiceConnection connection) {
- connection.run(service -> service.ping(new IRemoteCallback.Stub() {
+ connection.run(service -> service.ping(new ISandboxedDetectionService.IPingMe.Stub() {
@Override
- public void sendResult(Bundle bundle) throws RemoteException {
+ public void onPing() throws RemoteException {
// TODO: Exit if the service has been unbound already (though there's a very low
// chance this happens).
- if (DEBUG) {
- Slog.d(TAG, "updating hotword UID " + Binder.getCallingUid());
- }
+ Slog.d(TAG, "updating hotword UID " + Binder.getCallingUid());
// TODO: Have the provider point to the current state stored in
// VoiceInteractionManagerServiceImpl.
final int uid = Binder.getCallingUid();
diff --git a/telecomm/java/android/telecom/Connection.java b/telecomm/java/android/telecom/Connection.java
index 29d394201f39..ebe00782319a 100644
--- a/telecomm/java/android/telecom/Connection.java
+++ b/telecomm/java/android/telecom/Connection.java
@@ -1237,6 +1237,10 @@ public abstract class Connection extends Conferenceable {
builder.append(isLong ? " PROPERTY_IS_DOWNGRADED_CONFERENCE" : " dngrd_conf");
}
+ if ((properties & PROPERTY_CROSS_SIM) == PROPERTY_CROSS_SIM) {
+ builder.append(isLong ? " PROPERTY_CROSS_SIM" : " xsim");
+ }
+
builder.append("]");
return builder.toString();
}
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index fbba999bfb36..14d567d141cb 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -3371,14 +3371,13 @@ public class TelephonyManager {
return telephony.getDataNetworkTypeForSubscriber(subId, getOpPackageName(),
getAttributionTag());
} else {
- // This can happen when the ITelephony interface is not up yet.
+ Log.e(TAG, "getDataNetworkType: ITelephony interface is not up yet");
return NETWORK_TYPE_UNKNOWN;
}
- } catch(RemoteException ex) {
- // This shouldn't happen in the normal case
- return NETWORK_TYPE_UNKNOWN;
- } catch (NullPointerException ex) {
- // This could happen before phone restarts due to crashing
+ } catch (RemoteException // Shouldn't happen in the normal case
+ | NullPointerException ex // Could happen before phone restarts due to crashing
+ ) {
+ Log.e(TAG, "getDataNetworkType: " + ex.getMessage());
return NETWORK_TYPE_UNKNOWN;
}
}
diff --git a/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml b/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml
index 685ae9a5fef2..c5e7188e6efe 100644
--- a/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml
@@ -47,6 +47,8 @@
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
<!-- Disable AOD -->
<option name="run-command" value="settings put secure doze_always_on 0"/>
+ <!-- Disable explore hub mode -->
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/tests/FlickerTests/AppClose/AndroidTestTemplate.xml b/tests/FlickerTests/AppClose/AndroidTestTemplate.xml
index 5f92d7fe830b..22bd458e2751 100644
--- a/tests/FlickerTests/AppClose/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/AppClose/AndroidTestTemplate.xml
@@ -47,6 +47,8 @@
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
<!-- Disable AOD -->
<option name="run-command" value="settings put secure doze_always_on 0"/>
+ <!-- Disable explore hub mode -->
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml b/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml
index 1b90e99a8ba2..541ce26d1435 100644
--- a/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml
@@ -47,6 +47,8 @@
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
<!-- Disable AOD -->
<option name="run-command" value="settings put secure doze_always_on 0"/>
+ <!-- Disable explore hub mode -->
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml b/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml
index ffdbb02984a7..d2e02193f0fb 100644
--- a/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml
@@ -47,6 +47,8 @@
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
<!-- Disable AOD -->
<option name="run-command" value="settings put secure doze_always_on 0"/>
+ <!-- Disable explore hub mode -->
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/tests/FlickerTests/IME/AndroidTestTemplate.xml b/tests/FlickerTests/IME/AndroidTestTemplate.xml
index ac704e5e7c39..e112c82f0661 100644
--- a/tests/FlickerTests/IME/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/IME/AndroidTestTemplate.xml
@@ -49,6 +49,8 @@
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
<!-- Disable AOD -->
<option name="run-command" value="settings put secure doze_always_on 0"/>
+ <!-- Disable explore hub mode -->
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/tests/FlickerTests/Notification/AndroidTestTemplate.xml b/tests/FlickerTests/Notification/AndroidTestTemplate.xml
index e2ac5a9579ae..e5700c03cf77 100644
--- a/tests/FlickerTests/Notification/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/Notification/AndroidTestTemplate.xml
@@ -47,6 +47,8 @@
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
<!-- Disable AOD -->
<option name="run-command" value="settings put secure doze_always_on 0"/>
+ <!-- Disable explore hub mode -->
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml b/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml
index 1a4feb6e9eca..4c41a4c01180 100644
--- a/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml
@@ -47,6 +47,8 @@
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
<!-- Disable AOD -->
<option name="run-command" value="settings put secure doze_always_on 0"/>
+ <!-- Disable explore hub mode -->
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/tests/FlickerTests/Rotation/AndroidTestTemplate.xml b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml
index 1b2007deae27..27fc249e36b9 100644
--- a/tests/FlickerTests/Rotation/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml
@@ -47,6 +47,8 @@
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
<!-- Disable AOD -->
<option name="run-command" value="settings put secure doze_always_on 0"/>
+ <!-- Disable explore hub mode -->
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt b/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt
index e99c81493394..794fd0255726 100644
--- a/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt
+++ b/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt
@@ -214,9 +214,5 @@ class KeyGestureEventHandlerTest {
): Boolean {
return handler(event, focusedToken)
}
-
- override fun isKeyGestureSupported(gestureType: Int): Boolean {
- return true
- }
}
}
diff --git a/tests/Input/src/com/android/server/input/BatteryControllerTests.kt b/tests/Input/src/com/android/server/input/BatteryControllerTests.kt
index 044f11d6904c..890c346ea015 100644
--- a/tests/Input/src/com/android/server/input/BatteryControllerTests.kt
+++ b/tests/Input/src/com/android/server/input/BatteryControllerTests.kt
@@ -184,6 +184,8 @@ class BatteryControllerTests {
@get:Rule
val rule = MockitoJUnit.rule()!!
@get:Rule
+ val context = TestableContext(ApplicationProvider.getApplicationContext())
+ @get:Rule
val inputManagerRule = MockInputManagerRule()
@Mock
@@ -194,7 +196,6 @@ class BatteryControllerTests {
private lateinit var bluetoothBatteryManager: BluetoothBatteryManager
private lateinit var batteryController: BatteryController
- private lateinit var context: TestableContext
private lateinit var testLooper: TestLooper
private lateinit var devicesChangedListener: IInputDevicesChangedListener
private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession
@@ -202,7 +203,6 @@ class BatteryControllerTests {
@Before
fun setup() {
- context = TestableContext(ApplicationProvider.getApplicationContext())
testLooper = TestLooper()
val inputManager = InputManager(context)
context.addMockSystemService(InputManager::class.java, inputManager)
diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
index 2799f6c79215..4f1fb6487b19 100644
--- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
@@ -32,6 +32,7 @@ import android.hardware.input.InputGestureData
import android.hardware.input.InputManager
import android.hardware.input.InputManagerGlobal
import android.hardware.input.KeyGestureEvent
+import android.os.Handler
import android.os.IBinder
import android.os.Process
import android.os.SystemClock
@@ -48,9 +49,11 @@ import android.view.WindowManagerPolicyConstants.FLAG_INTERACTIVE
import androidx.test.core.app.ApplicationProvider
import com.android.dx.mockito.inline.extended.ExtendedMockito
import com.android.internal.R
+import com.android.internal.accessibility.AccessibilityShortcutController
import com.android.internal.annotations.Keep
import com.android.internal.util.FrameworkStatsLog
import com.android.modules.utils.testing.ExtendedMockitoRule
+import com.android.server.input.InputManagerService.WindowManagerCallbacks
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
@@ -67,6 +70,8 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito
+import org.mockito.kotlin.never
+import org.mockito.kotlin.times
/**
* Tests for {@link KeyGestureController}.
@@ -102,6 +107,7 @@ class KeyGestureControllerTests {
const val SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY = 0
const val SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL = 1
const val SETTINGS_KEY_BEHAVIOR_NOTHING = 2
+ const val TEST_PID = 10
}
@JvmField
@@ -116,11 +122,10 @@ class KeyGestureControllerTests {
@Rule
val rule = SetFlagsRule()
- @Mock
- private lateinit var iInputManager: IInputManager
-
- @Mock
- private lateinit var packageManager: PackageManager
+ @Mock private lateinit var iInputManager: IInputManager
+ @Mock private lateinit var packageManager: PackageManager
+ @Mock private lateinit var wmCallbacks: WindowManagerCallbacks
+ @Mock private lateinit var accessibilityShortcutController: AccessibilityShortcutController
private var currentPid = 0
private lateinit var context: Context
@@ -207,8 +212,34 @@ class KeyGestureControllerTests {
private fun setupKeyGestureController() {
keyGestureController =
- KeyGestureController(context, testLooper.looper, testLooper.looper, inputDataStore)
- Mockito.`when`(iInputManager.getAppLaunchBookmarks())
+ KeyGestureController(
+ context,
+ testLooper.looper,
+ testLooper.looper,
+ inputDataStore,
+ object : KeyGestureController.Injector() {
+ override fun getAccessibilityShortcutController(
+ context: Context?,
+ handler: Handler?
+ ): AccessibilityShortcutController {
+ return accessibilityShortcutController
+ }
+ })
+ Mockito.`when`(iInputManager.registerKeyGestureHandler(Mockito.any()))
+ .thenAnswer {
+ val args = it.arguments
+ if (args[0] != null) {
+ keyGestureController.registerKeyGestureHandler(
+ args[0] as IKeyGestureHandler,
+ TEST_PID
+ )
+ }
+ }
+ keyGestureController.setWindowManagerCallbacks(wmCallbacks)
+ Mockito.`when`(wmCallbacks.isKeyguardLocked(Mockito.anyInt())).thenReturn(false)
+ Mockito.`when`(accessibilityShortcutController
+ .isAccessibilityShortcutAvailable(Mockito.anyBoolean())).thenReturn(true)
+ Mockito.`when`(iInputManager.appLaunchBookmarks)
.thenReturn(keyGestureController.appLaunchBookmarks)
keyGestureController.systemRunning()
testLooper.dispatchAll()
@@ -1270,9 +1301,9 @@ class KeyGestureControllerTests {
)
),
TestData(
- "BACK + DPAD_DOWN -> TV Accessibility Chord",
+ "BACK + DPAD_DOWN -> Accessibility Chord(for TV)",
intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN),
- KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD,
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN),
0,
intArrayOf(
@@ -1622,6 +1653,52 @@ class KeyGestureControllerTests {
)
}
+ @Test
+ fun testAccessibilityShortcutChordPressed() {
+ setupKeyGestureController()
+
+ sendKeys(
+ intArrayOf(KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN),
+ // Assuming this value is always greater than the accessibility shortcut timeout, which
+ // currently defaults to 3000ms
+ timeDelayMs = 10000
+ )
+ Mockito.verify(accessibilityShortcutController, times(1)).performAccessibilityShortcut()
+ }
+
+ @Test
+ fun testAccessibilityTvShortcutChordPressed() {
+ setupKeyGestureController()
+
+ sendKeys(
+ intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN),
+ timeDelayMs = 10000
+ )
+ Mockito.verify(accessibilityShortcutController, times(1)).performAccessibilityShortcut()
+ }
+
+ @Test
+ fun testAccessibilityShortcutChordPressedForLessThanTimeout() {
+ setupKeyGestureController()
+
+ sendKeys(
+ intArrayOf(KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN),
+ timeDelayMs = 0
+ )
+ Mockito.verify(accessibilityShortcutController, never()).performAccessibilityShortcut()
+ }
+
+ @Test
+ fun testAccessibilityTvShortcutChordPressedForLessThanTimeout() {
+ setupKeyGestureController()
+
+ sendKeys(
+ intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN),
+ timeDelayMs = 0
+ )
+ Mockito.verify(accessibilityShortcutController, never()).performAccessibilityShortcut()
+ }
+
private fun testKeyGestureInternal(test: TestData) {
val handledEvents = mutableListOf<KeyGestureEvent>()
val handler = KeyGestureHandler { event, _ ->
@@ -1683,7 +1760,11 @@ class KeyGestureControllerTests {
assertEquals("Test: $testName should not produce Key gesture", 0, handledEvents.size)
}
- private fun sendKeys(testKeys: IntArray, assertNotSentToApps: Boolean = false) {
+ private fun sendKeys(
+ testKeys: IntArray,
+ assertNotSentToApps: Boolean = false,
+ timeDelayMs: Long = 0
+ ) {
var metaState = 0
val now = SystemClock.uptimeMillis()
for (key in testKeys) {
@@ -1699,6 +1780,11 @@ class KeyGestureControllerTests {
testLooper.dispatchAll()
}
+ if (timeDelayMs > 0) {
+ testLooper.moveTimeForward(timeDelayMs)
+ testLooper.dispatchAll()
+ }
+
for (key in testKeys.reversed()) {
val upEvent = KeyEvent(
now, now, KeyEvent.ACTION_UP, key, 0 /*repeat*/, metaState,
@@ -1742,9 +1828,5 @@ class KeyGestureControllerTests {
override fun handleKeyGesture(event: AidlKeyGestureEvent, token: IBinder?): Boolean {
return handler(event, token)
}
-
- override fun isKeyGestureSupported(gestureType: Int): Boolean {
- return true
- }
}
}
diff --git a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewControllerTests.java b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewControllerTests.java
index 5875520cd259..20528f23cc8c 100644
--- a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewControllerTests.java
+++ b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewControllerTests.java
@@ -23,7 +23,6 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import android.content.Context;
import android.graphics.Rect;
import android.hardware.input.InputManager;
import android.testing.AndroidTestingRunner;
@@ -60,9 +59,12 @@ public class TouchpadDebugViewControllerTests {
private static final String TAG = "TouchpadDebugViewController";
@Rule
+ public final TestableContext mTestableContext =
+ new TestableContext(InstrumentationRegistry.getInstrumentation().getContext());
+
+ @Rule
public final MockitoRule mockito = MockitoJUnit.rule();
- private Context mContext;
private TouchpadDebugViewController mTouchpadDebugViewController;
@Mock
private InputManager mInputManagerMock;
@@ -74,8 +76,6 @@ public class TouchpadDebugViewControllerTests {
@Before
public void setup() throws Exception {
- mContext = InstrumentationRegistry.getInstrumentation().getContext();
- TestableContext mTestableContext = new TestableContext(mContext);
mTestableContext.addMockSystemService(WindowManager.class, mWindowManagerMock);
Rect bounds = new Rect(0, 0, 2560, 1600);
diff --git a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
index 60fa52f85e34..1c366a134300 100644
--- a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
+++ b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
@@ -26,7 +26,6 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import android.content.Context;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
@@ -51,6 +50,7 @@ import com.android.server.input.TouchpadHardwareProperties;
import com.android.server.input.TouchpadHardwareState;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -70,6 +70,10 @@ public class TouchpadDebugViewTest {
private TouchpadDebugView mTouchpadDebugView;
private WindowManager.LayoutParams mWindowLayoutParams;
+ @Rule
+ public final TestableContext mTestableContext =
+ new TestableContext(InstrumentationRegistry.getInstrumentation().getContext());
+
@Mock
WindowManager mWindowManager;
@Mock
@@ -77,14 +81,10 @@ public class TouchpadDebugViewTest {
Rect mWindowBounds;
WindowMetrics mWindowMetrics;
- TestableContext mTestableContext;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- Context context = InstrumentationRegistry.getInstrumentation().getContext();
- mTestableContext = new TestableContext(context);
-
mTestableContext.addMockSystemService(WindowManager.class, mWindowManager);
mTestableContext.addMockSystemService(InputManager.class, mInputManager);
diff --git a/tests/utils/testutils/java/android/os/test/TestLooper.java b/tests/utils/testutils/java/android/os/test/TestLooper.java
index 4d379e45a81a..bb54a26036db 100644
--- a/tests/utils/testutils/java/android/os/test/TestLooper.java
+++ b/tests/utils/testutils/java/android/os/test/TestLooper.java
@@ -68,16 +68,7 @@ public class TestLooper {
* Baklava introduces new {@link TestLooperManager} APIs that we can use instead of reflection.
*/
private static boolean isAtLeastBaklava() {
- Method[] methods = TestLooperManager.class.getMethods();
- for (Method method : methods) {
- if (method.getName().equals("peekWhen")) {
- return true;
- }
- }
- return false;
- // TODO(shayba): delete the above, uncomment the below.
- // SDK_INT has not yet ramped to Baklava in all 25Q2 builds.
- // return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA;
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA;
}
static {
diff --git a/tools/aapt2/Resource.h b/tools/aapt2/Resource.h
index 0d261abd728d..e51477c668dd 100644
--- a/tools/aapt2/Resource.h
+++ b/tools/aapt2/Resource.h
@@ -249,6 +249,8 @@ struct ResourceFile {
// Flag
std::optional<FeatureFlagAttribute> flag;
+
+ bool uses_readwrite_feature_flags = false;
};
/**
diff --git a/tools/aapt2/ResourceTable.cpp b/tools/aapt2/ResourceTable.cpp
index 5435cba290fc..db7dddc49a99 100644
--- a/tools/aapt2/ResourceTable.cpp
+++ b/tools/aapt2/ResourceTable.cpp
@@ -664,6 +664,7 @@ bool ResourceTable::AddResource(NewResource&& res, android::IDiagnostics* diag)
if (!config_value->value) {
// Resource does not exist, add it now.
config_value->value = std::move(res.value);
+ config_value->uses_readwrite_feature_flags = res.uses_readwrite_feature_flags;
} else {
// When validation is enabled, ensure that a resource cannot have multiple values defined for
// the same configuration unless protected by flags.
@@ -681,12 +682,14 @@ bool ResourceTable::AddResource(NewResource&& res, android::IDiagnostics* diag)
ConfigKey{&res.config, res.product}, lt_config_key_ref()),
util::make_unique<ResourceConfigValue>(res.config, res.product));
(*it)->value = std::move(res.value);
+ (*it)->uses_readwrite_feature_flags = res.uses_readwrite_feature_flags;
break;
}
case CollisionResult::kTakeNew:
// Take the incoming value.
config_value->value = std::move(res.value);
+ config_value->uses_readwrite_feature_flags = res.uses_readwrite_feature_flags;
break;
case CollisionResult::kConflict:
@@ -843,6 +846,12 @@ NewResourceBuilder& NewResourceBuilder::SetAllowMangled(bool allow_mangled) {
return *this;
}
+NewResourceBuilder& NewResourceBuilder::SetUsesReadWriteFeatureFlags(
+ bool uses_readwrite_feature_flags) {
+ res_.uses_readwrite_feature_flags = uses_readwrite_feature_flags;
+ return *this;
+}
+
NewResource NewResourceBuilder::Build() {
return std::move(res_);
}
diff --git a/tools/aapt2/ResourceTable.h b/tools/aapt2/ResourceTable.h
index b0e185536d16..778b43adb50b 100644
--- a/tools/aapt2/ResourceTable.h
+++ b/tools/aapt2/ResourceTable.h
@@ -104,6 +104,9 @@ class ResourceConfigValue {
// The actual Value.
std::unique_ptr<Value> value;
+ // Whether the value uses read/write feature flags
+ bool uses_readwrite_feature_flags = false;
+
ResourceConfigValue(const android::ConfigDescription& config, android::StringPiece product)
: config(config), product(product) {
}
@@ -284,6 +287,7 @@ struct NewResource {
std::optional<AllowNew> allow_new;
std::optional<StagedId> staged_id;
bool allow_mangled = false;
+ bool uses_readwrite_feature_flags = false;
};
struct NewResourceBuilder {
@@ -297,6 +301,7 @@ struct NewResourceBuilder {
NewResourceBuilder& SetAllowNew(AllowNew allow_new);
NewResourceBuilder& SetStagedId(StagedId id);
NewResourceBuilder& SetAllowMangled(bool allow_mangled);
+ NewResourceBuilder& SetUsesReadWriteFeatureFlags(bool uses_feature_flags);
NewResource Build();
private:
diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp
index 2a7921600477..755dbb6f8e42 100644
--- a/tools/aapt2/cmd/Link.cpp
+++ b/tools/aapt2/cmd/Link.cpp
@@ -673,11 +673,13 @@ bool ResourceFileFlattener::Flatten(ResourceTable* table, IArchiveWriter* archiv
// Update the output format of this XML file.
file_ref->type = XmlFileTypeForOutputFormat(options_.output_format);
- bool result = table->AddResource(NewResourceBuilder(file.name)
- .SetValue(std::move(file_ref), file.config)
- .SetAllowMangled(true)
- .Build(),
- context_->GetDiagnostics());
+ bool result = table->AddResource(
+ NewResourceBuilder(file.name)
+ .SetValue(std::move(file_ref), file.config)
+ .SetAllowMangled(true)
+ .SetUsesReadWriteFeatureFlags(doc->file.uses_readwrite_feature_flags)
+ .Build(),
+ context_->GetDiagnostics());
if (!result) {
return false;
}
diff --git a/tools/aapt2/format/binary/BinaryResourceParser.cpp b/tools/aapt2/format/binary/BinaryResourceParser.cpp
index 2e20e8175213..bac871b8bdc3 100644
--- a/tools/aapt2/format/binary/BinaryResourceParser.cpp
+++ b/tools/aapt2/format/binary/BinaryResourceParser.cpp
@@ -414,6 +414,8 @@ bool BinaryResourceParser::ParseType(const ResourceTablePackage* package,
.SetId(res_id, OnIdConflict::CREATE_ENTRY)
.SetAllowMangled(true);
+ res_builder.SetUsesReadWriteFeatureFlags(entry->uses_feature_flags());
+
if (entry->flags() & ResTable_entry::FLAG_PUBLIC) {
Visibility visibility{Visibility::Level::kPublic};
diff --git a/tools/aapt2/format/binary/ResEntryWriter.cpp b/tools/aapt2/format/binary/ResEntryWriter.cpp
index 9dc205f4c1ba..0be392164453 100644
--- a/tools/aapt2/format/binary/ResEntryWriter.cpp
+++ b/tools/aapt2/format/binary/ResEntryWriter.cpp
@@ -199,6 +199,10 @@ void WriteEntry(const FlatEntry* entry, T* out_result, bool compact = false) {
flags |= ResTable_entry::FLAG_WEAK;
}
+ if (entry->uses_readwrite_feature_flags) {
+ flags |= ResTable_entry::FLAG_USES_FEATURE_FLAGS;
+ }
+
if constexpr (std::is_same_v<ResTable_entry_ext, T>) {
flags |= ResTable_entry::FLAG_COMPLEX;
}
diff --git a/tools/aapt2/format/binary/ResEntryWriter.h b/tools/aapt2/format/binary/ResEntryWriter.h
index c11598ec12f7..f54b29aa8f2a 100644
--- a/tools/aapt2/format/binary/ResEntryWriter.h
+++ b/tools/aapt2/format/binary/ResEntryWriter.h
@@ -38,6 +38,8 @@ struct FlatEntry {
// The entry string pool index to the entry's name.
uint32_t entry_key;
+
+ bool uses_readwrite_feature_flags;
};
// Pair of ResTable_entry and Res_value. These pairs are stored sequentially in values buffer.
diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp
index 1a82021bce71..50144ae816b6 100644
--- a/tools/aapt2/format/binary/TableFlattener.cpp
+++ b/tools/aapt2/format/binary/TableFlattener.cpp
@@ -502,7 +502,8 @@ class PackageFlattener {
// Group values by configuration.
for (auto& config_value : entry.values) {
config_to_entry_list_map[config_value->config].push_back(
- FlatEntry{&entry, config_value->value.get(), local_key_index});
+ FlatEntry{&entry, config_value->value.get(), local_key_index,
+ config_value->uses_readwrite_feature_flags});
}
}
diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp
index 0f1168514c4a..9156b96b67ec 100644
--- a/tools/aapt2/format/binary/TableFlattener_test.cpp
+++ b/tools/aapt2/format/binary/TableFlattener_test.cpp
@@ -1069,4 +1069,23 @@ TEST_F(TableFlattenerTest, FlattenTypeEntryWithNameCollapseInExemption) {
testing::IsTrue());
}
+TEST_F(TableFlattenerTest, UsesReadWriteFeatureFlagSerializesCorrectly) {
+ std::unique_ptr<ResourceTable> table =
+ test::ResourceTableBuilder()
+ .Add(NewResourceBuilder("com.app.a:color/foo")
+ .SetValue(util::make_unique<BinaryPrimitive>(
+ uint8_t(Res_value::TYPE_INT_COLOR_ARGB8), 0xffaabbcc))
+ .SetUsesReadWriteFeatureFlags(true)
+ .SetId(0x7f020000)
+ .Build())
+ .Build();
+ ResTable res_table;
+ TableFlattenerOptions options;
+ ASSERT_TRUE(Flatten(context_.get(), options, table.get(), &res_table));
+
+ uint32_t flags;
+ ASSERT_TRUE(res_table.getResourceEntryFlags(0x7f020000, &flags));
+ ASSERT_EQ(flags, ResTable_entry::FLAG_USES_FEATURE_FLAGS);
+}
+
} // namespace aapt
diff --git a/tools/aapt2/link/FlaggedResources_test.cpp b/tools/aapt2/link/FlaggedResources_test.cpp
index dbef77615515..47a71fe36e9f 100644
--- a/tools/aapt2/link/FlaggedResources_test.cpp
+++ b/tools/aapt2/link/FlaggedResources_test.cpp
@@ -14,6 +14,9 @@
* limitations under the License.
*/
+#include <regex>
+#include <string>
+
#include "LoadedApk.h"
#include "cmd/Dump.h"
#include "io/StringStream.h"
@@ -183,4 +186,49 @@ TEST_F(FlaggedResourcesTest, ReadWriteFlagInPathFails) {
"Only read only flags may be used with resources: test.package.rwFlag"));
}
+TEST_F(FlaggedResourcesTest, ReadWriteFlagInXmlGetsFlagged) {
+ auto apk_path = file::BuildPath({android::base::GetExecutableDirectory(), "resapp.apk"});
+ auto loaded_apk = LoadedApk::LoadApkFromPath(apk_path, &noop_diag);
+
+ std::string output;
+ DumpChunksToString(loaded_apk.get(), &output);
+
+ // The actual line looks something like:
+ // [ResTable_entry] id: 0x0000 name: layout1 keyIndex: 14 size: 8 flags: 0x0010
+ //
+ // This regex matches that line and captures the name and the flag value for checking.
+ std::regex regex("[0-9a-zA-Z:_\\]\\[ ]+name: ([0-9a-zA-Z]+)[0-9a-zA-Z: ]+flags: (0x\\d{4})");
+ std::smatch match;
+
+ std::stringstream ss(output);
+ std::string line;
+ bool found = false;
+ int fields_flagged = 0;
+ while (std::getline(ss, line)) {
+ bool first_line = false;
+ if (line.contains("config: v36")) {
+ std::getline(ss, line);
+ first_line = true;
+ }
+ if (!line.contains("flags")) {
+ continue;
+ }
+ if (std::regex_search(line, match, regex) && (match.size() == 3)) {
+ unsigned int hex_value;
+ std::stringstream hex_ss;
+ hex_ss << std::hex << match[2];
+ hex_ss >> hex_value;
+ if (hex_value & android::ResTable_entry::FLAG_USES_FEATURE_FLAGS) {
+ fields_flagged++;
+ if (first_line && match[1] == "layout1") {
+ found = true;
+ }
+ }
+ }
+ }
+ ASSERT_TRUE(found) << "No entry for layout1 at v36 with FLAG_USES_FEATURE_FLAGS bit set";
+ // There should only be 1 entry that has the FLAG_USES_FEATURE_FLAGS bit of flags set to 1
+ ASSERT_EQ(fields_flagged, 1);
+}
+
} // namespace aapt
diff --git a/tools/aapt2/link/FlaggedXmlVersioner.cpp b/tools/aapt2/link/FlaggedXmlVersioner.cpp
index 75c6f17dcb51..8a3337c446cb 100644
--- a/tools/aapt2/link/FlaggedXmlVersioner.cpp
+++ b/tools/aapt2/link/FlaggedXmlVersioner.cpp
@@ -66,6 +66,28 @@ class AllDisabledFlagsVisitor : public xml::Visitor {
bool had_flags_ = false;
};
+// An xml visitor that goes through the a doc and determines if any elements are behind a flag.
+class FindFlagsVisitor : public xml::Visitor {
+ public:
+ void Visit(xml::Element* node) override {
+ if (had_flags_) {
+ return;
+ }
+ auto* attr = node->FindAttribute(xml::kSchemaAndroid, xml::kAttrFeatureFlag);
+ if (attr != nullptr) {
+ had_flags_ = true;
+ return;
+ }
+ VisitChildren(node);
+ }
+
+ bool HadFlags() const {
+ return had_flags_;
+ }
+
+ bool had_flags_ = false;
+};
+
std::vector<std::unique_ptr<xml::XmlResource>> FlaggedXmlVersioner::Process(IAaptContext* context,
xml::XmlResource* doc) {
std::vector<std::unique_ptr<xml::XmlResource>> docs;
@@ -74,15 +96,20 @@ std::vector<std::unique_ptr<xml::XmlResource>> FlaggedXmlVersioner::Process(IAap
// Support for read/write flags was added in baklava so if the doc will only get used on
// baklava or later we can just return the original doc.
docs.push_back(doc->Clone());
+ FindFlagsVisitor visitor;
+ doc->root->Accept(&visitor);
+ docs.back()->file.uses_readwrite_feature_flags = visitor.HadFlags();
} else {
auto preBaklavaVersion = doc->Clone();
AllDisabledFlagsVisitor visitor;
preBaklavaVersion->root->Accept(&visitor);
+ preBaklavaVersion->file.uses_readwrite_feature_flags = false;
docs.push_back(std::move(preBaklavaVersion));
if (visitor.HadFlags()) {
auto baklavaVersion = doc->Clone();
baklavaVersion->file.config.sdkVersion = SDK_BAKLAVA;
+ baklavaVersion->file.uses_readwrite_feature_flags = true;
docs.push_back(std::move(baklavaVersion));
}
}
diff --git a/tools/processors/intdef_mappings/src/android/processor/IntDefProcessor.kt b/tools/processors/intdef_mappings/src/android/processor/IntDefProcessor.kt
index 4995eebdd79e..fe72dae9ff34 100644
--- a/tools/processors/intdef_mappings/src/android/processor/IntDefProcessor.kt
+++ b/tools/processors/intdef_mappings/src/android/processor/IntDefProcessor.kt
@@ -155,35 +155,35 @@ class IntDefProcessor : AbstractProcessor() {
) {
val indent = " "
- writer.appendln("{")
+ writer.appendLine("{")
val intDefTypesCount = annotationTypeToIntDefMapping.size
var currentIntDefTypesCount = 0
for ((field, intDefMapping) in annotationTypeToIntDefMapping) {
- writer.appendln("""$indent"$field": {""")
+ writer.appendLine("""$indent"$field": {""")
// Start IntDef
- writer.appendln("""$indent$indent"flag": ${intDefMapping.flag},""")
+ writer.appendLine("""$indent$indent"flag": ${intDefMapping.flag},""")
- writer.appendln("""$indent$indent"values": {""")
+ writer.appendLine("""$indent$indent"values": {""")
intDefMapping.entries.joinTo(writer, separator = ",\n") { (value, identifier) ->
"""$indent$indent$indent"$value": "$identifier""""
}
- writer.appendln()
- writer.appendln("$indent$indent}")
+ writer.appendLine()
+ writer.appendLine("$indent$indent}")
// End IntDef
writer.append("$indent}")
if (++currentIntDefTypesCount < intDefTypesCount) {
- writer.appendln(",")
+ writer.appendLine(",")
} else {
- writer.appendln("")
+ writer.appendLine("")
}
}
- writer.appendln("}")
+ writer.appendLine("}")
}
}
}