summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AconfigFlags.bp5
-rw-r--r--apex/jobscheduler/service/aconfig/alarm.aconfig10
-rw-r--r--apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java24
-rw-r--r--api/StubLibraries.bp6
-rw-r--r--core/api/current.txt20
-rw-r--r--core/api/system-current.txt8
-rw-r--r--core/api/test-current.txt4
-rw-r--r--core/java/android/app/ActivityTaskManager.java14
-rw-r--r--core/java/android/app/IActivityTaskManager.aidl6
-rw-r--r--core/java/android/app/INotificationManager.aidl1
-rw-r--r--core/java/android/app/Notification.java13
-rw-r--r--core/java/android/app/NotificationManager.java16
-rw-r--r--core/java/android/app/StatusBarManager.java57
-rw-r--r--core/java/android/app/UiAutomation.java105
-rw-r--r--core/java/android/app/notification.aconfig21
-rw-r--r--core/java/android/app/supervision/ISupervisionManager.aidl1
-rw-r--r--core/java/android/app/supervision/SupervisionManager.java17
-rw-r--r--core/java/android/app/supervision/flags.aconfig16
-rw-r--r--core/java/android/appwidget/AppWidgetHostView.java4
-rw-r--r--core/java/android/companion/virtual/flags/flags.aconfig7
-rw-r--r--core/java/android/content/ContentResolver.java4
-rw-r--r--core/java/android/content/Context.java4
-rw-r--r--core/java/android/content/pm/UserInfo.java6
-rw-r--r--core/java/android/content/pm/multiuser.aconfig10
-rw-r--r--core/java/android/credentials/flags.aconfig1
-rw-r--r--core/java/android/hardware/biometrics/ParentalControlsUtilsInternal.java38
-rw-r--r--core/java/android/hardware/camera2/CameraCharacteristics.java24
-rw-r--r--core/java/android/hardware/camera2/params/SharedSessionConfiguration.java2
-rw-r--r--core/java/android/hardware/input/input_framework.aconfig10
-rw-r--r--core/java/android/hardware/usb/OWNERS6
-rw-r--r--core/java/android/inputmethodservice/NavigationBarController.java18
-rw-r--r--core/java/android/inputmethodservice/navigationbar/NavigationBarView.java41
-rw-r--r--core/java/android/os/BaseBundle.java11
-rw-r--r--core/java/android/os/Binder.java16
-rw-r--r--core/java/android/os/Bundle.java21
-rw-r--r--core/java/android/os/CombinedMessageQueue/MessageQueue.java43
-rw-r--r--core/java/android/os/Parcel.java58
-rw-r--r--core/java/android/os/UidBatteryConsumer.java2
-rw-r--r--core/java/android/os/UserManager.java14
-rw-r--r--core/java/android/provider/Settings.java25
-rw-r--r--core/java/android/service/notification/Adjustment.java9
-rw-r--r--core/java/android/service/notification/NotificationListenerService.java24
-rw-r--r--core/java/android/service/notification/StatusBarNotification.java7
-rw-r--r--core/java/android/text/StaticLayout.java1
-rw-r--r--core/java/android/text/style/TtsSpan.java22
-rw-r--r--core/java/android/view/InsetsSourceControl.java2
-rw-r--r--core/java/android/widget/RemoteViews.java11
-rw-r--r--core/java/android/window/DesktopExperienceFlags.java124
-rw-r--r--core/java/android/window/DesktopModeFlags.java117
-rw-r--r--core/java/android/window/OWNERS1
-rw-r--r--core/java/android/window/flags/device_state_auto_rotate_setting.aconfig22
-rw-r--r--core/java/android/window/flags/lse_desktop_experience.aconfig39
-rw-r--r--core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java33
-rw-r--r--core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java30
-rw-r--r--core/java/com/android/internal/statusbar/IStatusBar.aidl8
-rw-r--r--core/java/com/android/internal/statusbar/IStatusBarService.aidl8
-rw-r--r--core/java/com/android/internal/widget/ConversationLayout.java29
-rw-r--r--core/java/com/android/internal/widget/MessagingData.java13
-rw-r--r--core/java/com/android/internal/widget/MessagingMessage.java2
-rw-r--r--core/jni/android_util_Binder.cpp34
-rw-r--r--core/res/res/values/config.xml4
-rw-r--r--core/res/res/values/public-final.xml82
-rw-r--r--core/res/res/values/public-staging.xml77
-rw-r--r--core/res/res/values/symbols.xml1
-rw-r--r--core/tests/FileSystemUtilsTest/Android.bp38
-rw-r--r--core/tests/FileSystemUtilsTest/AndroidTest.xml1
-rw-r--r--core/tests/FileSystemUtilsTest/app_with_4kb_elf/app_with_4kb_elf.xml1
-rw-r--r--core/tests/FileSystemUtilsTest/app_with_4kb_elf/app_with_4kb_no_override.xml37
-rw-r--r--core/tests/FileSystemUtilsTest/app_with_4kb_elf/page_size_compat_disabled.xml36
-rw-r--r--core/tests/FileSystemUtilsTest/app_with_4kb_elf/res/layout/hello.xml28
-rw-r--r--core/tests/FileSystemUtilsTest/app_with_4kb_elf/src/android/test/pagesizecompat/MainActivity.java3
-rw-r--r--core/tests/FileSystemUtilsTest/app_with_4kb_elf/src/android/test/pagesizecompat/PageSizeCompatTest.java37
-rw-r--r--core/tests/FileSystemUtilsTest/src/com/android/internal/content/FileSystemUtilsTest.java54
-rw-r--r--core/tests/coretests/src/android/app/NotificationManagerTest.java44
-rw-r--r--core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java20
-rw-r--r--core/tests/coretests/src/android/window/DesktopExperienceFlagsTest.java164
-rw-r--r--core/tests/coretests/src/android/window/DesktopModeFlagsTest.java284
-rw-r--r--data/etc/com.android.systemui.xml1
-rw-r--r--graphics/java/android/graphics/Paint.java8
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java14
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt4
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java2
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java40
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt60
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java72
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java151
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java518
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java54
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java93
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt90
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/UserProfileContexts.kt83
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/AppCompatUtils.kt47
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java19
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java78
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt87
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt87
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt112
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt77
-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/DesktopUserRepositories.kt10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/compatui/SystemModalsTransitionHandler.kt14
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt349
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilter.kt33
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt34
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt51
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/OnDeskRemovedListener.kt22
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt182
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java18
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java34
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipInputConsumer.java16
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java22
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java22
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java16
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java50
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java46
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java17
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java40
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecorViewModel.java38
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecoration.java28
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java18
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java7
-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/common/WindowDecorTaskResourceLoader.kt37
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt105
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt10
-rw-r--r--libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppFromAllAppsLandscape.kt44
-rw-r--r--libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppFromAllAppsPortrait.kt44
-rw-r--r--libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppFromTaskbarLandscape.kt44
-rw-r--r--libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppFromTaskbarPortrait.kt44
-rw-r--r--libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt3
-rw-r--r--libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeAppWindows.kt3
-rw-r--r--libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppFromAllApps.kt10
-rw-r--r--libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithKeyboardShortcuts.kt3
-rw-r--r--libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/CopyContentInSplit.kt2
-rw-r--r--libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByDivider.kt2
-rw-r--r--libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByGoHome.kt2
-rw-r--r--libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DragDividerToResize.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenAutoEnterPipOnGoToHomeTest.kt3
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java211
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt77
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/UserProfileContextsTest.kt166
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt12
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt166
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt130
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt117
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt54
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt13
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/compatui/SystemModalsTransitionHandlerTest.kt20
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt342
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt18
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilterTest.kt104
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt256
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt27
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java17
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java6
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt (renamed from libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/AppCompatUtilsTest.kt)29
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java10
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java19
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/WindowingEducationTestUtils.kt10
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt4
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java5
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt4
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt15
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt8
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt20
-rw-r--r--libs/hwui/Android.bp3
-rw-r--r--media/java/android/media/AudioManager.java109
-rw-r--r--media/java/android/media/MediaCodec.java33
-rw-r--r--media/java/android/media/MediaRouter2.java2
-rw-r--r--media/java/android/media/flags/projection.aconfig10
-rw-r--r--media/java/android/media/projection/IMediaProjectionWatcherCallback.aidl16
-rw-r--r--media/java/android/media/projection/MediaProjectionEvent.aidl3
-rw-r--r--media/java/android/media/projection/MediaProjectionEvent.java103
-rw-r--r--media/java/android/media/projection/MediaProjectionManager.java21
-rw-r--r--media/java/android/media/quality/MediaQualityContract.java332
-rw-r--r--media/java/android/media/quality/MediaQualityManager.java11
-rw-r--r--media/java/android/mtp/OWNERS6
-rw-r--r--media/jni/android_mtp_MtpDatabase.cpp4
-rw-r--r--media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioManagerTest.java51
-rw-r--r--media/tests/MtpTests/OWNERS6
-rw-r--r--packages/PrintSpooler/src/com/android/printspooler/model/PageContentRepository.java5
-rw-r--r--packages/PrintSpooler/src/com/android/printspooler/model/RemotePrintDocument.java17
-rw-r--r--packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java43
-rw-r--r--packages/SettingsLib/BannerMessagePreference/Android.bp4
-rw-r--r--packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled.xml3
-rw-r--r--packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_extra.xml3
-rw-r--r--packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_large.xml3
-rw-r--r--packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline.xml3
-rw-r--r--packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_extra.xml3
-rw-r--r--packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_large.xml3
-rw-r--r--packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal.xml3
-rw-r--r--packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_extra.xml3
-rw-r--r--packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_large.xml3
-rw-r--r--packages/SettingsLib/ButtonPreference/res/layout/settingslib_button_layout.xml3
-rw-r--r--packages/SettingsLib/ButtonPreference/res/layout/settingslib_number_button.xml3
-rw-r--r--packages/SettingsLib/ButtonPreference/res/layout/settingslib_section_button.xml3
-rw-r--r--packages/SettingsLib/DisplayUtils/Android.bp2
-rw-r--r--packages/SettingsLib/DisplayUtils/src/com/android/settingslib/display/DisplayDensityConfiguration.java45
-rw-r--r--packages/SettingsLib/Graph/graph.proto8
-rw-r--r--packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt39
-rw-r--r--packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt7
-rw-r--r--packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt89
-rw-r--r--packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt18
-rw-r--r--packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoDsl.kt7
-rw-r--r--packages/SettingsLib/IntroPreference/res/layout/settingslib_expressive_preference_intro.xml2
-rw-r--r--packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt130
-rw-r--r--packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Annotations.kt15
-rw-r--r--packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Bundles.kt46
-rw-r--r--packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt9
-rw-r--r--packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceCoordinate.kt68
-rw-r--r--packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt31
-rw-r--r--packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenBindingKeyProvider.kt7
-rw-r--r--packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenMetadata.kt63
-rw-r--r--packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt28
-rw-r--r--packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt5
-rw-r--r--packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt11
-rw-r--r--packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt7
-rw-r--r--packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt13
-rw-r--r--packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictionsProvider.kt4
-rw-r--r--packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt2
-rw-r--r--packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt5
-rw-r--r--packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt3
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/RestrictedDropDownPreference.java15
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/RestrictedPreference.java15
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java19
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/RestrictedSelectorWithWidgetPreference.java7
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java17
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java34
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java5
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java6
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java35
-rw-r--r--packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java1
-rw-r--r--packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java1
-rw-r--r--packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java6
-rw-r--r--packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java1
-rw-r--r--packages/SystemUI/AndroidManifest.xml10
-rw-r--r--packages/SystemUI/accessibility/accessibilitymenu/aconfig/Android.bp2
-rw-r--r--packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig2
-rw-r--r--packages/SystemUI/aconfig/systemui.aconfig70
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt25
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt4
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt3
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/MediaCarouselSection.kt12
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt85
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt9
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt70
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt4
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt2
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt70
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/Seek.kt203
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt7
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt75
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt17
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt11
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt16
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt40
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt61
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt22
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/DefaultShortcutCategoriesRepositoryTest.kt20
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarterTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt50
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfigTest.kt155
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelTest.kt84
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java84
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModelTest.kt161
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ModesTileTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt23
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QsBatteryModeControllerTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt56
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt19
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/display/FocusShadeDisplayPolicyTest.kt58
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicyTest.kt65
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt41
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolverTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt39
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/BundleNotificationInfoTest.java9
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java127
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfoTest.java156
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt67
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorTest.kt30
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorTest.kt22
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInputEventsInteractorTest.kt13
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt16
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt1
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt5
-rw-r--r--packages/SystemUI/res-keyguard/layout/bouncer_message_view.xml4
-rw-r--r--packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml2
-rw-r--r--packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml2
-rw-r--r--packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml2
-rw-r--r--packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml2
-rw-r--r--packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml2
-rw-r--r--packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml2
-rw-r--r--packages/SystemUI/res-keyguard/values/styles.xml11
-rw-r--r--packages/SystemUI/res/drawable/ic_widgets.xml26
-rw-r--r--packages/SystemUI/res/drawable/notif_footer_btn_background.xml3
-rw-r--r--packages/SystemUI/res/layout/promoted_notification_info.xml387
-rw-r--r--packages/SystemUI/res/layout/volume_dialog_slider.xml8
-rw-r--r--packages/SystemUI/res/values-xlarge-land/config.xml1
-rw-r--r--packages/SystemUI/res/values/config.xml5
-rw-r--r--packages/SystemUI/res/values/dimens.xml3
-rw-r--r--packages/SystemUI/res/values/strings.xml30
-rw-r--r--packages/SystemUI/res/values/styles.xml10
-rw-r--r--packages/SystemUI/res/xml/gradient_color_wallpaper.xml20
-rw-r--r--packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl7
-rw-r--r--packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java42
-rw-r--r--packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java43
-rw-r--r--packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt30
-rw-r--r--packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java26
-rw-r--r--packages/SystemUI/src/com/android/keyguard/CarrierText.java9
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardFingerprintListenModel.kt4
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java4
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java32
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/grid/ui/compose/SpannedGrids.kt28
-rw-r--r--packages/SystemUI/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt31
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/AccessibilityShortcutsSource.kt76
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/MultitaskingShortcutsSource.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt19
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt33
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfig.kt119
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WallpaperFocalAreaInteractor.kt176
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt22
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModel.kt48
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt30
-rw-r--r--packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java57
-rw-r--r--packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java72
-rw-r--r--packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java56
-rw-r--r--packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt52
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/BounceableInfo.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt32
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt28
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt29
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt48
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotCrossProfileService.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java16
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java21
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java23
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/QsBatteryModeController.kt20
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt24
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/data/repository/FakeShadeDisplayRepository.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt48
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/display/FakeShadeDisplayPolicy.kt36
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/display/FocusShadeDisplayPolicy.kt34
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/display/ShadeDisplayPolicy.kt33
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt22
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt43
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java10
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java74
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/headsup/shared/StatusBarNoHunBehavior.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/NmSummarizationUiFlag.kt50
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java22
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java16
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BundleNotificationInfo.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java91
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java15
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt124
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java17
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java15
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java104
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineViewBinder.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/SingleLineViewModel.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt32
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java20
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java17
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java16
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java22
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTransitionAnimatorController.kt19
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt20
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt44
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogEventModel.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponent.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/data/repository/VolumeDialogSliderTouchEventsRepository.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInputEventsInteractor.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/shared/model/SliderInputEvent.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinder.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinder.kt82
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt249
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt347
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogOverscrollViewModel.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModel.kt43
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt51
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/haptics/ui/VolumeHapticsConfigsProvider.kt41
-rw-r--r--packages/SystemUI/src/com/android/systemui/wallpapers/GradientColorWallpaper.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt70
-rw-r--r--packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java56
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt4
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt8
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt19
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt41
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java131
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewBinderTest.kt70
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt32
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt8
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java11
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java7
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeFocusedDisplayRepository.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfigKosmos.kt35
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt9
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/WallpaperFocalAreaInteractorKosmos.kt39
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelFactoryKosmos.kt (renamed from packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModelKosmos.kt)19
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt3
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryKosmos.kt17
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorKosmos.kt43
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java10
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt5
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt33
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt6
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModelKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryKosmos.kt4
-rw-r--r--packages/Vcn/framework-b/framework-vcn-jarjar-rules.txt3
-rw-r--r--packages/Vcn/service-b/src/com/android/server/ConnectivityServiceInitializerB.java3
-rw-r--r--packages/Vcn/service-b/src/com/android/server/VcnManagementService.java3
-rw-r--r--packages/Vcn/service-b/src/com/android/server/vcn/TelephonySubscriptionTracker.java3
-rw-r--r--packages/Vcn/service-b/src/com/android/server/vcn/Vcn.java3
-rw-r--r--packages/Vcn/service-b/src/com/android/server/vcn/VcnGatewayConnection.java3
-rw-r--r--packages/Vcn/service-b/src/com/android/server/vcn/VcnNetworkProvider.java3
-rw-r--r--packages/Vcn/service-b/src/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java3
-rw-r--r--packages/Vcn/service-b/src/com/android/server/vcn/routeselection/NetworkMetricMonitor.java3
-rw-r--r--packages/Vcn/service-b/src/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java3
-rw-r--r--packages/Vcn/service-b/src/com/android/server/vcn/routeselection/UnderlyingNetworkController.java3
-rw-r--r--packages/Vcn/service-b/src/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java3
-rw-r--r--ravenwood/Android.bp7
-rw-r--r--ravenwood/Framework.bp52
-rwxr-xr-xravenwood/scripts/pta-framework.sh3
-rw-r--r--ravenwood/texts/ravenwood-framework-policies.txt2
-rw-r--r--ravenwood/texts/ravenwood-standard-options.txt3
-rw-r--r--ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/Annotations.kt3
-rw-r--r--ravenwood/tools/ravenizer/Android.bp2
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java1
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java34
-rw-r--r--services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java (renamed from services/accessibility/java/com/android/server/accessibility/AutoclickController.java)156
-rw-r--r--services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickIndicatorView.java (renamed from services/accessibility/java/com/android/server/accessibility/AutoclickIndicatorView.java)26
-rw-r--r--services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java10
-rw-r--r--services/core/java/com/android/server/MasterClearReceiver.java2
-rw-r--r--services/core/java/com/android/server/am/BroadcastController.java2
-rw-r--r--services/core/java/com/android/server/am/PendingIntentRecord.java12
-rw-r--r--services/core/java/com/android/server/appop/AppOpsService.java89
-rw-r--r--services/core/java/com/android/server/audio/AudioManagerShellCommand.java39
-rw-r--r--services/core/java/com/android/server/audio/AudioService.java69
-rw-r--r--services/core/java/com/android/server/audio/PlaybackActivityMonitor.java67
-rw-r--r--services/core/java/com/android/server/backup/SystemBackupAgent.java7
-rw-r--r--services/core/java/com/android/server/display/DisplayBackupHelper.java137
-rw-r--r--services/core/java/com/android/server/display/feature/DisplayManagerFlags.java3
-rw-r--r--services/core/java/com/android/server/display/plugin/PluginManager.java10
-rw-r--r--services/core/java/com/android/server/display/plugin/PluginStorage.java200
-rw-r--r--services/core/java/com/android/server/hdmi/HdmiControlService.java4
-rw-r--r--services/core/java/com/android/server/input/InputGestureManager.java14
-rw-r--r--services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java40
-rw-r--r--services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java102
-rw-r--r--services/core/java/com/android/server/media/projection/MediaProjectionStopController.java37
-rw-r--r--services/core/java/com/android/server/media/quality/MediaQualityService.java504
-rw-r--r--services/core/java/com/android/server/net/NetworkPolicyManagerService.java9
-rw-r--r--services/core/java/com/android/server/notification/NotificationManagerService.java76
-rw-r--r--services/core/java/com/android/server/notification/NotificationRecord.java17
-rw-r--r--services/core/java/com/android/server/notification/NotificationRecordExtractorData.java11
-rw-r--r--services/core/java/com/android/server/notification/PreferencesHelper.java29
-rw-r--r--services/core/java/com/android/server/notification/ZenLog.java5
-rw-r--r--services/core/java/com/android/server/notification/flags.aconfig10
-rw-r--r--services/core/java/com/android/server/pm/DexOptHelper.java99
-rw-r--r--services/core/java/com/android/server/pm/InstallPackageHelper.java1
-rw-r--r--services/core/java/com/android/server/pm/UserManagerInternal.java6
-rw-r--r--services/core/java/com/android/server/pm/UserManagerService.java17
-rw-r--r--services/core/java/com/android/server/policy/PhoneWindowManager.java28
-rw-r--r--services/core/java/com/android/server/policy/SingleKeyGestureDetector.java15
-rw-r--r--services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java10
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperManagerInternal.java3
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperManagerService.java238
-rw-r--r--services/core/java/com/android/server/wm/AccessibilityController.java24
-rw-r--r--services/core/java/com/android/server/wm/ActivityRecord.java20
-rw-r--r--services/core/java/com/android/server/wm/ActivityTaskManagerService.java10
-rw-r--r--services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java5
-rw-r--r--services/core/java/com/android/server/wm/DisplayPolicy.java5
-rw-r--r--services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java2
-rw-r--r--services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java7
-rw-r--r--services/core/java/com/android/server/wm/Transition.java11
-rw-r--r--services/core/java/com/android/server/wm/WallpaperWindowToken.java5
-rw-r--r--services/core/java/com/android/server/wm/WindowContainer.java21
-rw-r--r--services/core/java/com/android/server/wm/WindowManagerService.java20
-rw-r--r--services/core/jni/com_android_server_utils_AnrTimer.cpp10
-rw-r--r--services/java/com/android/server/SystemServer.java2
-rw-r--r--services/supervision/java/com/android/server/supervision/SupervisionService.java96
-rw-r--r--services/supervision/java/com/android/server/supervision/SupervisionServiceShellCommand.java12
-rw-r--r--services/supervision/java/com/android/server/supervision/SupervisionUserData.java2
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/DisplayBackupHelperTest.kt86
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/plugin/PluginManagerTest.kt9
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/plugin/PluginStorageTest.kt75
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java26
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java34
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java12
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java184
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterTest.java1
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java18
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java (renamed from services/tests/servicestests/src/com/android/server/accessibility/AutoclickControllerTest.java)4
-rw-r--r--services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java8
-rw-r--r--services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java2
-rw-r--r--services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java3
-rw-r--r--services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java107
-rw-r--r--services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionStopControllerTest.java102
-rw-r--r--services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java6
-rw-r--r--services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java16
-rw-r--r--services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt50
-rw-r--r--services/tests/uiservicestests/Android.bp1
-rw-r--r--services/tests/uiservicestests/AndroidManifest.xml2
-rw-r--r--services/tests/uiservicestests/src/android/app/ExampleActivity.java (renamed from packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeUserActionsViewModelKosmos.kt)12
-rw-r--r--services/tests/uiservicestests/src/android/app/NotificationManagerZenTest.java284
-rw-r--r--services/tests/uiservicestests/src/android/app/NotificationSystemUtil.java71
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java10
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java83
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordExtractorDataTest.java55
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java47
-rw-r--r--services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java2
-rw-r--r--services/tests/wmtests/src/com/android/server/policy/DeferredKeyActionExecutorTests.java2
-rw-r--r--services/tests/wmtests/src/com/android/server/policy/KeyCombinationManagerTests.java3
-rw-r--r--services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java11
-rw-r--r--services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java5
-rw-r--r--services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java2
-rw-r--r--services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java2
-rw-r--r--services/tests/wmtests/src/com/android/server/policy/SingleKeyGestureTests.java48
-rw-r--r--services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java2
-rw-r--r--services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java5
-rw-r--r--services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java2
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java39
-rw-r--r--services/usb/OWNERS6
-rw-r--r--telecomm/java/com/android/internal/telecom/ITelecomService.aidl1
-rw-r--r--telephony/java/android/telephony/CarrierConfigManager.java1
-rw-r--r--telephony/java/android/telephony/satellite/SatelliteSubscriberInfo.java4
-rw-r--r--telephony/java/android/telephony/satellite/SatelliteSubscriberProvisionStatus.java4
-rw-r--r--telephony/java/com/android/internal/telephony/util/WorkerThread.java130
-rw-r--r--tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/RecentTasksUtils.kt (renamed from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/RecentTasksUtils.kt)4
-rw-r--r--tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt24
-rw-r--r--tests/Tracing/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java11
-rw-r--r--tests/testables/src/android/testing/OWNERS2
-rw-r--r--tests/testables/src/android/testing/TestableLooper.java6
-rw-r--r--tests/utils/testutils/java/android/os/test/OWNERS3
-rw-r--r--tests/utils/testutils/java/android/os/test/TestLooper.java12
-rwxr-xr-xtools/aapt2/tools/finalize_res.py111
612 files changed, 14848 insertions, 5079 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 302168d845fa..834398e5c2c2 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -824,6 +824,11 @@ java_aconfig_library {
defaults: ["framework-minus-apex-aconfig-java-defaults"],
}
+cc_aconfig_library {
+ name: "android.media.tv.flags-aconfig-cc",
+ aconfig_declarations: "android.media.tv.flags-aconfig",
+}
+
// Permissions
aconfig_declarations {
name: "android.permission.flags-aconfig",
diff --git a/apex/jobscheduler/service/aconfig/alarm.aconfig b/apex/jobscheduler/service/aconfig/alarm.aconfig
index a6e980726a9a..9181ef0c532a 100644
--- a/apex/jobscheduler/service/aconfig/alarm.aconfig
+++ b/apex/jobscheduler/service/aconfig/alarm.aconfig
@@ -7,3 +7,13 @@ flag {
description: "Persist list of users with alarms scheduled and wakeup stopped users before alarms are due"
bug: "314907186"
}
+
+flag {
+ name: "acquire_wakelock_before_send"
+ namespace: "backstage_power"
+ description: "Acquire the userspace alarm wakelock before sending the alarm"
+ bug: "391413964"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index 829442aed6ac..f89b13dce307 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -5334,6 +5334,18 @@ public class AlarmManagerService extends SystemService {
public void deliverLocked(Alarm alarm, long nowELAPSED) {
final long workSourceToken = ThreadLocalWorkSource.setUid(
getAlarmAttributionUid(alarm));
+
+ if (Flags.acquireWakelockBeforeSend()) {
+ // Acquire the wakelock before starting the app. This needs to be done to avoid
+ // random stalls in the receiving app in case a suspend attempt is already in
+ // progress. See b/391413964 for an incident where this was found to happen.
+ if (mBroadcastRefCount == 0) {
+ setWakelockWorkSource(alarm.workSource, alarm.creatorUid, alarm.statsTag, true);
+ mWakeLock.acquire();
+ mHandler.obtainMessage(AlarmHandler.REPORT_ALARMS_ACTIVE, 1, 0).sendToTarget();
+ }
+ }
+
try {
if (alarm.operation != null) {
// PendingIntent alarm
@@ -5399,14 +5411,16 @@ public class AlarmManagerService extends SystemService {
ThreadLocalWorkSource.restore(workSourceToken);
}
- // The alarm is now in flight; now arrange wakelock and stats tracking
if (DEBUG_WAKELOCK) {
Slog.d(TAG, "mBroadcastRefCount -> " + (mBroadcastRefCount + 1));
}
- if (mBroadcastRefCount == 0) {
- setWakelockWorkSource(alarm.workSource, alarm.creatorUid, alarm.statsTag, true);
- mWakeLock.acquire();
- mHandler.obtainMessage(AlarmHandler.REPORT_ALARMS_ACTIVE, 1, 0).sendToTarget();
+ if (!Flags.acquireWakelockBeforeSend()) {
+ // The alarm is now in flight; now arrange wakelock and stats tracking
+ if (mBroadcastRefCount == 0) {
+ setWakelockWorkSource(alarm.workSource, alarm.creatorUid, alarm.statsTag, true);
+ mWakeLock.acquire();
+ mHandler.obtainMessage(AlarmHandler.REPORT_ALARMS_ACTIVE, 1, 0).sendToTarget();
+ }
}
final InFlight inflight = new InFlight(AlarmManagerService.this, alarm, nowELAPSED);
mInFlight.add(inflight);
diff --git a/api/StubLibraries.bp b/api/StubLibraries.bp
index 787fdee6ee16..3314b4aa0d71 100644
--- a/api/StubLibraries.bp
+++ b/api/StubLibraries.bp
@@ -648,7 +648,7 @@ java_api_library {
java_api_library {
name: "android-non-updatable.stubs.module_lib.from-text",
- api_surface: "module_lib",
+ api_surface: "module-lib",
api_contributions: [
"api-stubs-docs-non-updatable.api.contribution",
"system-api-stubs-docs-non-updatable.api.contribution",
@@ -668,7 +668,7 @@ java_api_library {
// generated from this module, as this module is strictly used for hiddenapi only.
java_api_library {
name: "android-non-updatable.stubs.test_module_lib",
- api_surface: "module_lib",
+ api_surface: "module-lib",
api_contributions: [
"api-stubs-docs-non-updatable.api.contribution",
"system-api-stubs-docs-non-updatable.api.contribution",
@@ -689,7 +689,7 @@ java_api_library {
java_api_library {
name: "android-non-updatable.stubs.system_server.from-text",
- api_surface: "system_server",
+ api_surface: "system-server",
api_contributions: [
"api-stubs-docs-non-updatable.api.contribution",
"system-api-stubs-docs-non-updatable.api.contribution",
diff --git a/core/api/current.txt b/core/api/current.txt
index e0fc9590f9f7..e9a63f74d59f 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -452,7 +452,7 @@ package android {
field public static final int activityCloseExitAnimation = 16842939; // 0x10100bb
field public static final int activityOpenEnterAnimation = 16842936; // 0x10100b8
field public static final int activityOpenExitAnimation = 16842937; // 0x10100b9
- field @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public static final int adServiceTypes;
+ field @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public static final int adServiceTypes = 16844452; // 0x10106a4
field public static final int addPrintersActivity = 16843750; // 0x10103e6
field public static final int addStatesFromChildren = 16842992; // 0x10100f0
field public static final int adjustViewBounds = 16843038; // 0x101011e
@@ -1017,7 +1017,7 @@ package android {
field public static final int insetRight = 16843192; // 0x10101b8
field public static final int insetTop = 16843193; // 0x10101b9
field public static final int installLocation = 16843447; // 0x10102b7
- field @FlaggedApi("android.security.enable_intent_matching_flags") public static final int intentMatchingFlags;
+ field @FlaggedApi("android.security.enable_intent_matching_flags") public static final int intentMatchingFlags = 16844457; // 0x10106a9
field public static final int interactiveUiTimeout = 16844181; // 0x1010595
field public static final int interpolator = 16843073; // 0x1010141
field public static final int intro = 16844395; // 0x101066b
@@ -1072,7 +1072,7 @@ package android {
field public static final int label = 16842753; // 0x1010001
field public static final int labelFor = 16843718; // 0x10103c6
field @Deprecated public static final int labelTextSize = 16843317; // 0x1010235
- field @FlaggedApi("android.view.inputmethod.ime_switcher_revamp_api") public static final int languageSettingsActivity;
+ field @FlaggedApi("android.view.inputmethod.ime_switcher_revamp_api") public static final int languageSettingsActivity = 16844453; // 0x10106a5
field public static final int languageTag = 16844040; // 0x1010508
field public static final int largeHeap = 16843610; // 0x101035a
field public static final int largeScreens = 16843398; // 0x1010286
@@ -1085,7 +1085,7 @@ package android {
field public static final int layout = 16842994; // 0x10100f2
field public static final int layoutAnimation = 16842988; // 0x10100ec
field public static final int layoutDirection = 16843698; // 0x10103b2
- field @FlaggedApi("android.view.inputmethod.ime_switcher_revamp_api") public static final int layoutLabel;
+ field @FlaggedApi("android.view.inputmethod.ime_switcher_revamp_api") public static final int layoutLabel = 16844458; // 0x10106aa
field public static final int layoutMode = 16843738; // 0x10103da
field public static final int layout_above = 16843140; // 0x1010184
field public static final int layout_alignBaseline = 16843142; // 0x1010186
@@ -1207,7 +1207,7 @@ package android {
field public static final int minResizeHeight = 16843670; // 0x1010396
field public static final int minResizeWidth = 16843669; // 0x1010395
field public static final int minSdkVersion = 16843276; // 0x101020c
- field @FlaggedApi("android.sdk.major_minor_versioning_scheme") public static final int minSdkVersionFull;
+ field @FlaggedApi("android.sdk.major_minor_versioning_scheme") public static final int minSdkVersionFull = 16844461; // 0x10106ad
field public static final int minWidth = 16843071; // 0x101013f
field public static final int minimumHorizontalAngle = 16843901; // 0x101047d
field public static final int minimumVerticalAngle = 16843902; // 0x101047e
@@ -1282,7 +1282,7 @@ package android {
field public static final int paddingStart = 16843699; // 0x10103b3
field public static final int paddingTop = 16842967; // 0x10100d7
field public static final int paddingVertical = 16844094; // 0x101053e
- field @FlaggedApi("android.content.pm.app_compat_option_16kb") public static final int pageSizeCompat;
+ field @FlaggedApi("android.content.pm.app_compat_option_16kb") public static final int pageSizeCompat = 16844459; // 0x10106ab
field public static final int panelBackground = 16842846; // 0x101005e
field public static final int panelColorBackground = 16842849; // 0x1010061
field public static final int panelColorForeground = 16842848; // 0x1010060
@@ -1624,7 +1624,7 @@ package android {
field public static final int summaryColumn = 16843426; // 0x10102a2
field public static final int summaryOff = 16843248; // 0x10101f0
field public static final int summaryOn = 16843247; // 0x10101ef
- field @FlaggedApi("android.view.accessibility.supplemental_description") public static final int supplementalDescription;
+ field @FlaggedApi("android.view.accessibility.supplemental_description") public static final int supplementalDescription = 16844456; // 0x10106a8
field public static final int supportedTypes = 16844369; // 0x1010651
field public static final int supportsAssist = 16844016; // 0x10104f0
field public static final int supportsBatteryGameMode = 16844374; // 0x1010656
@@ -1871,7 +1871,7 @@ package android {
field public static final int wallpaperIntraOpenExitAnimation = 16843416; // 0x1010298
field public static final int wallpaperOpenEnterAnimation = 16843411; // 0x1010293
field public static final int wallpaperOpenExitAnimation = 16843412; // 0x1010294
- field @FlaggedApi("android.nfc.nfc_associated_role_services") public static final int wantsRoleHolderPriority;
+ field @FlaggedApi("android.nfc.nfc_associated_role_services") public static final int wantsRoleHolderPriority = 16844460; // 0x10106ac
field public static final int webTextViewStyle = 16843449; // 0x10102b9
field public static final int webViewStyle = 16842885; // 0x1010085
field public static final int weekDayTextAppearance = 16843592; // 0x1010348
@@ -11134,6 +11134,7 @@ package android.content {
field public static final String TELEPHONY_IMS_SERVICE = "telephony_ims";
field public static final String TELEPHONY_SERVICE = "phone";
field public static final String TELEPHONY_SUBSCRIPTION_SERVICE = "telephony_subscription_service";
+ field public static final String TETHERING_SERVICE = "tethering";
field public static final String TEXT_CLASSIFICATION_SERVICE = "textclassification";
field public static final String TEXT_SERVICES_MANAGER_SERVICE = "textservices";
field @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public static final String TV_AD_SERVICE = "tv_ad";
@@ -42267,6 +42268,7 @@ package android.service.notification {
method public int getRank();
method @NonNull public java.util.List<android.app.Notification.Action> getSmartActions();
method @NonNull public java.util.List<java.lang.CharSequence> getSmartReplies();
+ method @FlaggedApi("android.app.nm_summarization") @Nullable public String getSummarization();
method public int getSuppressedVisualEffects();
method public int getUserSentiment();
method public boolean isAmbient();
@@ -45145,7 +45147,7 @@ package android.telephony {
field public static final String KEY_SHOW_ICCID_IN_SIM_STATUS_BOOL = "show_iccid_in_sim_status_bool";
field public static final String KEY_SHOW_IMS_REGISTRATION_STATUS_BOOL = "show_ims_registration_status_bool";
field public static final String KEY_SHOW_ONSCREEN_DIAL_BUTTON_BOOL = "show_onscreen_dial_button_bool";
- field @FlaggedApi("com.android.internal.telephony.flags.hide_roaming_icon") public static final String KEY_SHOW_ROAMING_INDICATOR_BOOL = "show_roaming_indicator_bool";
+ field public static final String KEY_SHOW_ROAMING_INDICATOR_BOOL = "show_roaming_indicator_bool";
field public static final String KEY_SHOW_SIGNAL_STRENGTH_IN_SIM_STATUS_BOOL = "show_signal_strength_in_sim_status_bool";
field public static final String KEY_SHOW_VIDEO_CALL_CHARGES_ALERT_DIALOG_BOOL = "show_video_call_charges_alert_dialog_bool";
field public static final String KEY_SHOW_WFC_LOCATION_PRIVACY_POLICY_BOOL = "show_wfc_location_privacy_policy_bool";
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 937a9ffaf210..7483316e94b0 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -464,7 +464,7 @@ package android {
public static final class R.attr {
field public static final int allowClearUserDataOnFailedRestore = 16844288; // 0x1010600
- field @FlaggedApi("android.permission.flags.replace_body_sensor_permission_enabled") public static final int backgroundPermission;
+ field @FlaggedApi("android.permission.flags.replace_body_sensor_permission_enabled") public static final int backgroundPermission = 16844455; // 0x10106a7
field @FlaggedApi("android.content.res.manifest_flagging") public static final int featureFlag = 16844428; // 0x101068c
field public static final int gameSessionService = 16844373; // 0x1010655
field public static final int hotwordDetectionService = 16844326; // 0x1010626
@@ -543,7 +543,7 @@ package android {
field public static final int config_systemCallStreaming = 17039431; // 0x1040047
field public static final int config_systemCompanionDeviceProvider = 17039417; // 0x1040039
field public static final int config_systemContacts = 17039403; // 0x104002b
- field @FlaggedApi("android.content.pm.sdk_dependency_installer") public static final int config_systemDependencyInstaller;
+ field @FlaggedApi("android.content.pm.sdk_dependency_installer") public static final int config_systemDependencyInstaller = 17039434; // 0x104004a
field public static final int config_systemFinancedDeviceController = 17039430; // 0x1040046
field public static final int config_systemGallery = 17039399; // 0x1040027
field public static final int config_systemNotificationIntelligence = 17039413; // 0x1040035
@@ -555,7 +555,7 @@ package android {
field public static final int config_systemTextIntelligence = 17039414; // 0x1040036
field public static final int config_systemUi = 17039418; // 0x104003a
field public static final int config_systemUiIntelligence = 17039410; // 0x1040032
- field @FlaggedApi("android.permission.flags.system_vendor_intelligence_role_enabled") public static final int config_systemVendorIntelligence;
+ field @FlaggedApi("android.permission.flags.system_vendor_intelligence_role_enabled") public static final int config_systemVendorIntelligence = 17039435; // 0x104004b
field public static final int config_systemVisualIntelligence = 17039415; // 0x1040037
field public static final int config_systemWearHealthService = 17039428; // 0x1040044
field public static final int config_systemWellbeing = 17039408; // 0x1040030
@@ -3800,7 +3800,6 @@ package android.content {
field public static final String STATS_MANAGER = "stats";
field public static final String SYSTEM_CONFIG_SERVICE = "system_config";
field public static final String SYSTEM_UPDATE_SERVICE = "system_update";
- field public static final String TETHERING_SERVICE = "tethering";
field @FlaggedApi("com.android.net.thread.platform.flags.thread_enabled_platform") public static final String THREAD_NETWORK_SERVICE = "thread_network";
field public static final String TIME_MANAGER_SERVICE = "time_manager";
field public static final String TRANSLATION_MANAGER_SERVICE = "translation";
@@ -13446,6 +13445,7 @@ package android.service.notification {
field public static final String KEY_RANKING_SCORE = "key_ranking_score";
field public static final String KEY_SENSITIVE_CONTENT = "key_sensitive_content";
field public static final String KEY_SNOOZE_CRITERIA = "key_snooze_criteria";
+ field @FlaggedApi("android.app.nm_summarization") public static final String KEY_SUMMARIZATION = "key_summarization";
field public static final String KEY_TEXT_REPLIES = "key_text_replies";
field @FlaggedApi("android.service.notification.notification_classification") public static final String KEY_TYPE = "key_type";
field public static final String KEY_USER_SENTIMENT = "key_user_sentiment";
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 6e11cbc0c34b..36ef4f5f06ee 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1634,6 +1634,10 @@ package android.hardware.camera2.params {
method public void setColorSpace(@NonNull android.graphics.ColorSpace.Named);
}
+ @FlaggedApi("com.android.internal.camera.flags.camera_multi_client") public final class SharedSessionConfiguration {
+ ctor public SharedSessionConfiguration(int, @NonNull long[]);
+ }
+
}
package android.hardware.devicestate {
diff --git a/core/java/android/app/ActivityTaskManager.java b/core/java/android/app/ActivityTaskManager.java
index 16dcf2ad7e45..8d20e46c7df8 100644
--- a/core/java/android/app/ActivityTaskManager.java
+++ b/core/java/android/app/ActivityTaskManager.java
@@ -25,6 +25,7 @@ import android.annotation.SystemService;
import android.annotation.TestApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
+import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
@@ -483,6 +484,19 @@ public class ActivityTaskManager {
}
/**
+ * @return Whether the app could be universal resizeable (assuming it's on a large screen and
+ * ignoring possible overrides)
+ * @hide
+ */
+ public boolean canBeUniversalResizeable(@NonNull ApplicationInfo appInfo) {
+ try {
+ return getService().canBeUniversalResizeable(appInfo);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Detaches the navigation bar from the app it was attached to during a transition.
* @hide
*/
diff --git a/core/java/android/app/IActivityTaskManager.aidl b/core/java/android/app/IActivityTaskManager.aidl
index c6f62a21641d..4b1afa517122 100644
--- a/core/java/android/app/IActivityTaskManager.aidl
+++ b/core/java/android/app/IActivityTaskManager.aidl
@@ -158,6 +158,12 @@ interface IActivityTaskManager {
void reportAssistContextExtras(in IBinder assistToken, in Bundle extras,
in AssistStructure structure, in AssistContent content, in Uri referrer);
+ /**
+ * @return whether the app could be universal resizeable (assuming it's on a large screen and
+ * ignoring possible overrides)
+ */
+ boolean canBeUniversalResizeable(in ApplicationInfo appInfo);
+
void setFocusedRootTask(int taskId);
ActivityTaskManager.RootTaskInfo getFocusedRootTaskInfo();
Rect getTaskBounds(int taskId);
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index 8fa2362139a1..ff39329a0d2d 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -114,7 +114,6 @@ interface INotificationManager
NotificationChannel getNotificationChannelForPackage(String pkg, int uid, String channelId, String conversationId, boolean includeDeleted);
void deleteNotificationChannel(String pkg, String channelId);
ParceledListSlice getNotificationChannels(String callingPkg, String targetPkg, int userId);
- ParceledListSlice getOrCreateNotificationChannels(String callingPkg, String targetPkg, int userId, boolean createPrefsIfNeeded);
ParceledListSlice getNotificationChannelsForPackage(String pkg, int uid, boolean includeDeleted);
int getNumNotificationChannelsForPackage(String pkg, int uid, boolean includeDeleted);
int getDeletedChannelCount(String pkg, int uid);
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index fcb817ede6b3..40db6dd1b0ba 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -1772,6 +1772,11 @@ public class Notification implements Parcelable
*/
public static final String EXTRA_FOREGROUND_APPS = "android.foregroundApps";
+ /**
+ * @hide
+ */
+ public static final String EXTRA_SUMMARIZED_CONTENT = "android.summarization";
+
@UnsupportedAppUsage
private Icon mSmallIcon;
@UnsupportedAppUsage
@@ -4393,6 +4398,9 @@ public class Notification implements Parcelable
*/
@Nullable
public Pair<RemoteInput, Action> findRemoteInputActionPair(boolean requiresFreeform) {
+ if (isPromotedOngoing()) {
+ return null;
+ }
if (actions == null) {
return null;
}
@@ -6454,6 +6462,11 @@ public class Notification implements Parcelable
if (mActions == null) return Collections.emptyList();
List<Notification.Action> standardActions = new ArrayList<>();
for (Notification.Action action : mActions) {
+ // Actions with RemoteInput are ignored for RONs.
+ if (mN.isPromotedOngoing()
+ && hasValidRemoteInput(action)) {
+ continue;
+ }
if (!action.isContextual()) {
standardActions.add(action);
}
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index 1e1ec602d0a2..21dad28560df 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -1264,8 +1264,7 @@ public class NotificationManager {
mNotificationChannelListCache.query(new NotificationChannelQuery(
mContext.getOpPackageName(),
mContext.getPackageName(),
- mContext.getUserId(),
- true))); // create (default channel) if needed
+ mContext.getUserId())));
} else {
INotificationManager service = service();
try {
@@ -1293,8 +1292,7 @@ public class NotificationManager {
mNotificationChannelListCache.query(new NotificationChannelQuery(
mContext.getOpPackageName(),
mContext.getPackageName(),
- mContext.getUserId(),
- true))); // create (default channel) if needed
+ mContext.getUserId())));
} else {
INotificationManager service = service();
try {
@@ -1320,8 +1318,7 @@ public class NotificationManager {
return mNotificationChannelListCache.query(new NotificationChannelQuery(
mContext.getOpPackageName(),
mContext.getPackageName(),
- mContext.getUserId(),
- false));
+ mContext.getUserId()));
} else {
INotificationManager service = service();
try {
@@ -1461,8 +1458,8 @@ public class NotificationManager {
public List<NotificationChannel> apply(NotificationChannelQuery query) {
INotificationManager service = service();
try {
- return service.getOrCreateNotificationChannels(query.callingPkg,
- query.targetPkg, query.userId, query.createIfNeeded).getList();
+ return service.getNotificationChannels(query.callingPkg,
+ query.targetPkg, query.userId).getList();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -1490,8 +1487,7 @@ public class NotificationManager {
private record NotificationChannelQuery(
String callingPkg,
String targetPkg,
- int userId,
- boolean createIfNeeded) {}
+ int userId) {}
/**
* @hide
diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java
index b7285c38290c..e030c6c12b4c 100644
--- a/core/java/android/app/StatusBarManager.java
+++ b/core/java/android/app/StatusBarManager.java
@@ -60,6 +60,7 @@ import com.android.internal.statusbar.NotificationVisibility;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -195,12 +196,40 @@ public class StatusBarManager {
*/
private static final int DEFAULT_SIM_LOCKED_DISABLED_FLAGS = DISABLE_EXPAND;
- /** @hide */
- public static final int NAVIGATION_HINT_BACK_ALT = 1 << 0;
- /** @hide */
- public static final int NAVIGATION_HINT_IME_SHOWN = 1 << 1;
- /** @hide */
- public static final int NAVIGATION_HINT_IME_SWITCHER_SHOWN = 1 << 2;
+ /**
+ * The back button is visually adjusted to indicate that it will dismiss the IME when pressed.
+ * This only takes effect while the IME is visible. By default, it is set while the IME is
+ * visible, but may be overridden by the
+ * {@link android.inputmethodservice.InputMethodService.BackDispositionMode backDispositionMode}
+ * set by the IME.
+ *
+ * @hide
+ */
+ public static final int NAVIGATION_HINT_BACK_DISMISS_IME = 1 << 0;
+ /**
+ * The IME is visible.
+ *
+ * @hide
+ */
+ public static final int NAVIGATION_HINT_IME_VISIBLE = 1 << 1;
+ /**
+ * The IME Switcher button is visible. This only takes effect while the IME is visible.
+ *
+ * @hide
+ */
+ public static final int NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE = 1 << 2;
+ /**
+ * Navigation bar flags related to the IME state.
+ *
+ * @hide
+ */
+ @IntDef(flag = true, prefix = { "NAVIGATION_HINT_" }, value = {
+ NAVIGATION_HINT_BACK_DISMISS_IME,
+ NAVIGATION_HINT_IME_VISIBLE,
+ NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface NavigationHint {}
/** @hide */
public static final int WINDOW_STATUS_BAR = 1;
@@ -1325,6 +1354,22 @@ public class StatusBarManager {
}
/** @hide */
+ @NonNull
+ public static String navigationHintsToString(@NavigationHint int hints) {
+ final var hintStrings = new ArrayList<String>();
+ if ((hints & NAVIGATION_HINT_BACK_DISMISS_IME) != 0) {
+ hintStrings.add("NAVIGATION_HINT_BACK_DISMISS_IME");
+ }
+ if ((hints & NAVIGATION_HINT_IME_VISIBLE) != 0) {
+ hintStrings.add("NAVIGATION_HINT_IME_VISIBLE");
+ }
+ if ((hints & NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE) != 0) {
+ hintStrings.add("NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE");
+ }
+ return String.join(" | ", hintStrings);
+ }
+
+ /** @hide */
public static String windowStateToString(int state) {
if (state == WINDOW_STATE_HIDING) return "WINDOW_STATE_HIDING";
if (state == WINDOW_STATE_HIDDEN) return "WINDOW_STATE_HIDDEN";
diff --git a/core/java/android/app/UiAutomation.java b/core/java/android/app/UiAutomation.java
index c89cb553762b..8021ab4865af 100644
--- a/core/java/android/app/UiAutomation.java
+++ b/core/java/android/app/UiAutomation.java
@@ -228,7 +228,8 @@ public final class UiAutomation {
private OnAccessibilityEventListener mOnAccessibilityEventListener;
- private boolean mWaitingForEventDelivery;
+ // Count the nested clients waiting for data delivery
+ private int mCurrentEventWatchersCount = 0;
private long mLastEventTimeMillis;
@@ -1158,75 +1159,75 @@ public final class UiAutomation {
*/
public AccessibilityEvent executeAndWaitForEvent(Runnable command,
AccessibilityEventFilter filter, long timeoutMillis) throws TimeoutException {
+ int watchersDepth;
+ // Track events added after the index for this command, it is to support nested calls.
+ // This doesn't support concurrent calls correctly.
+ int eventQueueStartIndex;
+ final long executionStartTimeMillis;
+
// Acquire the lock and prepare for receiving events.
synchronized (mLock) {
throwIfNotConnectedLocked();
- mEventQueue.clear();
- // Prepare to wait for an event.
- mWaitingForEventDelivery = true;
+ watchersDepth = ++mCurrentEventWatchersCount;
+ executionStartTimeMillis = SystemClock.uptimeMillis();
+ eventQueueStartIndex = mEventQueue.size();
+ }
+ if (VERBOSE) {
+ Log.v(LOG_TAG, "executeAndWaitForEvent starts at depth=" + watchersDepth + ", "
+ + "command=" + command + ", filter=" + filter + ", timeout=" + timeoutMillis);
}
- // Note: We have to release the lock since calling out with this lock held
- // can bite. We will correctly filter out events from other interactions,
- // so starting to collect events before running the action is just fine.
-
- // We will ignore events from previous interactions.
- final long executionStartTimeMillis = SystemClock.uptimeMillis();
- // Execute the command *without* the lock being held.
- command.run();
-
- List<AccessibilityEvent> receivedEvents = new ArrayList<>();
-
- // Acquire the lock and wait for the event.
try {
- // Wait for the event.
- final long startTimeMillis = SystemClock.uptimeMillis();
- while (true) {
- List<AccessibilityEvent> localEvents = new ArrayList<>();
- synchronized (mLock) {
- localEvents.addAll(mEventQueue);
- mEventQueue.clear();
- }
- // Drain the event queue
- while (!localEvents.isEmpty()) {
- AccessibilityEvent event = localEvents.remove(0);
- // Ignore events from previous interactions.
- if (event.getEventTime() < executionStartTimeMillis) {
- continue;
- }
- if (filter.accept(event)) {
- return event;
- }
- receivedEvents.add(event);
- }
- // Check if timed out and if not wait.
- final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
- final long remainingTimeMillis = timeoutMillis - elapsedTimeMillis;
- if (remainingTimeMillis <= 0) {
- throw new TimeoutException("Expected event not received within: "
- + timeoutMillis + " ms among: " + receivedEvents);
+ // Execute the command *without* the lock being held.
+ command.run();
+ synchronized (mLock) {
+ if (watchersDepth != mCurrentEventWatchersCount) {
+ throw new IllegalStateException("Unexpected event watchers count, expected: "
+ + watchersDepth + ", actual: " + mCurrentEventWatchersCount);
}
+ }
+ final long startTimeMillis = SystemClock.uptimeMillis();
+ List<AccessibilityEvent> receivedEvents = new ArrayList<>();
+ long elapsedTimeMillis = 0;
+ int currentQueueSize = 0;
+ while (timeoutMillis > elapsedTimeMillis) {
+ AccessibilityEvent event = null;
synchronized (mLock) {
- if (mEventQueue.isEmpty()) {
+ currentQueueSize = mEventQueue.size();
+ if (eventQueueStartIndex < currentQueueSize) {
+ event = mEventQueue.get(eventQueueStartIndex++);
+ } else {
try {
- mLock.wait(remainingTimeMillis);
+ mLock.wait(timeoutMillis - elapsedTimeMillis);
} catch (InterruptedException ie) {
/* ignore */
}
}
}
+ elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
+ if (event == null || event.getEventTime() < executionStartTimeMillis) {
+ continue;
+ }
+ if (filter.accept(event)) {
+ return event;
+ }
+ receivedEvents.add(event);
}
- } finally {
- int size = receivedEvents.size();
- for (int i = 0; i < size; i++) {
- receivedEvents.get(i).recycle();
+ if (eventQueueStartIndex < currentQueueSize) {
+ Log.w(LOG_TAG, "Timed out before reading all events from the queue");
}
-
+ throw new TimeoutException("Expected event not received before timeout, events: "
+ + receivedEvents);
+ } finally {
synchronized (mLock) {
- mWaitingForEventDelivery = false;
- mEventQueue.clear();
+ if (--mCurrentEventWatchersCount == 0) {
+ mEventQueue.clear();
+ }
mLock.notifyAll();
}
+ if (VERBOSE) {
+ Log.v(LOG_TAG, "executeAndWaitForEvent ends at depth=" + watchersDepth);
+ }
}
}
@@ -1983,7 +1984,7 @@ public final class UiAutomation {
// It is not guaranteed that the accessibility framework sends events by the
// order of event timestamp.
mLastEventTimeMillis = Math.max(mLastEventTimeMillis, event.getEventTime());
- if (mWaitingForEventDelivery) {
+ if (mCurrentEventWatchersCount > 0) {
mEventQueue.add(AccessibilityEvent.obtain(event));
}
mLock.notifyAll();
diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig
index 914ca73f1ce4..733a348aa825 100644
--- a/core/java/android/app/notification.aconfig
+++ b/core/java/android/app/notification.aconfig
@@ -62,13 +62,6 @@ flag {
}
flag {
- name: "modes_ui_test"
- namespace: "systemui"
- description: "Guards new CTS tests for Modes; dependent on flags modes_api and modes_ui"
- bug: "360862012"
-}
-
-flag {
name: "modes_hsum"
namespace: "systemui"
description: "Fixes for modes (and DND/Zen in general) with HSUM or secondary users"
@@ -310,3 +303,17 @@ flag {
description: "removes sbnholder from NLS"
bug: "362981561"
}
+
+flag {
+ name: "nm_summarization"
+ namespace: "systemui"
+ description: "Allows the NAS to summarize notifications"
+ bug: "390417189"
+}
+
+flag {
+ name: "nm_summarization_ui"
+ namespace: "systemui"
+ description: "Shows summarized notifications in the UI"
+ bug: "390217880"
+}
diff --git a/core/java/android/app/supervision/ISupervisionManager.aidl b/core/java/android/app/supervision/ISupervisionManager.aidl
index 4598421eb3bc..c3f3b1ced33c 100644
--- a/core/java/android/app/supervision/ISupervisionManager.aidl
+++ b/core/java/android/app/supervision/ISupervisionManager.aidl
@@ -22,4 +22,5 @@ package android.app.supervision;
*/
interface ISupervisionManager {
boolean isSupervisionEnabledForUser(int userId);
+ String getActiveSupervisionAppPackage(int userId);
}
diff --git a/core/java/android/app/supervision/SupervisionManager.java b/core/java/android/app/supervision/SupervisionManager.java
index 92241f3634e8..12432ddd0eb9 100644
--- a/core/java/android/app/supervision/SupervisionManager.java
+++ b/core/java/android/app/supervision/SupervisionManager.java
@@ -16,6 +16,7 @@
package android.app.supervision;
+import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemService;
import android.annotation.UserHandleAware;
@@ -98,4 +99,20 @@ public class SupervisionManager {
throw e.rethrowFromSystemServer();
}
}
+
+ /**
+ * Returns the package name of the app that is acting as the active supervision app or null if
+ * supervision is disabled.
+ *
+ * @hide
+ */
+ @UserHandleAware
+ @Nullable
+ public String getActiveSupervisionAppPackage() {
+ try {
+ return mService.getActiveSupervisionAppPackage(mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/core/java/android/app/supervision/flags.aconfig b/core/java/android/app/supervision/flags.aconfig
index 18182b804627..232883cbfe00 100644
--- a/core/java/android/app/supervision/flags.aconfig
+++ b/core/java/android/app/supervision/flags.aconfig
@@ -48,3 +48,19 @@ flag {
description: "Flag that enables the Supervision settings screen with top-level Android settings entry point"
bug: "383404606"
}
+
+flag {
+ name: "enable_app_approval"
+ is_exported: true
+ namespace: "supervision"
+ description: "Flag to enable the App Approval settings in Android settings UI"
+ bug: "390185393"
+}
+
+flag {
+ name: "enable_supervision_pin_recovery_screen"
+ is_exported: true
+ namespace: "supervision"
+ description: "Flag that enables the Supervision pin recovery screen with Supervision settings entry point"
+ bug: "390500290"
+}
diff --git a/core/java/android/appwidget/AppWidgetHostView.java b/core/java/android/appwidget/AppWidgetHostView.java
index df1028e9e04c..b9b5c6a8bbc3 100644
--- a/core/java/android/appwidget/AppWidgetHostView.java
+++ b/core/java/android/appwidget/AppWidgetHostView.java
@@ -20,7 +20,6 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityOptions;
-import android.app.LoadedApk;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ComponentName;
import android.content.Context;
@@ -753,9 +752,6 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW
*/
protected Context getRemoteContextEnsuringCorrectCachedApkPath() {
try {
- ApplicationInfo expectedAppInfo = mInfo.providerInfo.applicationInfo;
- LoadedApk.checkAndUpdateApkPaths(expectedAppInfo);
- // Return if cloned successfully, otherwise default
Context newContext = mContext.createApplicationContext(
mInfo.providerInfo.applicationInfo,
Context.CONTEXT_RESTRICTED);
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index 6da2a073ec19..1cf42820f356 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -18,13 +18,6 @@ flag {
}
flag {
- namespace: "virtual_devices"
- name: "media_projection_keyguard_restrictions"
- description: "Auto-stop MP when the device locks"
- bug: "348335290"
-}
-
-flag {
namespace: "virtual_devices"
name: "virtual_display_insets"
description: "APIs for specifying virtual display insets (via cutout)"
diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java
index a126363237b8..efcaa0ea6f07 100644
--- a/core/java/android/content/ContentResolver.java
+++ b/core/java/android/content/ContentResolver.java
@@ -2722,10 +2722,10 @@ public abstract class ContentResolver implements ContentInterface {
/** @hide - designated user version */
@UnsupportedAppUsage
- public final void registerContentObserver(Uri uri, boolean notifyForDescendents,
+ public final void registerContentObserver(Uri uri, boolean notifyForDescendants,
ContentObserver observer, @UserIdInt int userHandle) {
try {
- getContentService().registerContentObserver(uri, notifyForDescendents,
+ getContentService().registerContentObserver(uri, notifyForDescendants,
observer.getContentObserver(), userHandle, mTargetSdkVersion);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 8d54673df74c..d811c0791c6c 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -4964,10 +4964,10 @@ public abstract class Context {
/**
* Use with {@link #getSystemService(String)} to retrieve a {@link android.net.TetheringManager}
* for managing tethering functions.
- * @hide
+ *
* @see android.net.TetheringManager
*/
- @SystemApi
+ @SuppressLint("UnflaggedApi")
public static final String TETHERING_SERVICE = "tethering";
/**
diff --git a/core/java/android/content/pm/UserInfo.java b/core/java/android/content/pm/UserInfo.java
index 8a3a3ad56a7b..582a1a9442ce 100644
--- a/core/java/android/content/pm/UserInfo.java
+++ b/core/java/android/content/pm/UserInfo.java
@@ -183,6 +183,12 @@ public class UserInfo implements Parcelable {
*
* <p>This is not necessarily the system user. For example, it will not be the system user on
* devices for which {@link UserManager#isHeadlessSystemUserMode()} returns true.
+ *
+ * <p>NB: Features should ideally not limit functionality to the main user. Ideally, they
+ * should either work for all users or for all admin users. If a feature should only work for
+ * select users, its determination of which user should be done intelligently or be
+ * customizable. Not all devices support a main user, and the idea of singling out one user as
+ * special is contrary to overall multiuser goals.
*/
public static final int FLAG_MAIN = 0x00004000;
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index 4a579a4c0e85..4e6fb8d3a8e7 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -500,6 +500,16 @@ flag {
}
flag {
+ name: "get_user_switchability_permission"
+ namespace: "multiuser"
+ description: "Update permissions for getUserSwitchability"
+ bug: "390458180"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "restrict_quiet_mode_credential_bug_fix_to_managed_profiles"
namespace: "profile_experiences"
description: "Use user states to check the state of quiet mode for managed profiles only"
diff --git a/core/java/android/credentials/flags.aconfig b/core/java/android/credentials/flags.aconfig
index 430ed2b68342..449423f1ea1f 100644
--- a/core/java/android/credentials/flags.aconfig
+++ b/core/java/android/credentials/flags.aconfig
@@ -133,6 +133,7 @@ flag {
metadata {
purpose: PURPOSE_BUGFIX
}
+ is_exported: true
}
flag {
diff --git a/core/java/android/hardware/biometrics/ParentalControlsUtilsInternal.java b/core/java/android/hardware/biometrics/ParentalControlsUtilsInternal.java
index de93234445ca..d3fb93588762 100644
--- a/core/java/android/hardware/biometrics/ParentalControlsUtilsInternal.java
+++ b/core/java/android/hardware/biometrics/ParentalControlsUtilsInternal.java
@@ -19,6 +19,7 @@ package android.hardware.biometrics;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.admin.DevicePolicyManager;
+import android.app.supervision.SupervisionManager;
import android.content.ComponentName;
import android.content.Context;
import android.os.Build;
@@ -55,27 +56,44 @@ public class ParentalControlsUtilsInternal {
return null;
}
- public static boolean parentConsentRequired(@NonNull Context context,
- @NonNull DevicePolicyManager dpm, @BiometricAuthenticator.Modality int modality,
+ /** @return true if parental consent is required in order for biometric sensors to be used. */
+ public static boolean parentConsentRequired(
+ @NonNull Context context,
+ @NonNull DevicePolicyManager dpm,
+ @Nullable SupervisionManager sm,
+ @BiometricAuthenticator.Modality int modality,
@NonNull UserHandle userHandle) {
if (getTestComponentName(context, userHandle.getIdentifier()) != null) {
return true;
}
- return parentConsentRequired(dpm, modality, userHandle);
+ return parentConsentRequired(dpm, sm, modality, userHandle);
}
/**
* @return true if parental consent is required in order for biometric sensors to be used.
*/
- public static boolean parentConsentRequired(@NonNull DevicePolicyManager dpm,
- @BiometricAuthenticator.Modality int modality, @NonNull UserHandle userHandle) {
- final ComponentName cn = getSupervisionComponentName(dpm, userHandle);
- if (cn == null) {
- return false;
+ public static boolean parentConsentRequired(
+ @NonNull DevicePolicyManager dpm,
+ @Nullable SupervisionManager sm,
+ @BiometricAuthenticator.Modality int modality,
+ @NonNull UserHandle userHandle) {
+ final int keyguardDisabledFeatures;
+
+ if (android.app.supervision.flags.Flags.deprecateDpmSupervisionApis()) {
+ if (sm != null && !sm.isSupervisionEnabledForUser(userHandle.getIdentifier())) {
+ return false;
+ }
+ // Check for keyguard features disabled by any admin.
+ keyguardDisabledFeatures = dpm.getKeyguardDisabledFeatures(/* admin= */ null);
+ } else {
+ final ComponentName cn = getSupervisionComponentName(dpm, userHandle);
+ if (cn == null) {
+ return false;
+ }
+ keyguardDisabledFeatures = dpm.getKeyguardDisabledFeatures(cn);
}
- final int keyguardDisabledFeatures = dpm.getKeyguardDisabledFeatures(cn);
final boolean dpmFpDisabled = containsFlag(keyguardDisabledFeatures,
DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT);
final boolean dpmFaceDisabled = containsFlag(keyguardDisabledFeatures,
@@ -97,7 +115,9 @@ public class ParentalControlsUtilsInternal {
return consentRequired;
}
+ /** @deprecated Use {@link SupervisionManager} to check for supervision. */
@Nullable
+ @Deprecated
public static ComponentName getSupervisionComponentName(@NonNull DevicePolicyManager dpm,
@NonNull UserHandle userHandle) {
return dpm.getProfileOwnerOrDeviceOwnerSupervisionComponent(userHandle);
diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java
index 5533a640b9d8..210653bb41e5 100644
--- a/core/java/android/hardware/camera2/CameraCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraCharacteristics.java
@@ -5256,9 +5256,6 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri
* <p>DYNAMIC_RANGE_PROFILE: {STANDARD, HLG10}</p>
* </li>
* </ul>
- * <p>All of the above configurations can be set up with a SessionConfiguration. The list of
- * OutputConfiguration contains the stream configurations and DYNAMIC_RANGE_PROFILE, and
- * the AE_TARGET_FPS_RANGE and VIDEO_STABILIZATION_MODE are set as session parameters.</p>
* <p>When set to BAKLAVA, the additional stream combinations below are verified
* by the compliance tests:</p>
* <table>
@@ -5268,6 +5265,8 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri
* <th style="text-align: center;">Size</th>
* <th style="text-align: center;">Target 2</th>
* <th style="text-align: center;">Size</th>
+ * <th style="text-align: center;">Target 3</th>
+ * <th style="text-align: center;">Size</th>
* </tr>
* </thead>
* <tbody>
@@ -5276,15 +5275,34 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri
* <td style="text-align: center;">S1080P</td>
* <td style="text-align: center;">PRIV</td>
* <td style="text-align: center;">S1080P</td>
+ * <td style="text-align: center;"></td>
+ * <td style="text-align: center;"></td>
* </tr>
* <tr>
* <td style="text-align: center;">PRIV</td>
* <td style="text-align: center;">S1080P</td>
* <td style="text-align: center;">PRIV</td>
* <td style="text-align: center;">S1440P</td>
+ * <td style="text-align: center;"></td>
+ * <td style="text-align: center;"></td>
+ * </tr>
+ * <tr>
+ * <td style="text-align: center;">PRIV</td>
+ * <td style="text-align: center;">S1080P</td>
+ * <td style="text-align: center;">YUV</td>
+ * <td style="text-align: center;">S1080P</td>
+ * <td style="text-align: center;">S1080P</td>
+ * <td style="text-align: center;">PRIV</td>
* </tr>
* </tbody>
* </table>
+ * <ul>
+ * <li>VIDEO_STABILIZATION_MODE: {OFF, ON} for the newly added stream combinations given the
+ * presence of dedicated video stream</li>
+ * </ul>
+ * <p>All of the above configurations can be set up with a SessionConfiguration. The list of
+ * OutputConfiguration contains the stream configurations and DYNAMIC_RANGE_PROFILE, and
+ * the AE_TARGET_FPS_RANGE and VIDEO_STABILIZATION_MODE are set as session parameters.</p>
* <p>This key is available on all devices.</p>
*/
@PublicKey
diff --git a/core/java/android/hardware/camera2/params/SharedSessionConfiguration.java b/core/java/android/hardware/camera2/params/SharedSessionConfiguration.java
index 365f870ba22d..b40c7d35c06b 100644
--- a/core/java/android/hardware/camera2/params/SharedSessionConfiguration.java
+++ b/core/java/android/hardware/camera2/params/SharedSessionConfiguration.java
@@ -21,6 +21,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
+import android.annotation.TestApi;
import android.graphics.ColorSpace;
import android.graphics.ImageFormat.Format;
import android.hardware.DataSpace.NamedDataSpace;
@@ -228,6 +229,7 @@ public final class SharedSessionConfiguration {
*
* @hide
*/
+ @TestApi
public SharedSessionConfiguration(int sharedColorSpace,
@NonNull long[] sharedOutputConfigurations) {
mColorSpace = sharedColorSpace;
diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig
index 62126963cba4..79323bf2f2f7 100644
--- a/core/java/android/hardware/input/input_framework.aconfig
+++ b/core/java/android/hardware/input/input_framework.aconfig
@@ -225,3 +225,13 @@ flag {
description: "Removes modifiers from the original key event that activated the fallback, ensuring that only the intended fallback event is sent."
bug: "382545048"
}
+
+flag {
+ name: "abort_slow_multi_press"
+ namespace: "wear_frameworks"
+ description: "If a press that's a part of a multipress takes too long, the multipress gesture will be cancelled."
+ bug: "370095426"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/android/hardware/usb/OWNERS b/core/java/android/hardware/usb/OWNERS
index 37604bc2eb65..1de8a242acfc 100644
--- a/core/java/android/hardware/usb/OWNERS
+++ b/core/java/android/hardware/usb/OWNERS
@@ -1,7 +1,7 @@
# Bug component: 175220
-anothermark@google.com
+vmartensson@google.com
+nkapron@google.com
febinthattil@google.com
-aprasath@google.com
+shubhankarm@google.com
badhri@google.com
-kumarashishg@google.com \ No newline at end of file
diff --git a/core/java/android/inputmethodservice/NavigationBarController.java b/core/java/android/inputmethodservice/NavigationBarController.java
index 019ba0045916..13352d716ffa 100644
--- a/core/java/android/inputmethodservice/NavigationBarController.java
+++ b/core/java/android/inputmethodservice/NavigationBarController.java
@@ -16,9 +16,9 @@
package android.inputmethodservice;
-import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT;
-import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN;
-import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN;
+import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_DISMISS_IME;
+import static android.app.StatusBarManager.NAVIGATION_HINT_IME_VISIBLE;
+import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE;
import static android.view.WindowInsets.Type.captionBar;
import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
@@ -242,10 +242,10 @@ final class NavigationBarController {
NavigationBarView.class::isInstance);
if (navigationBarView != null) {
// TODO(b/213337792): Support InputMethodService#setBackDisposition().
- // TODO(b/213337792): Set NAVIGATION_HINT_IME_SHOWN only when necessary.
- final int hints = NAVIGATION_HINT_BACK_ALT | NAVIGATION_HINT_IME_SHOWN
+ // TODO(b/213337792): Set NAVIGATION_HINT_IME_VISIBLE only when necessary.
+ final int hints = NAVIGATION_HINT_BACK_DISMISS_IME | NAVIGATION_HINT_IME_VISIBLE
| (mShouldShowImeSwitcherWhenImeIsShown
- ? NAVIGATION_HINT_IME_SWITCHER_SHOWN : 0);
+ ? NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE : 0);
navigationBarView.setNavigationIconHints(hints);
navigationBarView.prepareNavButtons(this);
}
@@ -515,10 +515,10 @@ final class NavigationBarController {
NavigationBarView.class::isInstance);
if (navigationBarView != null) {
// TODO(b/213337792): Support InputMethodService#setBackDisposition().
- // TODO(b/213337792): Set NAVIGATION_HINT_IME_SHOWN only when necessary.
- final int hints = NAVIGATION_HINT_BACK_ALT | NAVIGATION_HINT_IME_SHOWN
+ // TODO(b/213337792): Set NAVIGATION_HINT_IME_VISIBLE only when necessary.
+ final int hints = NAVIGATION_HINT_BACK_DISMISS_IME | NAVIGATION_HINT_IME_VISIBLE
| (mShouldShowImeSwitcherWhenImeIsShown
- ? NAVIGATION_HINT_IME_SWITCHER_SHOWN : 0);
+ ? NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE : 0);
navigationBarView.setNavigationIconHints(hints);
}
} else {
diff --git a/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java b/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java
index e7e46a9482c8..4be98c46300d 100644
--- a/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java
+++ b/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java
@@ -16,6 +16,8 @@
package android.inputmethodservice.navigationbar;
+import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_DISMISS_IME;
+import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE;
import static android.inputmethodservice.navigationbar.NavigationBarConstants.DARK_MODE_ICON_COLOR_SINGLE_TONE;
import static android.inputmethodservice.navigationbar.NavigationBarConstants.LIGHT_MODE_ICON_COLOR_SINGLE_TONE;
import static android.inputmethodservice.navigationbar.NavigationBarConstants.NAVBAR_BACK_BUTTON_IME_OFFSET;
@@ -28,6 +30,7 @@ import android.annotation.DrawableRes;
import android.annotation.FloatRange;
import android.annotation.NonNull;
import android.app.StatusBarManager;
+import android.app.StatusBarManager.NavigationHint;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Canvas;
@@ -63,7 +66,8 @@ public final class NavigationBarView extends FrameLayout {
private int mCurrentRotation = -1;
int mDisabledFlags = 0;
- int mNavigationIconHints = StatusBarManager.NAVIGATION_HINT_BACK_ALT;
+ @NavigationHint
+ private int mNavigationIconHints = 0;
private final int mNavBarMode = NAV_BAR_MODE_GESTURAL;
private KeyButtonDrawable mBackIcon;
@@ -241,10 +245,10 @@ public final class NavigationBarView extends FrameLayout {
}
private void orientBackButton(KeyButtonDrawable drawable) {
- final boolean useAltBack =
- (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0;
+ final boolean isBackDismissIme =
+ (mNavigationIconHints & NAVIGATION_HINT_BACK_DISMISS_IME) != 0;
final boolean isRtl = mConfiguration.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
- float degrees = useAltBack ? (isRtl ? 90 : -90) : 0;
+ float degrees = isBackDismissIme ? (isRtl ? 90 : -90) : 0;
if (drawable.getRotation() == degrees) {
return;
}
@@ -256,7 +260,7 @@ public final class NavigationBarView extends FrameLayout {
// Animate the back button's rotation to the new degrees and only in portrait move up the
// back button to line up with the other buttons
- float targetY = useAltBack
+ float targetY = isBackDismissIme
? -dpToPx(NAVBAR_BACK_BUTTON_IME_OFFSET, getResources())
: 0;
ObjectAnimator navBarAnimator = ObjectAnimator.ofPropertyValuesHolder(drawable,
@@ -284,13 +288,16 @@ public final class NavigationBarView extends FrameLayout {
*
* @param hints bit flags defined in {@link StatusBarManager}.
*/
- public void setNavigationIconHints(int hints) {
- if (hints == mNavigationIconHints) return;
- final boolean newBackAlt = (hints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0;
- final boolean oldBackAlt =
- (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0;
- if (newBackAlt != oldBackAlt) {
- //onBackAltChanged(newBackAlt);
+ public void setNavigationIconHints(@NavigationHint int hints) {
+ if (hints == mNavigationIconHints) {
+ return;
+ }
+ final boolean backDismissIme =
+ (hints & StatusBarManager.NAVIGATION_HINT_BACK_DISMISS_IME) != 0;
+ final boolean oldBackDismissIme =
+ (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_DISMISS_IME) != 0;
+ if (backDismissIme != oldBackDismissIme) {
+ //onBackDismissImeChanged(backDismissIme);
}
if (DEBUG) {
@@ -311,10 +318,12 @@ public final class NavigationBarView extends FrameLayout {
getImeSwitchButton().setImageDrawable(mImeSwitcherIcon);
- // Update IME button visibility, a11y and rotate button always overrides the appearance
- final boolean imeSwitcherVisible =
- (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN) != 0;
- getImeSwitchButton().setVisibility(imeSwitcherVisible ? View.VISIBLE : View.INVISIBLE);
+ // Update IME switcher button visibility, a11y and rotate button always overrides
+ // the appearance.
+ final boolean isImeSwitcherButtonVisible =
+ (mNavigationIconHints & NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE) != 0;
+ getImeSwitchButton()
+ .setVisibility(isImeSwitcherButtonVisible ? View.VISIBLE : View.INVISIBLE);
getBackButton().setVisibility(View.VISIBLE);
getHomeHandle().setVisibility(View.INVISIBLE);
diff --git a/core/java/android/os/BaseBundle.java b/core/java/android/os/BaseBundle.java
index 1041041b2a27..1cf293d46350 100644
--- a/core/java/android/os/BaseBundle.java
+++ b/core/java/android/os/BaseBundle.java
@@ -45,8 +45,7 @@ import java.util.function.BiFunction;
* {@link PersistableBundle} subclass.
*/
@android.ravenwood.annotation.RavenwoodKeepWholeClass
-@SuppressWarnings("HiddenSuperclass")
-public class BaseBundle implements Parcel.ClassLoaderProvider {
+public class BaseBundle {
/** @hide */
protected static final String TAG = "Bundle";
static final boolean DEBUG = false;
@@ -300,9 +299,8 @@ public class BaseBundle implements Parcel.ClassLoaderProvider {
/**
* Return the ClassLoader currently associated with this Bundle.
- * @hide
*/
- public ClassLoader getClassLoader() {
+ ClassLoader getClassLoader() {
return mClassLoader;
}
@@ -416,9 +414,6 @@ public class BaseBundle implements Parcel.ClassLoaderProvider {
if ((mFlags & Bundle.FLAG_VERIFY_TOKENS_PRESENT) != 0) {
Intent.maybeMarkAsMissingCreatorToken(object);
}
- } else if (object instanceof Bundle) {
- Bundle bundle = (Bundle) object;
- bundle.setClassLoaderSameAsContainerBundleWhenRetrievedFirstTime(this);
}
return (clazz != null) ? clazz.cast(object) : (T) object;
}
@@ -492,7 +487,7 @@ public class BaseBundle implements Parcel.ClassLoaderProvider {
int[] numLazyValues = new int[]{0};
try {
parcelledData.readArrayMap(map, count, !parcelledByNative,
- /* lazy */ ownsParcel, this, numLazyValues);
+ /* lazy */ ownsParcel, mClassLoader, numLazyValues);
} catch (BadParcelableException e) {
if (sShouldDefuse) {
Log.w(TAG, "Failed to parse Bundle, but defusing quietly", e);
diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java
index ee62dea7f9e5..6b1e918a3c47 100644
--- a/core/java/android/os/Binder.java
+++ b/core/java/android/os/Binder.java
@@ -149,6 +149,11 @@ public class Binder implements IBinder {
private static volatile boolean sStackTrackingEnabled = false;
/**
+ * The extension binder object
+ */
+ private IBinder mExtension = null;
+
+ /**
* Enable Binder IPC stack tracking. If enabled, every binder transaction will be logged to
* {@link TransactionTracker}.
*
@@ -1237,7 +1242,9 @@ public class Binder implements IBinder {
/** @hide */
@Override
- public final native @Nullable IBinder getExtension();
+ public final @Nullable IBinder getExtension() {
+ return mExtension;
+ }
/**
* Set the binder extension.
@@ -1245,7 +1252,12 @@ public class Binder implements IBinder {
*
* @hide
*/
- public final native void setExtension(@Nullable IBinder extension);
+ public final void setExtension(@Nullable IBinder extension) {
+ mExtension = extension;
+ setExtensionNative(extension);
+ }
+
+ private final native void setExtensionNative(@Nullable IBinder extension);
/**
* Default implementation rewinds the parcels and calls onTransact. On
diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java
index 55bfd451d97a..819d58d9f059 100644
--- a/core/java/android/os/Bundle.java
+++ b/core/java/android/os/Bundle.java
@@ -141,8 +141,6 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
STRIPPED.putInt("STRIPPED", 1);
}
- private boolean isFirstRetrievedFromABundle = false;
-
/**
* Constructs a new, empty Bundle.
*/
@@ -1022,9 +1020,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
return null;
}
try {
- Bundle bundle = (Bundle) o;
- bundle.setClassLoaderSameAsContainerBundleWhenRetrievedFirstTime(this);
- return bundle;
+ return (Bundle) o;
} catch (ClassCastException e) {
typeWarning(key, o, "Bundle", e);
return null;
@@ -1032,21 +1028,6 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
}
/**
- * Set the ClassLoader of a bundle to its container bundle. This is necessary so that when a
- * bundle's ClassLoader is changed, it can be propagated to its children. Do this only when it
- * is retrieved from the container bundle first time though. Once it is accessed outside of its
- * container, its ClassLoader should no longer be changed by its container anymore.
- *
- * @param containerBundle the bundle this bundle is retrieved from.
- */
- void setClassLoaderSameAsContainerBundleWhenRetrievedFirstTime(BaseBundle containerBundle) {
- if (!isFirstRetrievedFromABundle) {
- setClassLoader(containerBundle.getClassLoader());
- isFirstRetrievedFromABundle = true;
- }
- }
-
- /**
* Returns the value associated with the given key, or {@code null} if
* no mapping of the desired type exists for the given key or a {@code null}
* value is explicitly associated with the key.
diff --git a/core/java/android/os/CombinedMessageQueue/MessageQueue.java b/core/java/android/os/CombinedMessageQueue/MessageQueue.java
index 50b621c46778..1e663342522b 100644
--- a/core/java/android/os/CombinedMessageQueue/MessageQueue.java
+++ b/core/java/android/os/CombinedMessageQueue/MessageQueue.java
@@ -39,7 +39,6 @@ import android.util.proto.ProtoOutputStream;
import com.android.internal.ravenwood.RavenwoodEnvironment;
import dalvik.annotation.optimization.NeverCompile;
-import dalvik.annotation.optimization.NeverInline;
import java.io.FileDescriptor;
import java.lang.annotation.Retention;
@@ -238,7 +237,6 @@ public final class MessageQueue {
private final MatchDeliverableMessages mMatchDeliverableMessages =
new MatchDeliverableMessages();
- @NeverInline
private boolean isIdleConcurrent() {
final long now = SystemClock.uptimeMillis();
@@ -269,7 +267,6 @@ public final class MessageQueue {
return true;
}
- @NeverInline
private boolean isIdleLegacy() {
synchronized (this) {
final long now = SystemClock.uptimeMillis();
@@ -292,14 +289,12 @@ public final class MessageQueue {
}
}
- @NeverInline
private void addIdleHandlerConcurrent(@NonNull IdleHandler handler) {
synchronized (mIdleHandlersLock) {
mIdleHandlers.add(handler);
}
}
- @NeverInline
private void addIdleHandlerLegacy(@NonNull IdleHandler handler) {
synchronized (this) {
mIdleHandlers.add(handler);
@@ -326,15 +321,11 @@ public final class MessageQueue {
addIdleHandlerLegacy(handler);
}
}
-
- @NeverInline
private void removeIdleHandlerConcurrent(@NonNull IdleHandler handler) {
synchronized (mIdleHandlersLock) {
mIdleHandlers.remove(handler);
}
}
-
- @NeverInline
private void removeIdleHandlerLegacy(@NonNull IdleHandler handler) {
synchronized (this) {
mIdleHandlers.remove(handler);
@@ -358,14 +349,12 @@ public final class MessageQueue {
}
}
- @NeverInline
private boolean isPollingConcurrent() {
// If the loop is quitting then it must not be idling.
// We can assume mPtr != 0 when sQuitting is false.
return !((boolean) sQuitting.getVolatile(this)) && nativeIsPolling(mPtr);
}
- @NeverInline
private boolean isPollingLegacy() {
synchronized (this) {
return isPollingLocked();
@@ -396,7 +385,6 @@ public final class MessageQueue {
// We can assume mPtr != 0 when mQuitting is false.
return !mQuitting && nativeIsPolling(mPtr);
}
- @NeverInline
private void addOnFileDescriptorEventListenerConcurrent(@NonNull FileDescriptor fd,
@OnFileDescriptorEventListener.Events int events,
@NonNull OnFileDescriptorEventListener listener) {
@@ -405,7 +393,6 @@ public final class MessageQueue {
}
}
- @NeverInline
private void addOnFileDescriptorEventListenerLegacy(@NonNull FileDescriptor fd,
@OnFileDescriptorEventListener.Events int events,
@NonNull OnFileDescriptorEventListener listener) {
@@ -455,14 +442,12 @@ public final class MessageQueue {
}
}
- @NeverInline
private void removeOnFileDescriptorEventListenerConcurrent(@NonNull FileDescriptor fd) {
synchronized (mFileDescriptorRecordsLock) {
updateOnFileDescriptorEventListenerLocked(fd, 0, null);
}
}
- @NeverInline
private void removeOnFileDescriptorEventListenerLegacy(@NonNull FileDescriptor fd) {
synchronized (this) {
updateOnFileDescriptorEventListenerLocked(fd, 0, null);
@@ -796,7 +781,6 @@ public final class MessageQueue {
}
}
- @NeverInline
private Message nextConcurrent() {
final long ptr = mPtr;
if (ptr == 0) {
@@ -871,7 +855,6 @@ public final class MessageQueue {
}
}
- @NeverInline
private Message nextLegacy() {
// Return here if the message loop has already quit and been disposed.
// This can happen if the application tries to restart a looper after quit
@@ -1036,13 +1019,11 @@ public final class MessageQueue {
}
}
- @NeverInline
private int postSyncBarrierConcurrent() {
return postSyncBarrier(SystemClock.uptimeMillis());
}
- @NeverInline
private int postSyncBarrierLegacy() {
return postSyncBarrier(SystemClock.uptimeMillis());
}
@@ -1162,7 +1143,6 @@ public final class MessageQueue {
}
}
- @NeverInline
private void removeSyncBarrierConcurrent(int token) {
boolean removed;
MessageNode first;
@@ -1189,7 +1169,6 @@ public final class MessageQueue {
}
}
- @NeverInline
private void removeSyncBarrierLegacy(int token) {
synchronized (this) {
Message prev = null;
@@ -1249,7 +1228,6 @@ public final class MessageQueue {
}
- @NeverInline
private boolean enqueueMessageConcurrent(Message msg, long when) {
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
@@ -1258,7 +1236,6 @@ public final class MessageQueue {
return enqueueMessageUnchecked(msg, when);
}
- @NeverInline
private boolean enqueueMessageLegacy(Message msg, long when) {
synchronized (this) {
if (msg.isInUse()) {
@@ -1495,13 +1472,11 @@ public final class MessageQueue {
private final MatchHandlerWhatAndObject mMatchHandlerWhatAndObject =
new MatchHandlerWhatAndObject();
- @NeverInline
private boolean hasMessagesConcurrent(Handler h, int what, Object object) {
return findOrRemoveMessages(h, what, object, null, 0, mMatchHandlerWhatAndObject,
false);
}
- @NeverInline
private boolean hasMessagesLegacy(Handler h, int what, Object object) {
synchronized (this) {
Message p = mMessages;
@@ -1540,13 +1515,11 @@ public final class MessageQueue {
private final MatchHandlerWhatAndObjectEquals mMatchHandlerWhatAndObjectEquals =
new MatchHandlerWhatAndObjectEquals();
- @NeverInline
private boolean hasEqualMessagesConcurrent(Handler h, int what, Object object) {
return findOrRemoveMessages(h, what, object, null, 0, mMatchHandlerWhatAndObjectEquals,
false);
}
- @NeverInline
private boolean hasEqualMessagesLegacy(Handler h, int what, Object object) {
synchronized (this) {
Message p = mMessages;
@@ -1585,13 +1558,11 @@ public final class MessageQueue {
private final MatchHandlerRunnableAndObject mMatchHandlerRunnableAndObject =
new MatchHandlerRunnableAndObject();
- @NeverInline
private boolean hasMessagesConcurrent(Handler h, Runnable r, Object object) {
return findOrRemoveMessages(h, -1, object, r, 0, mMatchHandlerRunnableAndObject,
false);
}
- @NeverInline
private boolean hasMessagesLegacy(Handler h, Runnable r, Object object) {
synchronized (this) {
Message p = mMessages;
@@ -1626,12 +1597,10 @@ public final class MessageQueue {
}
private final MatchHandler mMatchHandler = new MatchHandler();
- @NeverInline
private boolean hasMessagesConcurrent(Handler h) {
return findOrRemoveMessages(h, -1, null, null, 0, mMatchHandler, false);
}
- @NeverInline
private boolean hasMessagesLegacy(Handler h) {
synchronized (this) {
Message p = mMessages;
@@ -1656,12 +1625,10 @@ public final class MessageQueue {
}
}
- @NeverInline
private void removeMessagesConcurrent(Handler h, int what, Object object) {
findOrRemoveMessages(h, what, object, null, 0, mMatchHandlerWhatAndObject, true);
}
- @NeverInline
private void removeMessagesLegacy(Handler h, int what, Object object) {
synchronized (this) {
Message p = mMessages;
@@ -1716,12 +1683,10 @@ public final class MessageQueue {
}
}
- @NeverInline
private void removeEqualMessagesConcurrent(Handler h, int what, Object object) {
findOrRemoveMessages(h, what, object, null, 0, mMatchHandlerWhatAndObjectEquals, true);
}
- @NeverInline
private void removeEqualMessagesLegacy(Handler h, int what, Object object) {
synchronized (this) {
Message p = mMessages;
@@ -1777,12 +1742,10 @@ public final class MessageQueue {
}
}
- @NeverInline
private void removeMessagesConcurrent(Handler h, Runnable r, Object object) {
findOrRemoveMessages(h, -1, object, r, 0, mMatchHandlerRunnableAndObject, true);
}
- @NeverInline
private void removeMessagesLegacy(Handler h, Runnable r, Object object) {
synchronized (this) {
Message p = mMessages;
@@ -1852,12 +1815,10 @@ public final class MessageQueue {
private final MatchHandlerRunnableAndObjectEquals mMatchHandlerRunnableAndObjectEquals =
new MatchHandlerRunnableAndObjectEquals();
- @NeverInline
private void removeEqualMessagesConcurrent(Handler h, Runnable r, Object object) {
findOrRemoveMessages(h, -1, object, r, 0, mMatchHandlerRunnableAndObjectEquals, true);
}
- @NeverInline
private void removeEqualMessagesLegacy(Handler h, Runnable r, Object object) {
synchronized (this) {
Message p = mMessages;
@@ -1926,12 +1887,10 @@ public final class MessageQueue {
}
private final MatchHandlerAndObject mMatchHandlerAndObject = new MatchHandlerAndObject();
- @NeverInline
private void removeCallbacksAndMessagesConcurrent(Handler h, Object object) {
findOrRemoveMessages(h, -1, object, null, 0, mMatchHandlerAndObject, true);
}
- @NeverInline
private void removeCallbacksAndMessagesLegacy(Handler h, Object object) {
synchronized (this) {
Message p = mMessages;
@@ -2000,12 +1959,10 @@ public final class MessageQueue {
private final MatchHandlerAndObjectEquals mMatchHandlerAndObjectEquals =
new MatchHandlerAndObjectEquals();
- @NeverInline
void removeCallbacksAndEqualMessagesConcurrent(Handler h, Object object) {
findOrRemoveMessages(h, -1, object, null, 0, mMatchHandlerAndObjectEquals, true);
}
- @NeverInline
void removeCallbacksAndEqualMessagesLegacy(Handler h, Object object) {
synchronized (this) {
Message p = mMessages;
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index 3c4139d39762..e58934746c14 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -4661,7 +4661,7 @@ public final class Parcel {
* @hide
*/
@Nullable
- private Object readLazyValue(@Nullable ClassLoaderProvider loaderProvider) {
+ public Object readLazyValue(@Nullable ClassLoader loader) {
int start = dataPosition();
int type = readInt();
if (isLengthPrefixed(type)) {
@@ -4672,17 +4672,12 @@ public final class Parcel {
int end = MathUtils.addOrThrow(dataPosition(), objectLength);
int valueLength = end - start;
setDataPosition(end);
- return new LazyValue(this, start, valueLength, type, loaderProvider);
+ return new LazyValue(this, start, valueLength, type, loader);
} else {
- return readValue(type, getClassLoader(loaderProvider), /* clazz */ null);
+ return readValue(type, loader, /* clazz */ null);
}
}
- @Nullable
- private static ClassLoader getClassLoader(@Nullable ClassLoaderProvider loaderProvider) {
- return loaderProvider == null ? null : loaderProvider.getClassLoader();
- }
-
private static final class LazyValue implements BiFunction<Class<?>, Class<?>[], Object> {
/**
@@ -4696,12 +4691,7 @@ public final class Parcel {
private final int mPosition;
private final int mLength;
private final int mType;
- // this member is set when a bundle that includes a LazyValue is unparceled. But it is used
- // when apply method is called. Between these 2 events, the bundle's ClassLoader could have
- // changed. Let the bundle be a ClassLoaderProvider allows the bundle provides its current
- // ClassLoader at the time apply method is called.
- @NonNull
- private final ClassLoaderProvider mLoaderProvider;
+ @Nullable private final ClassLoader mLoader;
@Nullable private Object mObject;
/**
@@ -4712,13 +4702,12 @@ public final class Parcel {
*/
@Nullable private volatile Parcel mSource;
- LazyValue(Parcel source, int position, int length, int type,
- @NonNull ClassLoaderProvider loaderProvider) {
+ LazyValue(Parcel source, int position, int length, int type, @Nullable ClassLoader loader) {
mSource = requireNonNull(source);
mPosition = position;
mLength = length;
mType = type;
- mLoaderProvider = loaderProvider;
+ mLoader = loader;
}
@Override
@@ -4731,8 +4720,7 @@ public final class Parcel {
int restore = source.dataPosition();
try {
source.setDataPosition(mPosition);
- mObject = source.readValue(mLoaderProvider.getClassLoader(), clazz,
- itemTypes);
+ mObject = source.readValue(mLoader, clazz, itemTypes);
} finally {
source.setDataPosition(restore);
}
@@ -4805,8 +4793,7 @@ public final class Parcel {
return Objects.equals(mObject, value.mObject);
}
// Better safely fail here since this could mean we get different objects.
- if (!Objects.equals(mLoaderProvider.getClassLoader(),
- value.mLoaderProvider.getClassLoader())) {
+ if (!Objects.equals(mLoader, value.mLoader)) {
return false;
}
// Otherwise compare metadata prior to comparing payload.
@@ -4820,24 +4807,10 @@ public final class Parcel {
@Override
public int hashCode() {
// Accessing mSource first to provide memory barrier for mObject
- return Objects.hash(mSource == null, mObject, mLoaderProvider.getClassLoader(), mType,
- mLength);
+ return Objects.hash(mSource == null, mObject, mLoader, mType, mLength);
}
}
- /**
- * Provides a ClassLoader.
- * @hide
- */
- public interface ClassLoaderProvider {
- /**
- * Returns a ClassLoader.
- *
- * @return ClassLoader
- */
- ClassLoader getClassLoader();
- }
-
/** Same as {@link #readValue(ClassLoader, Class, Class[])} without any item types. */
private <T> T readValue(int type, @Nullable ClassLoader loader, @Nullable Class<T> clazz) {
// Avoids allocating Class[0] array
@@ -5578,8 +5551,8 @@ public final class Parcel {
}
private void readArrayMapInternal(@NonNull ArrayMap<? super String, Object> outVal,
- int size, @Nullable ClassLoaderProvider loaderProvider) {
- readArrayMap(outVal, size, /* sorted */ true, /* lazy */ false, loaderProvider, null);
+ int size, @Nullable ClassLoader loader) {
+ readArrayMap(outVal, size, /* sorted */ true, /* lazy */ false, loader, null);
}
/**
@@ -5593,12 +5566,11 @@ public final class Parcel {
* @hide
*/
void readArrayMap(ArrayMap<? super String, Object> map, int size, boolean sorted,
- boolean lazy, @Nullable ClassLoaderProvider loaderProvider, int[] lazyValueCount) {
+ boolean lazy, @Nullable ClassLoader loader, int[] lazyValueCount) {
ensureWithinMemoryLimit(SIZE_COMPLEX_TYPE, size);
while (size > 0) {
String key = readString();
- Object value = (lazy) ? readLazyValue(loaderProvider) : readValue(
- getClassLoader(loaderProvider));
+ Object value = (lazy) ? readLazyValue(loader) : readValue(loader);
if (value instanceof LazyValue) {
lazyValueCount[0]++;
}
@@ -5619,12 +5591,12 @@ public final class Parcel {
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public void readArrayMap(@NonNull ArrayMap<? super String, Object> outVal,
- @Nullable ClassLoaderProvider loaderProvider) {
+ @Nullable ClassLoader loader) {
final int N = readInt();
if (N < 0) {
return;
}
- readArrayMapInternal(outVal, N, loaderProvider);
+ readArrayMapInternal(outVal, N, loader);
}
/**
diff --git a/core/java/android/os/UidBatteryConsumer.java b/core/java/android/os/UidBatteryConsumer.java
index 976bfe41ba45..62d5015af914 100644
--- a/core/java/android/os/UidBatteryConsumer.java
+++ b/core/java/android/os/UidBatteryConsumer.java
@@ -147,7 +147,7 @@ public final class UidBatteryConsumer extends BatteryConsumer {
for (int screenState = 0; screenState < SCREEN_STATE_COUNT; screenState++) {
if (mData.layout.screenStateDataIncluded
- && screenState == POWER_STATE_UNSPECIFIED) {
+ && screenState == SCREEN_STATE_UNSPECIFIED) {
continue;
}
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 967f55ce7a88..6c21dbf126bb 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -2910,10 +2910,18 @@ public class UserManager {
* <p>Currently, on most form factors the first human user on the device will be the main user;
* in the future, the concept may be transferable, so a different user (or even no user at all)
* may be designated the main user instead. On other form factors there might not be a main
+ * user. In the future, the concept may be removed, i.e. typical future devices may have no main
* user.
*
* <p>Note that this will not be the system user on devices for which
* {@link #isHeadlessSystemUserMode()} returns true.
+ *
+ * <p>NB: Features should ideally not limit functionality to the main user. Ideally, they
+ * should either work for all users or for all admin users. If a feature should only work for
+ * select users, its determination of which user should be done intelligently or be
+ * customizable. Not all devices support a main user, and the idea of singling out one user as
+ * special is contrary to overall multiuser goals.
+ *
* @hide
*/
@SystemApi
@@ -2930,6 +2938,12 @@ public class UserManager {
/**
* Returns the designated "main user" of the device, or {@code null} if there is no main user.
*
+ * <p>NB: Features should ideally not limit functionality to the main user. Ideally, they
+ * should either work for all users or for all admin users. If a feature should only work for
+ * select users, its determination of which user should be done intelligently or be
+ * customizable. Not all devices support a main user, and the idea of singling out one user as
+ * special is contrary to overall multiuser goals.
+ *
* @see #isMainUser()
* @hide
*/
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 2e231e3957c6..65c857a51b29 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -11008,6 +11008,21 @@ public final class Settings {
@Readable
public static final String SHOW_NOTIFICATION_SNOOZE = "show_notification_snooze";
+ /**
+ * Controls whether dual shade is enabled. This splits notifications and quick settings to
+ * have their own independently expandable/collapsible panels, appearing on either side of
+ * the large screen (including unfolded device) or sharing a space on a narrow screen
+ * (including a folded device). Both panels will now cover the screen only partially
+ * (wrapping their content), so a running app or the lockscreen will remain visible in the
+ * background.
+ * <p>
+ * Type: int (0 for false, 1 for true)
+ *
+ * @hide
+ */
+ @android.provider.Settings.Readable
+ public static final String DUAL_SHADE = "dual_shade";
+
/**
* 1 if it is allowed to remove the primary GAIA account. 0 by default.
* @hide
@@ -13788,6 +13803,16 @@ public final class Settings {
= "enable_freeform_support";
/**
+ * Whether to override the availability of the desktop experiences features on the
+ * device. With desktop experiences enabled, secondary displays can be used to run
+ * apps, in desktop mode by default. Otherwise they can only be used for mirroring.
+ * @hide
+ */
+ @Readable
+ public static final String DEVELOPMENT_OVERRIDE_DESKTOP_EXPERIENCE_FEATURES =
+ "override_desktop_experience_features";
+
+ /**
* Whether to override the availability of the desktop mode on the main display of the
* device. If on, users can make move an app to the desktop, allowing a freeform windowing
* experience.
diff --git a/core/java/android/service/notification/Adjustment.java b/core/java/android/service/notification/Adjustment.java
index 073f512a85fb..09ebc9f030c2 100644
--- a/core/java/android/service/notification/Adjustment.java
+++ b/core/java/android/service/notification/Adjustment.java
@@ -21,6 +21,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.StringDef;
import android.annotation.SystemApi;
+import android.annotation.TestApi;
import android.app.Notification;
import android.os.Build;
import android.os.Bundle;
@@ -223,6 +224,14 @@ public final class Adjustment implements Parcelable {
public static final int TYPE_CONTENT_RECOMMENDATION = 4;
/**
+ * Data type: String, the classification type of this notification. The OS may display
+ * notifications differently depending on the type, and may change the alerting level of the
+ * notification.
+ */
+ @FlaggedApi(android.app.Flags.FLAG_NM_SUMMARIZATION)
+ public static final String KEY_SUMMARIZATION = "key_summarization";
+
+ /**
* Create a notification adjustment.
*
* @param pkg The package of the notification.
diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java
index 72569075c2ed..f23006584621 100644
--- a/core/java/android/service/notification/NotificationListenerService.java
+++ b/core/java/android/service/notification/NotificationListenerService.java
@@ -23,6 +23,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SdkConstant;
import android.annotation.SystemApi;
+import android.annotation.TestApi;
import android.annotation.UiThread;
import android.app.ActivityManager;
import android.app.INotificationManager;
@@ -56,6 +57,7 @@ import android.os.Parcelable;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
+import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import android.widget.RemoteViews;
@@ -1824,6 +1826,7 @@ public abstract class NotificationListenerService extends Service {
private int mProposedImportance;
// Sensitive info detected by the notification assistant
private boolean mSensitiveContent;
+ private String mSummarization;
private static final int PARCEL_VERSION = 2;
@@ -1864,6 +1867,7 @@ public abstract class NotificationListenerService extends Service {
out.writeBoolean(mIsBubble);
out.writeInt(mProposedImportance);
out.writeBoolean(mSensitiveContent);
+ out.writeString(mSummarization);
}
/** @hide */
@@ -1904,6 +1908,7 @@ public abstract class NotificationListenerService extends Service {
mIsBubble = in.readBoolean();
mProposedImportance = in.readInt();
mSensitiveContent = in.readBoolean();
+ mSummarization = in.readString();
}
@@ -2180,6 +2185,16 @@ public abstract class NotificationListenerService extends Service {
}
/**
+ * Returns a summary of the content in the notification, or potentially of the current
+ * notification and related notifications (for example, if this is provided for a group
+ * summary notification it may be summarizing all the child notifications).
+ */
+ @FlaggedApi(android.app.Flags.FLAG_NM_SUMMARIZATION)
+ public @Nullable String getSummarization() {
+ return mSummarization;
+ }
+
+ /**
* Returns the intended transition to ranking passed by {@link NotificationAssistantService}
* @hide
*/
@@ -2201,7 +2216,7 @@ public abstract class NotificationListenerService extends Service {
ArrayList<CharSequence> smartReplies, boolean canBubble,
boolean isTextChanged, boolean isConversation, ShortcutInfo shortcutInfo,
int rankingAdjustment, boolean isBubble, int proposedImportance,
- boolean sensitiveContent) {
+ boolean sensitiveContent, String summarization) {
mKey = key;
mRank = rank;
mIsAmbient = importance < NotificationManager.IMPORTANCE_LOW;
@@ -2229,6 +2244,7 @@ public abstract class NotificationListenerService extends Service {
mIsBubble = isBubble;
mProposedImportance = proposedImportance;
mSensitiveContent = sensitiveContent;
+ mSummarization = TextUtils.nullIfEmpty(summarization);
}
/**
@@ -2271,7 +2287,8 @@ public abstract class NotificationListenerService extends Service {
other.mRankingAdjustment,
other.mIsBubble,
other.mProposedImportance,
- other.mSensitiveContent);
+ other.mSensitiveContent,
+ other.mSummarization);
}
/**
@@ -2332,7 +2349,8 @@ public abstract class NotificationListenerService extends Service {
&& Objects.equals(mRankingAdjustment, other.mRankingAdjustment)
&& Objects.equals(mIsBubble, other.mIsBubble)
&& Objects.equals(mProposedImportance, other.mProposedImportance)
- && Objects.equals(mSensitiveContent, other.mSensitiveContent);
+ && Objects.equals(mSensitiveContent, other.mSensitiveContent)
+ && Objects.equals(mSummarization, other.mSummarization);
}
}
diff --git a/core/java/android/service/notification/StatusBarNotification.java b/core/java/android/service/notification/StatusBarNotification.java
index 105fa3ffd4cd..79957f411597 100644
--- a/core/java/android/service/notification/StatusBarNotification.java
+++ b/core/java/android/service/notification/StatusBarNotification.java
@@ -18,6 +18,8 @@ package android.service.notification;
import static android.text.TextUtils.formatSimple;
+import static com.android.window.flags.Flags.enablePerDisplayPackageContextCacheInStatusbarNotif;
+
import android.annotation.NonNull;
import android.app.Notification;
import android.app.NotificationManager;
@@ -37,9 +39,9 @@ import android.util.ArrayMap;
import com.android.internal.logging.InstanceId;
import com.android.internal.logging.nano.MetricsProto;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import static com.android.window.flags.Flags.enablePerDisplayPackageContextCacheInStatusbarNotif;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.Map;
/**
@@ -81,7 +83,8 @@ public class StatusBarNotification implements Parcelable {
@Deprecated
private Context mContext; // used for inflation & icon expansion
// Maps display id to context used for remote view content inflation and status bar icon.
- private final Map<Integer, Context> mContextForDisplayId = new ArrayMap<>();
+ private final Map<Integer, Context> mContextForDisplayId =
+ Collections.synchronizedMap(new ArrayMap<>());
/** @hide */
public StatusBarNotification(String pkg, String opPkg, int id,
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java
index cb498503f201..a5d52957c40e 100644
--- a/core/java/android/text/StaticLayout.java
+++ b/core/java/android/text/StaticLayout.java
@@ -121,6 +121,7 @@ public class StaticLayout extends Layout {
b.mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE;
b.mJustificationMode = Layout.JUSTIFICATION_MODE_NONE;
b.mLineBreakConfig = LineBreakConfig.NONE;
+ b.mUseBoundsForWidth = false;
b.mMinimumFontMetrics = null;
return b;
}
diff --git a/core/java/android/text/style/TtsSpan.java b/core/java/android/text/style/TtsSpan.java
index a337ba2a57fb..e0d4ec1ca826 100644
--- a/core/java/android/text/style/TtsSpan.java
+++ b/core/java/android/text/style/TtsSpan.java
@@ -108,11 +108,13 @@ public class TtsSpan implements ParcelableSpan {
/**
* The text associated with this span is a time, consisting of a number of
- * hours and minutes, specified with {@link #ARG_HOURS} and
- * {@link #ARG_MINUTES}.
+ * hours, minutes, and seconds specified with {@link #ARG_HOURS}, {@link #ARG_MINUTES}, and
+ * {@link #ARG_SECONDS}.
* Also accepts the arguments {@link #ARG_GENDER},
* {@link #ARG_ANIMACY}, {@link #ARG_MULTIPLICITY} and
- * {@link #ARG_CASE}.
+ * {@link #ARG_CASE}. This is different from {@link #TYPE_DURATION}. This should be used to
+ * convey a particular moment in time, such as a clock time, while {@link #TYPE_DURATION} should
+ * be used to convey an interval of time.
*/
public static final String TYPE_TIME = "android.type.time";
@@ -310,16 +312,18 @@ public class TtsSpan implements ParcelableSpan {
public static final String ARG_UNIT = "android.arg.unit";
/**
- * Argument used to specify the hours of a time. The hours should be
- * provided as an integer in the range from 0 up to and including 24.
- * Can be used with {@link #TYPE_TIME}.
+ * Argument used to specify the hours of a time or duration. The hours should be
+ * provided as an integer in the range from 0 up to and including 24 for
+ * {@link #TYPE_TIME}.
+ * Can be used with {@link #TYPE_TIME} or {@link #TYPE_DURATION}.
*/
public static final String ARG_HOURS = "android.arg.hours";
/**
- * Argument used to specify the minutes of a time. The minutes should be
- * provided as an integer in the range from 0 up to and including 59.
- * Can be used with {@link #TYPE_TIME}.
+ * Argument used to specify the minutes of a time or duration. The minutes should be
+ * provided as an integer in the range from 0 up to and including 59 for
+ * {@link #TYPE_TIME}.
+ * Can be used with {@link #TYPE_TIME} or {@link #TYPE_DURATION}.
*/
public static final String ARG_MINUTES = "android.arg.minutes";
diff --git a/core/java/android/view/InsetsSourceControl.java b/core/java/android/view/InsetsSourceControl.java
index 7f2f0e8863df..cfb4835a13f7 100644
--- a/core/java/android/view/InsetsSourceControl.java
+++ b/core/java/android/view/InsetsSourceControl.java
@@ -194,7 +194,7 @@ public class InsetsSourceControl implements Parcelable {
}
public void release(Consumer<SurfaceControl> surfaceReleaseConsumer) {
- if (mLeash != null) {
+ if (mLeash != null && mLeash.isValid()) {
surfaceReleaseConsumer.accept(mLeash);
}
}
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 7c75d7b30037..0e329c2859db 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -44,7 +44,6 @@ import android.app.Activity;
import android.app.ActivityOptions;
import android.app.ActivityThread;
import android.app.Application;
-import android.app.LoadedApk;
import android.app.PendingIntent;
import android.app.RemoteInput;
import android.appwidget.AppWidgetHostView;
@@ -8484,8 +8483,14 @@ public class RemoteViews implements Parcelable, Filter {
return context;
}
try {
- LoadedApk.checkAndUpdateApkPaths(mApplication);
- Context applicationContext = context.createApplicationContext(mApplication,
+ // Use PackageManager as the source of truth for application information, rather
+ // than the parceled ApplicationInfo provided by the app.
+ ApplicationInfo sanitizedApplication =
+ context.getPackageManager().getApplicationInfoAsUser(
+ mApplication.packageName, 0,
+ UserHandle.getUserId(mApplication.uid));
+ Context applicationContext = context.createApplicationContext(
+ sanitizedApplication,
Context.CONTEXT_RESTRICTED);
// Get the correct apk paths while maintaining the current context's configuration.
return applicationContext.createConfigurationContext(
diff --git a/core/java/android/window/DesktopExperienceFlags.java b/core/java/android/window/DesktopExperienceFlags.java
new file mode 100644
index 000000000000..0d1bb77ae8a2
--- /dev/null
+++ b/core/java/android/window/DesktopExperienceFlags.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+import static com.android.server.display.feature.flags.Flags.enableDisplayContentModeManagement;
+
+import android.annotation.Nullable;
+import android.os.SystemProperties;
+import android.util.Log;
+
+import com.android.window.flags.Flags;
+
+import java.util.function.BooleanSupplier;
+
+/**
+ * Checks Desktop Experience flag state.
+ *
+ * <p>This enum provides a centralized way to control the behavior of flags related to desktop
+ * experience features which are aiming for developer preview before their release. It allows
+ * developer option to override the default behavior of these flags.
+ *
+ * <p>The flags here will be controlled by the {@code
+ * persist.wm.debug.desktop_experience_devopts} system property.
+ *
+ * <p>NOTE: Flags should only be added to this enum when they have received Product and UX alignment
+ * that the feature is ready for developer preview, otherwise just do a flag check.
+ *
+ * @hide
+ */
+public enum DesktopExperienceFlags {
+ ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT(() -> enableDisplayContentModeManagement(), true);
+
+ /**
+ * Flag class, to be used in case the enum cannot be used because the flag is not accessible.
+ *
+ * <p>This class will still use the process-wide cache.
+ */
+ public static class DesktopExperienceFlag {
+ // Function called to obtain aconfig flag value.
+ private final BooleanSupplier mFlagFunction;
+ // Whether the flag state should be affected by developer option.
+ private final boolean mShouldOverrideByDevOption;
+
+ public DesktopExperienceFlag(BooleanSupplier flagFunction, boolean shouldOverrideByDevOption) {
+ this.mFlagFunction = flagFunction;
+ this.mShouldOverrideByDevOption = shouldOverrideByDevOption;
+ }
+
+ /**
+ * Determines state of flag based on the actual flag and desktop experience developer option
+ * overrides.
+ */
+ public boolean isTrue() {
+ return isFlagTrue(mFlagFunction, mShouldOverrideByDevOption);
+ }
+ }
+
+ private static final String TAG = "DesktopExperienceFlags";
+ // Function called to obtain aconfig flag value.
+ private final BooleanSupplier mFlagFunction;
+ // Whether the flag state should be affected by developer option.
+ private final boolean mShouldOverrideByDevOption;
+
+ // Local cache for toggle override, which is initialized once on its first access. It needs to
+ // be refreshed only on reboots as overridden state is expected to take effect on reboots.
+ @Nullable private static Boolean sCachedToggleOverride;
+
+ public static final String SYSTEM_PROPERTY_NAME = "persist.wm.debug.desktop_experience_devopts";
+
+ DesktopExperienceFlags(BooleanSupplier flagFunction, boolean shouldOverrideByDevOption) {
+ this.mFlagFunction = flagFunction;
+ this.mShouldOverrideByDevOption = shouldOverrideByDevOption;
+ }
+
+ /**
+ * Determines state of flag based on the actual flag and desktop experience developer option
+ * overrides.
+ */
+ public boolean isTrue() {
+ return isFlagTrue(mFlagFunction, mShouldOverrideByDevOption);
+ }
+
+ private static boolean isFlagTrue(
+ BooleanSupplier flagFunction, boolean shouldOverrideByDevOption) {
+ if (shouldOverrideByDevOption
+ && Flags.showDesktopExperienceDevOption()
+ && getToggleOverride()) {
+ return true;
+ }
+ return flagFunction.getAsBoolean();
+ }
+
+ private static boolean getToggleOverride() {
+ // If cached, return it
+ if (sCachedToggleOverride != null) {
+ return sCachedToggleOverride;
+ }
+
+ // Otherwise, fetch and cache it
+ boolean override = getToggleOverrideFromSystem();
+ sCachedToggleOverride = override;
+ Log.d(TAG, "Toggle override initialized to: " + override);
+ return override;
+ }
+
+ /** Returns the {@link ToggleOverride} from the system property.. */
+ private static boolean getToggleOverrideFromSystem() {
+ return SystemProperties.getBoolean(SYSTEM_PROPERTY_NAME, false);
+ }
+}
diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java
index be69d3da3874..d1e3a2d953ef 100644
--- a/core/java/android/window/DesktopModeFlags.java
+++ b/core/java/android/window/DesktopModeFlags.java
@@ -20,12 +20,13 @@ import android.annotation.Nullable;
import android.app.ActivityThread;
import android.app.Application;
import android.content.ContentResolver;
+import android.os.SystemProperties;
import android.provider.Settings;
import android.util.Log;
import com.android.window.flags.Flags;
-import java.util.function.Supplier;
+import java.util.function.BooleanSupplier;
/**
* Checks desktop mode flag state.
@@ -80,19 +81,54 @@ public enum DesktopModeFlags {
ENABLE_DESKTOP_WINDOWING_PERSISTENCE(Flags::enableDesktopWindowingPersistence, false),
ENABLE_HANDLE_INPUT_FIX(Flags::enableHandleInputFix, true),
ENABLE_DESKTOP_WINDOWING_ENTER_TRANSITIONS_BUGFIX(
- Flags::enableDesktopWindowingEnterTransitionBugfix, false),
+ Flags::enableDesktopWindowingEnterTransitionBugfix, true),
ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX(
- Flags::enableDesktopWindowingExitTransitionsBugfix, false),
+ Flags::enableDesktopWindowingExitTransitionsBugfix, true),
ENABLE_DESKTOP_APP_LAUNCH_ALTTAB_TRANSITIONS_BUGFIX(
- Flags::enableDesktopAppLaunchAlttabTransitionsBugfix, false),
+ Flags::enableDesktopAppLaunchAlttabTransitionsBugfix, true),
ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX(
- Flags::enableDesktopAppLaunchTransitionsBugfix, false),
+ Flags::enableDesktopAppLaunchTransitionsBugfix, true),
INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC(
- Flags::includeTopTransparentFullscreenTaskInDesktopHeuristic, true);
+ Flags::includeTopTransparentFullscreenTaskInDesktopHeuristic, true),
+ ENABLE_DESKTOP_WINDOWING_HSUM(Flags::enableDesktopWindowingHsum, true),
+ ENABLE_MINIMIZE_BUTTON(Flags::enableMinimizeButton, true),
+ ENABLE_RESIZING_METRICS(Flags::enableResizingMetrics, true),
+ ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS(Flags::enableTaskResizingKeyboardShortcuts, true),
+ ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER(
+ Flags::enableDesktopWallpaperActivityForSystemUser, true),
+ ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX(
+ Flags::enableDesktopRecentsTransitionsCornersBugfix, false),
+ ENABLE_DESKTOP_SYSTEM_DIALOGS_TRANSITIONS(Flags::enableDesktopSystemDialogsTransitions, true);
- private static final String TAG = "DesktopModeFlagsUtil";
+ /**
+ * Flag class, to be used in case the enum cannot be used because the flag is not accessible.
+ *
+ * <p> This class will still use the process-wide cache.
+ */
+ public static class DesktopModeFlag {
+ // Function called to obtain aconfig flag value.
+ private final BooleanSupplier mFlagFunction;
+ // Whether the flag state should be affected by developer option.
+ private final boolean mShouldOverrideByDevOption;
+
+ public DesktopModeFlag(BooleanSupplier flagFunction, boolean shouldOverrideByDevOption) {
+ this.mFlagFunction = flagFunction;
+ this.mShouldOverrideByDevOption = shouldOverrideByDevOption;
+ }
+
+ /**
+ * Determines state of flag based on the actual flag and desktop mode developer option
+ * overrides.
+ */
+ public boolean isTrue() {
+ return isFlagTrue(mFlagFunction, mShouldOverrideByDevOption);
+ }
+
+ }
+
+ private static final String TAG = "DesktopModeFlags";
// Function called to obtain aconfig flag value.
- private final Supplier<Boolean> mFlagFunction;
+ private final BooleanSupplier mFlagFunction;
// Whether the flag state should be affected by developer option.
private final boolean mShouldOverrideByDevOption;
@@ -100,7 +136,9 @@ public enum DesktopModeFlags {
// be refreshed only on reboots as overridden state is expected to take effect on reboots.
private static ToggleOverride sCachedToggleOverride;
- DesktopModeFlags(Supplier<Boolean> flagFunction, boolean shouldOverrideByDevOption) {
+ public static final String SYSTEM_PROPERTY_NAME = "persist.wm.debug.desktop_experience_devopts";
+
+ DesktopModeFlags(BooleanSupplier flagFunction, boolean shouldOverrideByDevOption) {
this.mFlagFunction = flagFunction;
this.mShouldOverrideByDevOption = shouldOverrideByDevOption;
}
@@ -110,24 +148,42 @@ public enum DesktopModeFlags {
* overrides.
*/
public boolean isTrue() {
- Application application = ActivityThread.currentApplication();
- if (!Flags.showDesktopWindowingDevOption()
- || !mShouldOverrideByDevOption
- || application == null) {
- return mFlagFunction.get();
- } else {
+ return isFlagTrue(mFlagFunction, mShouldOverrideByDevOption);
+ }
+
+ private static boolean isFlagTrue(BooleanSupplier flagFunction,
+ boolean shouldOverrideByDevOption) {
+ if (!shouldOverrideByDevOption) return flagFunction.getAsBoolean();
+ if (Flags.showDesktopExperienceDevOption()) {
+ return switch (getToggleOverride(null)) {
+ case OVERRIDE_UNSET, OVERRIDE_OFF -> flagFunction.getAsBoolean();
+ case OVERRIDE_ON -> true;
+ };
+ }
+ if (Flags.showDesktopWindowingDevOption()) {
+ Application application = ActivityThread.currentApplication();
+ if (application == null) {
+ Log.w(TAG, "Could not get the current application.");
+ return flagFunction.getAsBoolean();
+ }
+ ContentResolver contentResolver = application.getContentResolver();
+ if (contentResolver == null) {
+ Log.w(TAG, "Could not get the content resolver for the application.");
+ return flagFunction.getAsBoolean();
+ }
boolean shouldToggleBeEnabledByDefault = Flags.enableDesktopWindowingMode();
- return switch (getToggleOverride(application.getContentResolver())) {
- case OVERRIDE_UNSET -> mFlagFunction.get();
+ return switch (getToggleOverride(contentResolver)) {
+ case OVERRIDE_UNSET -> flagFunction.getAsBoolean();
// When toggle override matches its default state, don't override flags. This
// helps users reset their feature overrides.
- case OVERRIDE_OFF -> !shouldToggleBeEnabledByDefault && mFlagFunction.get();
- case OVERRIDE_ON -> shouldToggleBeEnabledByDefault ? mFlagFunction.get() : true;
+ case OVERRIDE_OFF -> !shouldToggleBeEnabledByDefault && flagFunction.getAsBoolean();
+ case OVERRIDE_ON -> !shouldToggleBeEnabledByDefault || flagFunction.getAsBoolean();
};
}
+ return flagFunction.getAsBoolean();
}
- private ToggleOverride getToggleOverride(ContentResolver contentResolver) {
+ private static ToggleOverride getToggleOverride(@Nullable ContentResolver contentResolver) {
// If cached, return it
if (sCachedToggleOverride != null) {
return sCachedToggleOverride;
@@ -143,12 +199,21 @@ public enum DesktopModeFlags {
/**
* Returns {@link ToggleOverride} from Settings.Global set by toggle.
*/
- private ToggleOverride getToggleOverrideFromSystem(ContentResolver contentResolver) {
- int settingValue = Settings.Global.getInt(
- contentResolver,
- Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES,
- ToggleOverride.OVERRIDE_UNSET.getSetting()
- );
+ private static ToggleOverride getToggleOverrideFromSystem(
+ @Nullable ContentResolver contentResolver) {
+ int settingValue;
+ if (Flags.showDesktopExperienceDevOption()) {
+ settingValue = SystemProperties.getInt(
+ SYSTEM_PROPERTY_NAME,
+ ToggleOverride.OVERRIDE_UNSET.getSetting()
+ );
+ } else {
+ settingValue = Settings.Global.getInt(
+ contentResolver,
+ Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES,
+ ToggleOverride.OVERRIDE_UNSET.getSetting()
+ );
+ }
return ToggleOverride.fromSetting(settingValue, ToggleOverride.OVERRIDE_UNSET);
}
diff --git a/core/java/android/window/OWNERS b/core/java/android/window/OWNERS
index 77c99b98cf4a..82d37244dc70 100644
--- a/core/java/android/window/OWNERS
+++ b/core/java/android/window/OWNERS
@@ -3,3 +3,4 @@ set noparent
include /services/core/java/com/android/server/wm/OWNERS
per-file DesktopModeFlags.java = file:/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS
+per-file DesktopExperienceFlags.java = file:/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS
diff --git a/core/java/android/window/flags/device_state_auto_rotate_setting.aconfig b/core/java/android/window/flags/device_state_auto_rotate_setting.aconfig
new file mode 100644
index 000000000000..bb66989b9946
--- /dev/null
+++ b/core/java/android/window/flags/device_state_auto_rotate_setting.aconfig
@@ -0,0 +1,22 @@
+package: "com.android.window.flags"
+container: "system"
+
+flag {
+ name: "enable_device_state_auto_rotate_setting_logging"
+ namespace: "windowing_frontend"
+ description: "Enable device state auto rotate setting logging"
+ bug: "391147112"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
+ name: "enable_device_state_auto_rotate_setting_refactor"
+ namespace: "windowing_frontend"
+ description: "Enable refactored device state auto rotate setting logic"
+ bug: "350946537"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+} \ No newline at end of file
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index b4e7675402b9..222088e8a8b9 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -555,4 +555,41 @@ flag {
metadata {
purpose: PURPOSE_BUGFIX
}
-} \ No newline at end of file
+}
+
+flag {
+ name: "show_desktop_experience_dev_option"
+ namespace: "lse_desktop_experience"
+ description: "Replace the freeform windowing dev options with a desktop experience one."
+ bug: "389092752"
+}
+
+flag {
+ name: "enable_quickswitch_desktop_split_bugfix"
+ namespace: "lse_desktop_experience"
+ description: "Enables splitting QuickSwitch between fullscreen apps and Desktop workspaces."
+ bug: "345296916"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
+ name: "enable_desktop_windowing_exit_by_minimize_transition_bugfix"
+ namespace: "lse_desktop_experience"
+ description: "Enables exit desktop windowing by minimize transition & motion polish changes"
+ bug: "390161102"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
+ name: "enable_start_launch_transition_from_taskbar_bugfix"
+ namespace: "lse_desktop_experience"
+ description: "Enables starting a launch transition directly from the taskbar if desktop tasks are visible."
+ bug: "361366053"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java
index e0a77d2be724..1f9df3cc842a 100644
--- a/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java
@@ -28,6 +28,7 @@ import com.android.internal.protolog.common.IProtoLogGroup;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
+import java.io.IOException;
import java.util.ArrayList;
public class ProcessedPerfettoProtoLogImpl extends PerfettoProtoLogImpl {
@@ -161,15 +162,39 @@ public class ProcessedPerfettoProtoLogImpl extends PerfettoProtoLogImpl {
messageString = message.getMessage(mViewerConfigReader);
if (messageString == null) {
- throw new RuntimeException("Failed to decode message for logcat. "
- + "Message hash (" + message.getMessageHash() + ") either not available in "
- + "viewerConfig file (" + mViewerConfigFilePath + ") or "
- + "not loaded into memory from file before decoding.");
+ // Either we failed to load the config for this log message from the viewer config file
+ // into memory, or the message hash is simply not available in the viewer config file.
+ // We want to confirm that the message hash is not available in the viewer config file
+ // before throwing an exception.
+ throw new RuntimeException(getReasonForFailureToGetMessageString(message));
}
return messageString;
}
+ private String getReasonForFailureToGetMessageString(Message message) {
+ if (message.getMessageHash() == null) {
+ return "Trying to get message from null message hash";
+ }
+
+ try {
+ if (mViewerConfigReader.messageHashIsAvailableInFile(message.getMessageHash())) {
+ return "Failed to decode message for logcat logging. "
+ + "Message hash (" + message.getMessageHash() + ") is not available in "
+ + "viewerConfig file (" + mViewerConfigFilePath + "). This might be due "
+ + "to the viewer config file and the executing code being out of sync.";
+ } else {
+ return "Failed to decode message for logcat. "
+ + "Message hash (" + message.getMessageHash() + ") was available in the "
+ + "viewerConfig file (" + mViewerConfigFilePath + ") but wasn't loaded "
+ + "into memory from file before decoding! This is likely a bug.";
+ }
+ } catch (IOException e) {
+ return "Failed to get string message to log but could not identify the root cause due "
+ + "to an IO error in reading the viewer config file.";
+ }
+ }
+
private void loadLogcatGroupsViewerConfig(@NonNull IProtoLogGroup[] protoLogGroups) {
final var groupsLoggingToLogcat = new ArrayList<String>();
for (IProtoLogGroup protoLogGroup : protoLogGroups) {
diff --git a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
index 524f64225084..f77179949fbf 100644
--- a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
+++ b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
@@ -100,6 +100,36 @@ public class ProtoLogViewerConfigReader {
}
}
+ /**
+ * Return whether or not the viewer config file contains a message with the specified hash.
+ * @param messageHash The hash message we are looking for in the viewer config file
+ * @return True iff the message with message hash is contained in the viewer config.
+ * @throws IOException if there was an issue reading the viewer config file.
+ */
+ public boolean messageHashIsAvailableInFile(long messageHash)
+ throws IOException {
+ try (var pisWrapper = mViewerConfigInputStreamProvider.getInputStream()) {
+ final var pis = pisWrapper.get();
+ while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+ if (pis.getFieldNumber() == (int) MESSAGES) {
+ final long inMessageToken = pis.start(MESSAGES);
+
+ while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+ if (pis.getFieldNumber() == (int) MESSAGE_ID) {
+ if (pis.readLong(MESSAGE_ID) == messageHash) {
+ return true;
+ }
+ }
+ }
+
+ pis.end(inMessageToken);
+ }
+ }
+ }
+
+ return false;
+ }
+
@NonNull
private Map<Long, String> loadViewerConfigMappingForGroup(@NonNull String group)
throws IOException {
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index ec3975205542..72cb9d1a20ac 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -51,6 +51,14 @@ oneway interface IStatusBar
void showWirelessChargingAnimation(int batteryLevel);
+ /**
+ * Sets the new IME window status.
+ *
+ * @param displayId The id of the display to which the IME is bound.
+ * @param vis The IME window visibility.
+ * @param backDisposition The IME back disposition mode.
+ * @param showImeSwitcher Whether the IME Switcher button should be shown.
+ */
void setImeWindowStatus(int displayId, int vis, int backDisposition, boolean showImeSwitcher);
void setWindowState(int display, int window, int state);
diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
index ec0954d5590a..1fa1e0bbc69a 100644
--- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
@@ -61,6 +61,14 @@ interface IStatusBarService
void setIconVisibility(String slot, boolean visible);
@UnsupportedAppUsage
void removeIcon(String slot);
+ /**
+ * Sets the new IME window status.
+ *
+ * @param displayId The id of the display to which the IME is bound.
+ * @param vis The IME window visibility.
+ * @param backDisposition The IME back disposition mode.
+ * @param showImeSwitcher Whether the IME Switcher button should be shown.
+ */
void setImeWindowStatus(int displayId, int vis, int backDisposition, boolean showImeSwitcher);
void expandSettingsPanel(String subPanel);
diff --git a/core/java/com/android/internal/widget/ConversationLayout.java b/core/java/com/android/internal/widget/ConversationLayout.java
index b3ab5d3cd258..04ce9bcd7afd 100644
--- a/core/java/com/android/internal/widget/ConversationLayout.java
+++ b/core/java/com/android/internal/widget/ConversationLayout.java
@@ -105,6 +105,7 @@ public class ConversationLayout extends FrameLayout
private ArrayList<MessagingGroup> mAddedGroups = new ArrayList<>();
private Person mUser;
private CharSequence mNameReplacement;
+ private CharSequence mSummarizedContent;
private boolean mIsCollapsed;
private ImageResolver mImageResolver;
private CachingIconView mConversationIconView;
@@ -397,7 +398,7 @@ public class ConversationLayout extends FrameLayout
*
* @param isCollapsed is it collapsed
*/
- @RemotableViewMethod
+ @RemotableViewMethod(asyncImpl = "setIsCollapsedAsync")
public void setIsCollapsed(boolean isCollapsed) {
mIsCollapsed = isCollapsed;
mMessagingLinearLayout.setMaxDisplayedLines(isCollapsed ? 1 : Integer.MAX_VALUE);
@@ -406,6 +407,15 @@ public class ConversationLayout extends FrameLayout
}
/**
+ * setDataAsync needs to do different stuff for the collapsed vs expanded view, so store the
+ * collapsed state early.
+ */
+ public Runnable setIsCollapsedAsync(boolean isCollapsed) {
+ mIsCollapsed = isCollapsed;
+ return () -> setIsCollapsed(isCollapsed);
+ }
+
+ /**
* Set conversation data
*
* @param extras Bundle contains conversation data
@@ -439,8 +449,16 @@ public class ConversationLayout extends FrameLayout
extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false);
int unreadCount = extras.getInt(Notification.EXTRA_CONVERSATION_UNREAD_MESSAGE_COUNT);
- final List<MessagingMessage> newMessagingMessages =
- createMessages(newMessages, /* isHistoric= */false, usePrecomputedText);
+ List<MessagingMessage> newMessagingMessages;
+ mSummarizedContent = extras.getCharSequence(Notification.EXTRA_SUMMARIZED_CONTENT);
+ if (mSummarizedContent != null && mIsCollapsed) {
+ Notification.MessagingStyle.Message summary =
+ new Notification.MessagingStyle.Message(mSummarizedContent, 0, "");
+ newMessagingMessages = createMessages(List.of(summary), false, usePrecomputedText);
+ } else {
+ newMessagingMessages =
+ createMessages(newMessages, /* isHistoric= */false, usePrecomputedText);
+ }
final List<MessagingMessage> newHistoricMessagingMessages =
createMessages(newHistoricMessages, /* isHistoric= */true, usePrecomputedText);
@@ -463,7 +481,7 @@ public class ConversationLayout extends FrameLayout
return new MessagingData(user, showSpinner, unreadCount,
newHistoricMessagingMessages, newMessagingMessages, groups, senders,
- conversationHeaderData);
+ conversationHeaderData, mSummarizedContent);
}
/**
@@ -1622,6 +1640,9 @@ public class ConversationLayout extends FrameLayout
@Nullable
public CharSequence getConversationText() {
+ if (mSummarizedContent != null) {
+ return mSummarizedContent;
+ }
if (mMessages.isEmpty()) {
return null;
}
diff --git a/core/java/com/android/internal/widget/MessagingData.java b/core/java/com/android/internal/widget/MessagingData.java
index fb1f28fb8ef3..cb5041efd10f 100644
--- a/core/java/com/android/internal/widget/MessagingData.java
+++ b/core/java/com/android/internal/widget/MessagingData.java
@@ -32,6 +32,7 @@ final class MessagingData {
private final List<List<MessagingMessage>> mGroups;
private final List<Person> mSenders;
private final int mUnreadCount;
+ private final CharSequence mSummarization;
private ConversationHeaderData mConversationHeaderData;
@@ -41,8 +42,7 @@ final class MessagingData {
List<Person> senders) {
this(user, showSpinner, /* unreadCount= */0,
historicMessagingMessages, newMessagingMessages,
- groups,
- senders, null);
+ groups, senders, null, null);
}
MessagingData(Person user, boolean showSpinner,
@@ -51,7 +51,8 @@ final class MessagingData {
List<MessagingMessage> newMessagingMessages,
List<List<MessagingMessage>> groups,
List<Person> senders,
- @Nullable ConversationHeaderData conversationHeaderData) {
+ @Nullable ConversationHeaderData conversationHeaderData,
+ CharSequence summarization) {
mUser = user;
mShowSpinner = showSpinner;
mUnreadCount = unreadCount;
@@ -60,6 +61,7 @@ final class MessagingData {
mGroups = groups;
mSenders = senders;
mConversationHeaderData = conversationHeaderData;
+ mSummarization = summarization;
}
public Person getUser() {
@@ -94,4 +96,9 @@ final class MessagingData {
public ConversationHeaderData getConversationHeaderData() {
return mConversationHeaderData;
}
+
+ @Nullable
+ public CharSequence getSummarization() {
+ return mSummarization;
+ }
}
diff --git a/core/java/com/android/internal/widget/MessagingMessage.java b/core/java/com/android/internal/widget/MessagingMessage.java
index a59ee77cc693..c7f22836dd93 100644
--- a/core/java/com/android/internal/widget/MessagingMessage.java
+++ b/core/java/com/android/internal/widget/MessagingMessage.java
@@ -24,7 +24,7 @@ import java.util.ArrayList;
import java.util.Objects;
/**
- * A message of a {@link MessagingLayout}.
+ * A message or summary of a {@link MessagingLayout}.
*/
public interface MessagingMessage extends MessagingLinearLayout.MessagingChild {
diff --git a/core/jni/android_util_Binder.cpp b/core/jni/android_util_Binder.cpp
index 639f5bff7614..91b25c2bda06 100644
--- a/core/jni/android_util_Binder.cpp
+++ b/core/jni/android_util_Binder.cpp
@@ -74,6 +74,7 @@ static struct bindernative_offsets_t
jmethodID mExecTransact;
jmethodID mGetInterfaceDescriptor;
jmethodID mTransactionCallback;
+ jmethodID mGetExtension;
// Object state.
jfieldID mObject;
@@ -489,8 +490,12 @@ public:
if (mVintf) {
::android::internal::Stability::markVintf(b.get());
}
- if (mExtension != nullptr) {
- b.get()->setExtension(mExtension);
+ if (mSetExtensionCalled) {
+ jobject javaIBinderObject = env->CallObjectMethod(obj, gBinderOffsets.mGetExtension);
+ sp<IBinder> extensionFromJava = ibinderForJavaObject(env, javaIBinderObject);
+ if (extensionFromJava != nullptr) {
+ b.get()->setExtension(extensionFromJava);
+ }
}
mBinder = b;
ALOGV("Creating JavaBinder %p (refs %p) for Object %p, weakCount=%" PRId32 "\n",
@@ -516,21 +521,12 @@ public:
mVintf = false;
}
- sp<IBinder> getExtension() {
- AutoMutex _l(mLock);
- sp<JavaBBinder> b = mBinder.promote();
- if (b != nullptr) {
- return b.get()->getExtension();
- }
- return mExtension;
- }
-
void setExtension(const sp<IBinder>& extension) {
AutoMutex _l(mLock);
- mExtension = extension;
+ mSetExtensionCalled = true;
sp<JavaBBinder> b = mBinder.promote();
if (b != nullptr) {
- b.get()->setExtension(mExtension);
+ b.get()->setExtension(extension);
}
}
@@ -542,8 +538,7 @@ private:
// is too much binder state here, we can think about making JavaBBinder an
// sp here (avoid recreating it)
bool mVintf = false;
-
- sp<IBinder> mExtension;
+ bool mSetExtensionCalled = false;
};
// ----------------------------------------------------------------------------
@@ -1249,10 +1244,6 @@ static void android_os_Binder_blockUntilThreadAvailable(JNIEnv* env, jobject cla
return IPCThreadState::self()->blockUntilThreadAvailable();
}
-static jobject android_os_Binder_getExtension(JNIEnv* env, jobject obj) {
- JavaBBinderHolder* jbh = (JavaBBinderHolder*) env->GetLongField(obj, gBinderOffsets.mObject);
- return javaObjectForIBinder(env, jbh->getExtension());
-}
static void android_os_Binder_setExtension(JNIEnv* env, jobject obj, jobject extensionObject) {
JavaBBinderHolder* jbh = (JavaBBinderHolder*) env->GetLongField(obj, gBinderOffsets.mObject);
@@ -1295,8 +1286,7 @@ static const JNINativeMethod gBinderMethods[] = {
{ "getNativeBBinderHolder", "()J", (void*)android_os_Binder_getNativeBBinderHolder },
{ "getNativeFinalizer", "()J", (void*)android_os_Binder_getNativeFinalizer },
{ "blockUntilThreadAvailable", "()V", (void*)android_os_Binder_blockUntilThreadAvailable },
- { "getExtension", "()Landroid/os/IBinder;", (void*)android_os_Binder_getExtension },
- { "setExtension", "(Landroid/os/IBinder;)V", (void*)android_os_Binder_setExtension },
+ { "setExtensionNative", "(Landroid/os/IBinder;)V", (void*)android_os_Binder_setExtension },
};
// clang-format on
@@ -1313,6 +1303,8 @@ static int int_register_android_os_Binder(JNIEnv* env)
gBinderOffsets.mTransactionCallback =
GetStaticMethodIDOrDie(env, clazz, "transactionCallback", "(IIII)V");
gBinderOffsets.mObject = GetFieldIDOrDie(env, clazz, "mObject", "J");
+ gBinderOffsets.mGetExtension = GetMethodIDOrDie(env, clazz, "getExtension",
+ "()Landroid/os/IBinder;");
return RegisterMethodsOrDie(
env, kBinderPathName,
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index b894d3a6888f..586cafdd2b57 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2009,6 +2009,10 @@
<!-- Component name of the built in wallpaper used to display bitmap wallpapers. This must not be null. -->
<string name="image_wallpaper_component" translatable="false">com.android.systemui/com.android.systemui.wallpapers.ImageWallpaper</string>
+ <!-- Component name of the built in wallpaper that is used when the user-selected wallpaper is
+ incompatible with the display's resolution or aspect ratio. -->
+ <string name="fallback_wallpaper_component" translatable="false">com.android.systemui/com.android.systemui.wallpapers.GradientColorWallpaper</string>
+
<!-- True if WallpaperService is enabled -->
<bool name="config_enableWallpaperService">true</bool>
diff --git a/core/res/res/values/public-final.xml b/core/res/res/values/public-final.xml
index d421944917ea..d8e89318a134 100644
--- a/core/res/res/values/public-final.xml
+++ b/core/res/res/values/public-final.xml
@@ -3922,4 +3922,86 @@
<public type="color" name="system_error_900" id="0x010600d0" />
<public type="color" name="system_error_1000" id="0x010600d1" />
+ <!-- ===============================================================
+ Resources added in version NEXT of the platform
+
+ NOTE: After this version of the platform is forked, changes cannot be made to the root
+ branch's groups for that release. Only merge changes to the forked platform branch.
+ =============================================================== -->
+ <eat-comment/>
+
+ <staging-public-group-final type="attr" first-id="0x01b70000">
+ <public name="removed_" />
+ <!-- @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") -->
+ <public name="adServiceTypes" />
+ <!-- @FlaggedApi("android.view.inputmethod.ime_switcher_revamp_api") -->
+ <public name="languageSettingsActivity"/>
+ <!-- @FlaggedApi(android.service.controls.flags.Flags.FLAG_HOME_PANEL_DREAM) -->
+ <public name="dreamCategory"/>
+ <!-- @FlaggedApi("android.permission.flags.replace_body_sensor_permission_enabled")
+ @hide @SystemApi -->
+ <public name="backgroundPermission"/>
+ <!-- @FlaggedApi(android.view.accessibility.supplemental_description) -->
+ <public name="supplementalDescription"/>
+ <!-- @FlaggedApi("android.security.enable_intent_matching_flags") -->
+ <public name="intentMatchingFlags"/>
+ <!-- @FlaggedApi(android.view.inputmethod.Flags.FLAG_IME_SWITCHER_REVAMP_API) -->
+ <public name="layoutLabel"/>
+ <public name="removed_" />
+ <public name="removed_" />
+ <!-- @FlaggedApi(android.content.pm.Flags.FLAG_APP_COMPAT_OPTION_16KB) -->
+ <public name="pageSizeCompat" />
+ <!-- @FlaggedApi(android.nfc.Flags.FLAG_NFC_ASSOCIATED_ROLE_SERVICES) -->
+ <public name="wantsRoleHolderPriority"/>
+ <!-- @FlaggedApi(android.sdk.Flags.FLAG_MAJOR_MINOR_VERSIONING_SCHEME) -->
+ <public name="minSdkVersionFull"/>
+ <public name="removed_" />
+ <public name="removed_" />
+ <public name="removed_" />
+ <public name="removed_" />
+ </staging-public-group-final>
+
+ <!-- @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") -->
+ <public type="attr" name="adServiceTypes" id="0x010106a4" />
+ <!-- @FlaggedApi("android.view.inputmethod.ime_switcher_revamp_api") -->
+ <public type="attr" name="languageSettingsActivity" id="0x010106a5" />
+ <!-- @FlaggedApi(android.service.controls.flags.Flags.FLAG_HOME_PANEL_DREAM) -->
+ <public type="attr" name="dreamCategory" id="0x010106a6" />
+ <!-- @FlaggedApi("android.permission.flags.replace_body_sensor_permission_enabled")
+ @hide @SystemApi -->
+ <public type="attr" name="backgroundPermission" id="0x010106a7" />
+ <!-- @FlaggedApi(android.view.accessibility.supplemental_description) -->
+ <public type="attr" name="supplementalDescription" id="0x010106a8" />
+ <!-- @FlaggedApi("android.security.enable_intent_matching_flags") -->
+ <public type="attr" name="intentMatchingFlags" id="0x010106a9" />
+ <!-- @FlaggedApi(android.view.inputmethod.Flags.FLAG_IME_SWITCHER_REVAMP_API) -->
+ <public type="attr" name="layoutLabel" id="0x010106aa" />
+ <!-- @FlaggedApi(android.content.pm.Flags.FLAG_APP_COMPAT_OPTION_16KB) -->
+ <public type="attr" name="pageSizeCompat" id="0x010106ab" />
+ <!-- @FlaggedApi(android.nfc.Flags.FLAG_NFC_ASSOCIATED_ROLE_SERVICES) -->
+ <public type="attr" name="wantsRoleHolderPriority" id="0x010106ac" />
+ <!-- @FlaggedApi(android.sdk.Flags.FLAG_MAJOR_MINOR_VERSIONING_SCHEME) -->
+ <public type="attr" name="minSdkVersionFull" id="0x010106ad" />
+
+ <staging-public-group-final type="string" first-id="0x01b40000">
+ <!-- @FlaggedApi(android.content.pm.Flags.FLAG_SDK_DEPENDENCY_INSTALLER)
+ @hide @SystemApi -->
+ <public name="config_systemDependencyInstaller" />
+ <!-- @hide @SystemApi -->
+ <public name="removed_config_defaultReservedForTestingProfileGroupExclusivity" />
+ <!-- @FlaggedApi(android.permission.flags.Flags.FLAG_SYSTEM_VENDOR_INTELLIGENCE_ROLE_ENABLED)
+ @hide @SystemApi -->
+ <public name="config_systemVendorIntelligence" />
+ <public name="removed_" />
+ <public name="removed_" />
+ <public name="removed_" />
+ </staging-public-group-final>
+
+ <!-- @FlaggedApi(android.content.pm.Flags.FLAG_SDK_DEPENDENCY_INSTALLER)
+ @hide @SystemApi -->
+ <public type="string" name="config_systemDependencyInstaller" id="0x0104004a" />
+ <!-- @FlaggedApi(android.permission.flags.Flags.FLAG_SYSTEM_VENDOR_INTELLIGENCE_ROLE_ENABLED)
+ @hide @SystemApi -->
+ <public type="string" name="config_systemVendorIntelligence" id="0x0104004b" />
+
</resources>
diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml
index 7baaa6d590f2..2d411d0268b3 100644
--- a/core/res/res/values/public-staging.xml
+++ b/core/res/res/values/public-staging.xml
@@ -109,69 +109,44 @@
=============================================================== -->
<eat-comment/>
- <staging-public-group type="attr" first-id="0x01b70000">
+ <staging-public-group type="attr" first-id="0x01b30000">
<!-- @FlaggedApi("android.content.pm.sdk_lib_independence") -->
<public name="optional"/>
- <!-- @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") -->
- <public name="adServiceTypes" />
- <!-- @FlaggedApi("android.view.inputmethod.ime_switcher_revamp_api") -->
- <public name="languageSettingsActivity"/>
- <!-- @FlaggedApi(android.service.controls.flags.Flags.FLAG_HOME_PANEL_DREAM) -->
- <public name="dreamCategory"/>
- <!-- @FlaggedApi("android.permission.flags.replace_body_sensor_permission_enabled")
- @hide @SystemApi -->
- <public name="backgroundPermission"/>
- <!-- @FlaggedApi(android.view.accessibility.supplemental_description) -->
- <public name="supplementalDescription"/>
- <!-- @FlaggedApi("android.security.enable_intent_matching_flags") -->
- <public name="intentMatchingFlags"/>
- <!-- @FlaggedApi(android.view.inputmethod.Flags.FLAG_IME_SWITCHER_REVAMP_API) -->
- <public name="layoutLabel"/>
<!-- @FlaggedApi(android.content.pm.Flags.FLAG_CHANGE_LAUNCHER_BADGING) -->
<public name="alternateLauncherIcons"/>
<!-- @FlaggedApi(android.content.pm.Flags.FLAG_CHANGE_LAUNCHER_BADGING) -->
<public name="alternateLauncherLabels"/>
- <!-- @FlaggedApi(android.content.pm.Flags.FLAG_APP_COMPAT_OPTION_16KB) -->
- <public name="pageSizeCompat" />
- <!-- @FlaggedApi(android.nfc.Flags.FLAG_NFC_ASSOCIATED_ROLE_SERVICES) -->
- <public name="wantsRoleHolderPriority"/>
- <!-- @FlaggedApi(android.sdk.Flags.FLAG_MAJOR_MINOR_VERSIONING_SCHEME) -->
- <public name="minSdkVersionFull"/>
+ <!-- @hide Only for device overlay to use this. -->
+ <public name="pointerIconVectorFill"/>
+ <!-- @hide Only for device overlay to use this. -->
+ <public name="pointerIconVectorFillInverse"/>
+ <!-- @hide Only for device overlay to use this. -->
+ <public name="pointerIconVectorStroke"/>
+ <!-- @hide Only for device overlay to use this. -->
+ <public name="pointerIconVectorStrokeInverse"/>
</staging-public-group>
- <staging-public-group type="id" first-id="0x01b60000">
+ <staging-public-group type="id" first-id="0x01b20000">
<!-- @FlaggedApi(android.appwidget.flags.Flags.FLAG_ENGAGEMENT_METRICS) -->
<public name="remoteViewsMetricsId"/>
</staging-public-group>
- <staging-public-group type="style" first-id="0x01b50000">
+ <staging-public-group type="style" first-id="0x01b10000">
</staging-public-group>
- <staging-public-group type="string" first-id="0x01b40000">
- <!-- @FlaggedApi(android.content.pm.Flags.FLAG_SDK_DEPENDENCY_INSTALLER)
- @hide @SystemApi -->
- <public name="config_systemDependencyInstaller" />
- <!-- @hide @SystemApi -->
- <public name="removed_config_defaultReservedForTestingProfileGroupExclusivity" />
- <!-- @FlaggedApi(android.permission.flags.Flags.FLAG_SYSTEM_VENDOR_INTELLIGENCE_ROLE_ENABLED)
- @hide @SystemApi -->
- <public name="config_systemVendorIntelligence" />
-
+ <staging-public-group type="string" first-id="0x01b00000">
<!-- @FlaggedApi(android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE_MODULE)
@hide @SystemApi -->
<public name="config_defaultOnDeviceIntelligenceService" />
-
<!-- @FlaggedApi(android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE_MODULE)
@hide @SystemApi -->
<public name="config_defaultOnDeviceSandboxedInferenceService" />
-
<!-- @FlaggedApi(android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE_MODULE)
@hide @SystemApi -->
<public name="config_defaultOnDeviceIntelligenceDeviceConfigNamespace" />
-
</staging-public-group>
- <staging-public-group type="dimen" first-id="0x01b30000">
+ <staging-public-group type="dimen" first-id="0x01af0000">
<!-- @FlaggedApi(android.os.Flags.FLAG_MATERIAL_MOTION_TOKENS)-->
<public name="config_motionStandardFastSpatialDamping"/>
<!-- @FlaggedApi(android.os.Flags.FLAG_MATERIAL_MOTION_TOKENS)-->
@@ -208,7 +183,7 @@
<public name="config_shapeCornerRadiusXlarge"/>
</staging-public-group>
- <staging-public-group type="color" first-id="0x01b20000">
+ <staging-public-group type="color" first-id="0x01ae0000">
<!-- @FlaggedApi(android.os.Flags.FLAG_MATERIAL_COLORS_10_2024)-->
<public name="system_inverse_on_surface_light"/>
<!-- @FlaggedApi(android.os.Flags.FLAG_MATERIAL_COLORS_10_2024)-->
@@ -235,28 +210,28 @@
<public name="system_surface_tint_dark"/>
</staging-public-group>
- <staging-public-group type="array" first-id="0x01b10000">
+ <staging-public-group type="array" first-id="0x01ad0000">
</staging-public-group>
- <staging-public-group type="drawable" first-id="0x01b00000">
+ <staging-public-group type="drawable" first-id="0x01ac0000">
</staging-public-group>
- <staging-public-group type="layout" first-id="0x01af0000">
+ <staging-public-group type="layout" first-id="0x01ab0000">
</staging-public-group>
- <staging-public-group type="anim" first-id="0x01ae0000">
+ <staging-public-group type="anim" first-id="0x01aa0000">
</staging-public-group>
- <staging-public-group type="animator" first-id="0x01ad0000">
+ <staging-public-group type="animator" first-id="0x01a90000">
</staging-public-group>
- <staging-public-group type="interpolator" first-id="0x01ac0000">
+ <staging-public-group type="interpolator" first-id="0x01a80000">
</staging-public-group>
- <staging-public-group type="mipmap" first-id="0x01ab0000">
+ <staging-public-group type="mipmap" first-id="0x01a70000">
</staging-public-group>
- <staging-public-group type="integer" first-id="0x01aa0000">
+ <staging-public-group type="integer" first-id="0x01a60000">
<!-- @FlaggedApi(android.os.Flags.FLAG_MATERIAL_MOTION_TOKENS)-->
<public name="config_motionStandardFastSpatialStiffness"/>
<!-- @FlaggedApi(android.os.Flags.FLAG_MATERIAL_MOTION_TOKENS)-->
@@ -283,16 +258,16 @@
<public name="config_motionExpressiveSlowEffectStiffness"/>
</staging-public-group>
- <staging-public-group type="transition" first-id="0x01a90000">
+ <staging-public-group type="transition" first-id="0x01a50000">
</staging-public-group>
- <staging-public-group type="raw" first-id="0x01a80000">
+ <staging-public-group type="raw" first-id="0x01a40000">
</staging-public-group>
- <staging-public-group type="bool" first-id="0x01a70000">
+ <staging-public-group type="bool" first-id="0x01a30000">
</staging-public-group>
- <staging-public-group type="fraction" first-id="0x01a60000">
+ <staging-public-group type="fraction" first-id="0x01a20000">
</staging-public-group>
</resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 8315e3cf1a11..772a7413a4a7 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2270,6 +2270,7 @@
<java-symbol type="string" name="heavy_weight_notification" />
<java-symbol type="string" name="heavy_weight_notification_detail" />
<java-symbol type="string" name="image_wallpaper_component" />
+ <java-symbol type="string" name="fallback_wallpaper_component" />
<java-symbol type="string" name="input_method_binding_label" />
<java-symbol type="string" name="input_method_ime_switch_long_click_action_desc" />
<java-symbol type="string" name="launch_warning_original" />
diff --git a/core/tests/FileSystemUtilsTest/Android.bp b/core/tests/FileSystemUtilsTest/Android.bp
index 962ff3c0a6e0..f4d92522bb25 100644
--- a/core/tests/FileSystemUtilsTest/Android.bp
+++ b/core/tests/FileSystemUtilsTest/Android.bp
@@ -36,10 +36,10 @@ cc_library {
ldflags: ["-z max-page-size=0x1000"],
}
-android_test_helper_app {
- name: "app_with_4kb_elf",
+java_defaults {
+ name: "app_with_4kb_elf_defaults",
srcs: ["app_with_4kb_elf/src/**/*.java"],
- manifest: "app_with_4kb_elf/app_with_4kb_elf.xml",
+ resource_dirs: ["app_with_4kb_elf/res"],
compile_multilib: "64",
jni_libs: [
"libpunchtest_4kb",
@@ -47,7 +47,36 @@ android_test_helper_app {
static_libs: [
"androidx.test.rules",
"platform-test-annotations",
+ "androidx.test.uiautomator_uiautomator",
+ "sysui-helper",
],
+}
+
+android_test_helper_app {
+ name: "app_with_4kb_elf",
+ defaults: ["app_with_4kb_elf_defaults"],
+ manifest: "app_with_4kb_elf/app_with_4kb_elf.xml",
+ use_embedded_native_libs: true,
+}
+
+android_test_helper_app {
+ name: "app_with_4kb_compressed_elf",
+ defaults: ["app_with_4kb_elf_defaults"],
+ manifest: "app_with_4kb_elf/app_with_4kb_elf.xml",
+ use_embedded_native_libs: false,
+}
+
+android_test_helper_app {
+ name: "page_size_compat_disabled_app",
+ defaults: ["app_with_4kb_elf_defaults"],
+ manifest: "app_with_4kb_elf/page_size_compat_disabled.xml",
+ use_embedded_native_libs: true,
+}
+
+android_test_helper_app {
+ name: "app_with_4kb_elf_no_override",
+ defaults: ["app_with_4kb_elf_defaults"],
+ manifest: "app_with_4kb_elf/app_with_4kb_no_override.xml",
use_embedded_native_libs: true,
}
@@ -99,6 +128,9 @@ java_test_host {
":embedded_native_libs_test_app",
":extract_native_libs_test_app",
":app_with_4kb_elf",
+ ":page_size_compat_disabled_app",
+ ":app_with_4kb_compressed_elf",
+ ":app_with_4kb_elf_no_override",
],
test_suites: ["general-tests"],
test_config: "AndroidTest.xml",
diff --git a/core/tests/FileSystemUtilsTest/AndroidTest.xml b/core/tests/FileSystemUtilsTest/AndroidTest.xml
index 651a7ca15dac..27f49b2289ba 100644
--- a/core/tests/FileSystemUtilsTest/AndroidTest.xml
+++ b/core/tests/FileSystemUtilsTest/AndroidTest.xml
@@ -22,7 +22,6 @@
<option name="cleanup-apks" value="true" />
<option name="test-file-name" value="embedded_native_libs_test_app.apk" />
<option name="test-file-name" value="extract_native_libs_test_app.apk" />
- <option name="test-file-name" value="app_with_4kb_elf.apk" />
</target_preparer>
<test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
diff --git a/core/tests/FileSystemUtilsTest/app_with_4kb_elf/app_with_4kb_elf.xml b/core/tests/FileSystemUtilsTest/app_with_4kb_elf/app_with_4kb_elf.xml
index b9d6d4db2c81..d7a37336cbc3 100644
--- a/core/tests/FileSystemUtilsTest/app_with_4kb_elf/app_with_4kb_elf.xml
+++ b/core/tests/FileSystemUtilsTest/app_with_4kb_elf/app_with_4kb_elf.xml
@@ -18,7 +18,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="android.test.pagesizecompat">
<application
- android:extractNativeLibs="false"
android:pageSizeCompat="enabled">
<uses-library android:name="android.test.runner"/>
<activity android:name=".MainActivity"
diff --git a/core/tests/FileSystemUtilsTest/app_with_4kb_elf/app_with_4kb_no_override.xml b/core/tests/FileSystemUtilsTest/app_with_4kb_elf/app_with_4kb_no_override.xml
new file mode 100644
index 000000000000..b0b5204d6e80
--- /dev/null
+++ b/core/tests/FileSystemUtilsTest/app_with_4kb_elf/app_with_4kb_no_override.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.test.pagesizecompat">
+ <application
+ android:label="PageSizeCompatTestApp">
+ <uses-library android:name="android.test.runner"/>
+ <activity android:name=".MainActivity"
+ android:exported="true"
+ android:label="Home page"
+ android:process=":NewProcess">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ </intent-filter>
+ </activity>
+ </application>
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.test.pagesizecompat"/>
+</manifest> \ No newline at end of file
diff --git a/core/tests/FileSystemUtilsTest/app_with_4kb_elf/page_size_compat_disabled.xml b/core/tests/FileSystemUtilsTest/app_with_4kb_elf/page_size_compat_disabled.xml
new file mode 100644
index 000000000000..641c5e741014
--- /dev/null
+++ b/core/tests/FileSystemUtilsTest/app_with_4kb_elf/page_size_compat_disabled.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.test.pagesizecompat">
+ <application
+ android:pageSizeCompat="disabled">
+ <uses-library android:name="android.test.runner"/>
+ <activity android:name=".MainActivity"
+ android:exported="true"
+ android:process=":NewProcess">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ </intent-filter>
+ </activity>
+ </application>
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.test.pagesizecompat"/>
+</manifest> \ No newline at end of file
diff --git a/core/tests/FileSystemUtilsTest/app_with_4kb_elf/res/layout/hello.xml b/core/tests/FileSystemUtilsTest/app_with_4kb_elf/res/layout/hello.xml
new file mode 100644
index 000000000000..473f3f9f9402
--- /dev/null
+++ b/core/tests/FileSystemUtilsTest/app_with_4kb_elf/res/layout/hello.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:keepScreenOn="true">
+ <TextView
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="this is a test activity"
+ />
+</LinearLayout>
+
diff --git a/core/tests/FileSystemUtilsTest/app_with_4kb_elf/src/android/test/pagesizecompat/MainActivity.java b/core/tests/FileSystemUtilsTest/app_with_4kb_elf/src/android/test/pagesizecompat/MainActivity.java
index 893f9cd01497..5d8d8081b0e5 100644
--- a/core/tests/FileSystemUtilsTest/app_with_4kb_elf/src/android/test/pagesizecompat/MainActivity.java
+++ b/core/tests/FileSystemUtilsTest/app_with_4kb_elf/src/android/test/pagesizecompat/MainActivity.java
@@ -19,6 +19,7 @@ package android.test.pagesizecompat;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
+import android.view.View;
import androidx.annotation.VisibleForTesting;
@@ -43,6 +44,8 @@ public class MainActivity extends Activity {
@Override
public void onCreate(Bundle savedOnstanceState) {
super.onCreate(savedOnstanceState);
+ View view = getLayoutInflater().inflate(R.layout.hello, null);
+ setContentView(view);
Intent received = getIntent();
int op1 = received.getIntExtra(KEY_OPERAND_1, -1);
diff --git a/core/tests/FileSystemUtilsTest/app_with_4kb_elf/src/android/test/pagesizecompat/PageSizeCompatTest.java b/core/tests/FileSystemUtilsTest/app_with_4kb_elf/src/android/test/pagesizecompat/PageSizeCompatTest.java
index 9cbe414a0993..7d05c64f7624 100644
--- a/core/tests/FileSystemUtilsTest/app_with_4kb_elf/src/android/test/pagesizecompat/PageSizeCompatTest.java
+++ b/core/tests/FileSystemUtilsTest/app_with_4kb_elf/src/android/test/pagesizecompat/PageSizeCompatTest.java
@@ -16,6 +16,8 @@
package android.test.pagesizecompat;
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -23,6 +25,10 @@ import android.content.IntentFilter;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
import org.junit.Assert;
import org.junit.Test;
@@ -33,9 +39,10 @@ import java.util.concurrent.TimeUnit;
@RunWith(AndroidJUnit4.class)
public class PageSizeCompatTest {
+ private static final String WARNING_TEXT = "PageSizeCompatTestApp";
+ private static final long TIMEOUT = 5000;
- @Test
- public void testPageSizeCompat_embedded4KbLib() throws Exception {
+ public void testPageSizeCompat_appLaunch(boolean shouldPass) throws Exception {
Context context = InstrumentationRegistry.getContext();
CountDownLatch receivedSignal = new CountDownLatch(1);
@@ -62,6 +69,30 @@ public class PageSizeCompatTest {
launchIntent.putExtra(MainActivity.KEY_OPERAND_2, op2);
context.startActivity(launchIntent);
- Assert.assertTrue("Failed to launch app", receivedSignal.await(10, TimeUnit.SECONDS));
+ UiDevice device = UiDevice.getInstance(getInstrumentation());
+ device.waitForWindowUpdate(null, TIMEOUT);
+
+ Assert.assertEquals(receivedSignal.await(10, TimeUnit.SECONDS), shouldPass);
+ }
+
+ @Test
+ public void testPageSizeCompat_compatEnabled() throws Exception {
+ testPageSizeCompat_appLaunch(true);
+ }
+
+ @Test
+ public void testPageSizeCompat_compatDisabled() throws Exception {
+ testPageSizeCompat_appLaunch(false);
+ }
+
+ @Test
+ public void testPageSizeCompat_compatByAlignmentChecks() throws Exception {
+ testPageSizeCompat_appLaunch(true);
+
+ //verify warning dialog
+ UiDevice device = UiDevice.getInstance(getInstrumentation());
+ device.waitForWindowUpdate(null, TIMEOUT);
+ UiObject2 targetObject = device.wait(Until.findObject(By.text(WARNING_TEXT)), TIMEOUT);
+ Assert.assertTrue(targetObject != null);
}
}
diff --git a/core/tests/FileSystemUtilsTest/src/com/android/internal/content/FileSystemUtilsTest.java b/core/tests/FileSystemUtilsTest/src/com/android/internal/content/FileSystemUtilsTest.java
index aed907a0242f..208d74e49afe 100644
--- a/core/tests/FileSystemUtilsTest/src/com/android/internal/content/FileSystemUtilsTest.java
+++ b/core/tests/FileSystemUtilsTest/src/com/android/internal/content/FileSystemUtilsTest.java
@@ -17,10 +17,12 @@
package com.android.internal.content;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
import android.platform.test.annotations.AppModeFull;
import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.targetprep.TargetSetupError;
import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
@@ -29,6 +31,12 @@ import org.junit.runner.RunWith;
@RunWith(DeviceJUnit4ClassRunner.class)
public class FileSystemUtilsTest extends BaseHostJUnit4Test {
+ private static final String PAGE_SIZE_COMPAT_ENABLED = "app_with_4kb_elf.apk";
+ private static final String PAGE_SIZE_COMPAT_DISABLED = "page_size_compat_disabled_app.apk";
+ private static final String PAGE_SIZE_COMPAT_ENABLED_COMPRESSED_ELF =
+ "app_with_4kb_compressed_elf.apk";
+ private static final String PAGE_SIZE_COMPAT_ENABLED_BY_PLATFORM =
+ "app_with_4kb_elf_no_override.apk";
@Test
@AppModeFull
@@ -48,12 +56,50 @@ public class FileSystemUtilsTest extends BaseHostJUnit4Test {
runDeviceTests(appPackage, appPackage + "." + testName);
}
- @Test
- @AppModeFull
- public void runAppWith4KbLib_overrideCompatMode() throws DeviceNotAvailableException {
+ private void runPageSizeCompatTest(String appName, String testMethodName)
+ throws DeviceNotAvailableException, TargetSetupError {
+ getDevice().enableAdbRoot();
+ String result = getDevice().executeShellCommand("getconf PAGE_SIZE");
+ assumeTrue("16384".equals(result.strip()));
+ installPackage(appName, "-r");
String appPackage = "android.test.pagesizecompat";
String testName = "PageSizeCompatTest";
assertTrue(isPackageInstalled(appPackage));
- runDeviceTests(appPackage, appPackage + "." + testName);
+ assertTrue(runDeviceTests(appPackage, appPackage + "." + testName,
+ testMethodName));
+ uninstallPackage(appPackage);
+ }
+
+ @Test
+ @AppModeFull
+ public void runAppWith4KbLib_overrideCompatMode()
+ throws DeviceNotAvailableException, TargetSetupError {
+ runPageSizeCompatTest(PAGE_SIZE_COMPAT_ENABLED, "testPageSizeCompat_compatEnabled");
+ }
+
+ @Test
+ @AppModeFull
+ public void runAppWith4KbCompressedLib_overrideCompatMode()
+ throws DeviceNotAvailableException, TargetSetupError {
+ runPageSizeCompatTest(PAGE_SIZE_COMPAT_ENABLED_COMPRESSED_ELF,
+ "testPageSizeCompat_compatEnabled");
+ }
+
+ @Test
+ @AppModeFull
+ public void runAppWith4KbLib_disabledCompatMode()
+ throws DeviceNotAvailableException, TargetSetupError {
+ // This test is expected to fail since compat is disabled in manifest
+ runPageSizeCompatTest(PAGE_SIZE_COMPAT_DISABLED,
+ "testPageSizeCompat_compatDisabled");
+ }
+
+ @Test
+ @AppModeFull
+ public void runAppWith4KbLib_compatByAlignmentChecks()
+ throws DeviceNotAvailableException, TargetSetupError {
+ // This test is expected to fail since compat is disabled in manifest
+ runPageSizeCompatTest(PAGE_SIZE_COMPAT_ENABLED_BY_PLATFORM,
+ "testPageSizeCompat_compatByAlignmentChecks");
}
}
diff --git a/core/tests/coretests/src/android/app/NotificationManagerTest.java b/core/tests/coretests/src/android/app/NotificationManagerTest.java
index 41432294b3c2..9b97c8feaf12 100644
--- a/core/tests/coretests/src/android/app/NotificationManagerTest.java
+++ b/core/tests/coretests/src/android/app/NotificationManagerTest.java
@@ -19,7 +19,6 @@ package android.app;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeast;
@@ -304,9 +303,8 @@ public class NotificationManagerTest {
// It doesn't matter what the returned contents are, as long as we return a channel.
// This setup must set up getNotificationChannels(), as that's the method called.
- when(mNotificationManager.mBackendService.getOrCreateNotificationChannels(any(), any(),
- anyInt(), anyBoolean())).thenReturn(
- new ParceledListSlice<>(List.of(exampleChannel())));
+ when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(),
+ anyInt())).thenReturn(new ParceledListSlice<>(List.of(exampleChannel())));
// ask for the same channel 100 times without invalidating the cache
for (int i = 0; i < 100; i++) {
@@ -318,7 +316,7 @@ public class NotificationManagerTest {
NotificationChannel unused = mNotificationManager.getNotificationChannel("id");
verify(mNotificationManager.mBackendService, times(2))
- .getOrCreateNotificationChannels(any(), any(), anyInt(), anyBoolean());
+ .getNotificationChannels(any(), any(), anyInt());
}
@Test
@@ -331,24 +329,23 @@ public class NotificationManagerTest {
NotificationChannel c2 = new NotificationChannel("id2", "name2",
NotificationManager.IMPORTANCE_NONE);
- when(mNotificationManager.mBackendService.getOrCreateNotificationChannels(any(), any(),
- anyInt(), anyBoolean())).thenReturn(new ParceledListSlice<>(List.of(c1, c2)));
+ when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(),
+ anyInt())).thenReturn(new ParceledListSlice<>(List.of(c1, c2)));
assertThat(mNotificationManager.getNotificationChannel("id1")).isEqualTo(c1);
assertThat(mNotificationManager.getNotificationChannel("id2")).isEqualTo(c2);
assertThat(mNotificationManager.getNotificationChannel("id3")).isNull();
verify(mNotificationManager.mBackendService, times(1))
- .getOrCreateNotificationChannels(any(), any(), anyInt(), anyBoolean());
+ .getNotificationChannels(any(), any(), anyInt());
}
@Test
@EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
public void getNotificationChannels_cachedUntilInvalidated() throws Exception {
NotificationManager.invalidateNotificationChannelCache();
- when(mNotificationManager.mBackendService.getOrCreateNotificationChannels(any(), any(),
- anyInt(), anyBoolean())).thenReturn(
- new ParceledListSlice<>(List.of(exampleChannel())));
+ when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(),
+ anyInt())).thenReturn(new ParceledListSlice<>(List.of(exampleChannel())));
// ask for channels 100 times without invalidating the cache
for (int i = 0; i < 100; i++) {
@@ -360,7 +357,7 @@ public class NotificationManagerTest {
List<NotificationChannel> res = mNotificationManager.getNotificationChannels();
verify(mNotificationManager.mBackendService, times(2))
- .getOrCreateNotificationChannels(any(), any(), anyInt(), anyBoolean());
+ .getNotificationChannels(any(), any(), anyInt());
assertThat(res).containsExactlyElementsIn(List.of(exampleChannel()));
}
@@ -378,9 +375,8 @@ public class NotificationManagerTest {
NotificationChannel c2 = new NotificationChannel("other", "name2",
NotificationManager.IMPORTANCE_DEFAULT);
- when(mNotificationManager.mBackendService.getOrCreateNotificationChannels(any(), any(),
- anyInt(), anyBoolean())).thenReturn(
- new ParceledListSlice<>(List.of(c1, conv1, c2)));
+ when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(),
+ anyInt())).thenReturn(new ParceledListSlice<>(List.of(c1, conv1, c2)));
// Lookup for channel c1 and c2: returned as expected
assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(c1);
@@ -397,9 +393,9 @@ public class NotificationManagerTest {
// Lookup of a nonexistent channel is null
assertThat(mNotificationManager.getNotificationChannel("id3")).isNull();
- // All of that should have been one call to getOrCreateNotificationChannels()
+ // All of that should have been one call to getNotificationChannels()
verify(mNotificationManager.mBackendService, times(1))
- .getOrCreateNotificationChannels(any(), any(), anyInt(), anyBoolean());
+ .getNotificationChannels(any(), any(), anyInt());
}
@Test
@@ -419,12 +415,12 @@ public class NotificationManagerTest {
NotificationChannel channel3 = channel1.copy();
channel3.setName("name3");
- when(mNotificationManager.mBackendService.getOrCreateNotificationChannels(any(), eq(pkg1),
- eq(userId), anyBoolean())).thenReturn(new ParceledListSlice<>(List.of(channel1)));
- when(mNotificationManager.mBackendService.getOrCreateNotificationChannels(any(), eq(pkg2),
- eq(userId), anyBoolean())).thenReturn(new ParceledListSlice<>(List.of(channel2)));
- when(mNotificationManager.mBackendService.getOrCreateNotificationChannels(any(), eq(pkg1),
- eq(userId1), anyBoolean())).thenReturn(new ParceledListSlice<>(List.of(channel3)));
+ when(mNotificationManager.mBackendService.getNotificationChannels(any(), eq(pkg1),
+ eq(userId))).thenReturn(new ParceledListSlice<>(List.of(channel1)));
+ when(mNotificationManager.mBackendService.getNotificationChannels(any(), eq(pkg2),
+ eq(userId))).thenReturn(new ParceledListSlice<>(List.of(channel2)));
+ when(mNotificationManager.mBackendService.getNotificationChannels(any(), eq(pkg1),
+ eq(userId1))).thenReturn(new ParceledListSlice<>(List.of(channel3)));
// set our context to pretend to be from package 1 and userId 0
mContext.setParameters(pkg1, pkg1, userId);
@@ -440,7 +436,7 @@ public class NotificationManagerTest {
// Those should have been three different calls
verify(mNotificationManager.mBackendService, times(3))
- .getOrCreateNotificationChannels(any(), any(), anyInt(), anyBoolean());
+ .getNotificationChannels(any(), any(), anyInt());
}
@Test
diff --git a/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java b/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java
index 1bdb006c3465..9f1580cb8b57 100644
--- a/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java
+++ b/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java
@@ -124,7 +124,8 @@ public class NotificationRankingUpdateTest {
getRankingAdjustment(i),
isBubble(i),
getProposedImportance(i),
- hasSensitiveContent(i)
+ hasSensitiveContent(i),
+ getSummarization(i)
);
rankings[i] = ranking;
}
@@ -334,6 +335,17 @@ public class NotificationRankingUpdateTest {
}
/**
+ * Produces a String that can be used to represent getSummarization, based on the provided
+ * index.
+ */
+ public static String getSummarization(int index) {
+ if ((android.app.Flags.nmSummarizationUi() || android.app.Flags.nmSummarization())) {
+ return "summary " + index;
+ }
+ return null;
+ }
+
+ /**
* Produces a boolean that can be used to represent isBubble, based on the provided index.
*/
public static boolean isBubble(int index) {
@@ -461,7 +473,8 @@ public class NotificationRankingUpdateTest {
/* rankingAdjustment= */ 0,
/* isBubble= */ false,
/* proposedImportance= */ 0,
- /* sensitiveContent= */ false
+ /* sensitiveContent= */ false,
+ /* summarization = */ null
);
return ranking;
}
@@ -550,7 +563,8 @@ public class NotificationRankingUpdateTest {
tweak.getRankingAdjustment(),
tweak.isBubble(),
tweak.getProposedImportance(),
- tweak.hasSensitiveContent()
+ tweak.hasSensitiveContent(),
+ tweak.getSummarization()
);
assertNotEquals(nru, nru2);
}
diff --git a/core/tests/coretests/src/android/window/DesktopExperienceFlagsTest.java b/core/tests/coretests/src/android/window/DesktopExperienceFlagsTest.java
new file mode 100644
index 000000000000..cc06f3d21332
--- /dev/null
+++ b/core/tests/coretests/src/android/window/DesktopExperienceFlagsTest.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+import static com.android.window.flags.Flags.FLAG_SHOW_DESKTOP_EXPERIENCE_DEV_OPTION;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeTrue;
+import static org.junit.Assume.assumeFalse;
+
+import android.content.Context;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.FlagsParameterization;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.support.test.uiautomator.UiDevice;
+import android.window.DesktopExperienceFlags.DesktopExperienceFlag;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.window.flags.Flags;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
+import java.lang.reflect.Field;
+import java.util.List;
+
+/**
+ * Test class for {@link android.window.DesktopExperienceFlags}
+ *
+ * <p>Build/Install/Run: atest FrameworksCoreTests:DesktopExperienceFlagsTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(ParameterizedAndroidJunit4.class)
+public class DesktopExperienceFlagsTest {
+
+ @Parameters(name = "{0}")
+ public static List<FlagsParameterization> getParams() {
+ return FlagsParameterization.allCombinationsOf(FLAG_SHOW_DESKTOP_EXPERIENCE_DEV_OPTION);
+ }
+
+ @Rule public SetFlagsRule mSetFlagsRule;
+
+ private UiDevice mUiDevice;
+ private Context mContext;
+ private boolean mLocalFlagValue = false;
+ private final DesktopExperienceFlag mOverriddenLocalFlag =
+ new DesktopExperienceFlag(() -> mLocalFlagValue, true);
+ private final DesktopExperienceFlag mNotOverriddenLocalFlag =
+ new DesktopExperienceFlag(() -> mLocalFlagValue, false);
+
+ private static final String OVERRIDE_OFF_SETTING = "0";
+ private static final String OVERRIDE_ON_SETTING = "1";
+ private static final String OVERRIDE_INVALID_SETTING = "garbage";
+
+ public DesktopExperienceFlagsTest(FlagsParameterization flags) {
+ mSetFlagsRule = new SetFlagsRule(flags);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+ setSysProp(null);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ resetCache();
+ setSysProp(null);
+ }
+
+ @Test
+ public void isTrue_overrideOff_featureFlagOn_returnsTrue() throws Exception {
+ mLocalFlagValue = true;
+ setSysProp(OVERRIDE_OFF_SETTING);
+
+ assertThat(mOverriddenLocalFlag.isTrue()).isTrue();
+ assertThat(mNotOverriddenLocalFlag.isTrue()).isTrue();
+ }
+
+ @Test
+ public void isTrue_overrideOn_featureFlagOn_returnsTrue() throws Exception {
+ mLocalFlagValue = true;
+ setSysProp(OVERRIDE_ON_SETTING);
+
+ assertThat(mOverriddenLocalFlag.isTrue()).isTrue();
+ assertThat(mNotOverriddenLocalFlag.isTrue()).isTrue();
+ }
+
+ @Test
+ public void isTrue_overrideOff_featureFlagOff_returnsFalse() throws Exception {
+ mLocalFlagValue = false;
+ setSysProp(OVERRIDE_OFF_SETTING);
+
+ assertThat(mOverriddenLocalFlag.isTrue()).isFalse();
+ assertThat(mNotOverriddenLocalFlag.isTrue()).isFalse();
+ }
+
+ @Test
+ public void isTrue_devOptionEnabled_overrideOn_featureFlagOff() throws Exception {
+ assumeTrue(Flags.showDesktopExperienceDevOption());
+ mLocalFlagValue = false;
+ setSysProp(OVERRIDE_ON_SETTING);
+
+ assertThat(mOverriddenLocalFlag.isTrue()).isTrue();
+ assertThat(mNotOverriddenLocalFlag.isTrue()).isFalse();
+ }
+
+ @Test
+ public void isTrue_devOptionDisabled_overrideOn_featureFlagOff_returnsFalse() throws Exception {
+ assumeFalse(Flags.showDesktopExperienceDevOption());
+ mLocalFlagValue = false;
+ setSysProp(OVERRIDE_ON_SETTING);
+
+ assertThat(mOverriddenLocalFlag.isTrue()).isFalse();
+ assertThat(mNotOverriddenLocalFlag.isTrue()).isFalse();
+ }
+
+ private void setSysProp(String value) throws Exception {
+ if (value == null) {
+ resetSysProp();
+ } else {
+ mUiDevice.executeShellCommand(
+ "setprop " + DesktopModeFlags.SYSTEM_PROPERTY_NAME + " " + value);
+ }
+ }
+
+ private void resetSysProp() throws Exception {
+ mUiDevice.executeShellCommand("setprop " + DesktopModeFlags.SYSTEM_PROPERTY_NAME + " ''");
+ }
+
+ private void resetCache() throws Exception {
+ Field cachedToggleOverride =
+ DesktopExperienceFlags.class.getDeclaredField("sCachedToggleOverride");
+ cachedToggleOverride.setAccessible(true);
+ cachedToggleOverride.set(null, null);
+ }
+}
diff --git a/core/tests/coretests/src/android/window/DesktopModeFlagsTest.java b/core/tests/coretests/src/android/window/DesktopModeFlagsTest.java
index b28e2b04b342..49927be65ae5 100644
--- a/core/tests/coretests/src/android/window/DesktopModeFlagsTest.java
+++ b/core/tests/coretests/src/android/window/DesktopModeFlagsTest.java
@@ -21,33 +21,42 @@ import static android.window.DesktopModeFlags.ToggleOverride.OVERRIDE_OFF;
import static android.window.DesktopModeFlags.ToggleOverride.OVERRIDE_ON;
import static android.window.DesktopModeFlags.ToggleOverride.OVERRIDE_UNSET;
import static android.window.DesktopModeFlags.ToggleOverride.fromSetting;
-import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TRANSITIONS;
import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE;
-import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TRANSITIONS;
+import static com.android.window.flags.Flags.FLAG_SHOW_DESKTOP_EXPERIENCE_DEV_OPTION;
import static com.android.window.flags.Flags.FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeTrue;
+
import android.content.ContentResolver;
import android.content.Context;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.FlagsParameterization;
import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.Settings;
+import android.support.test.uiautomator.UiDevice;
+import android.window.DesktopModeFlags.DesktopModeFlag;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.window.flags.Flags;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
import java.lang.reflect.Field;
+import java.util.List;
/**
* Test class for {@link android.window.DesktopModeFlags}
@@ -57,21 +66,39 @@ import java.lang.reflect.Field;
*/
@SmallTest
@Presubmit
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedAndroidJunit4.class)
public class DesktopModeFlagsTest {
+ @Parameters(name = "{0}")
+ public static List<FlagsParameterization> getParams() {
+ return FlagsParameterization.allCombinationsOf(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION,
+ FLAG_SHOW_DESKTOP_EXPERIENCE_DEV_OPTION);
+ }
+
@Rule
- public SetFlagsRule setFlagsRule = new SetFlagsRule();
+ public SetFlagsRule mSetFlagsRule;
+ private UiDevice mUiDevice;
private Context mContext;
+ private boolean mLocalFlagValue = false;
+ private final DesktopModeFlag mOverriddenLocalFlag = new DesktopModeFlag(
+ () -> mLocalFlagValue, true);
+ private final DesktopModeFlag mNotOverriddenLocalFlag = new DesktopModeFlag(
+ () -> mLocalFlagValue, false);
private static final int OVERRIDE_OFF_SETTING = 0;
private static final int OVERRIDE_ON_SETTING = 1;
private static final int OVERRIDE_UNSET_SETTING = -1;
+ public DesktopModeFlagsTest(FlagsParameterization flags) {
+ mSetFlagsRule = new SetFlagsRule(flags);
+ }
+
@Before
- public void setUp() {
+ public void setUp() throws Exception {
mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+ setOverride(null);
}
@After
@@ -80,26 +107,35 @@ public class DesktopModeFlagsTest {
}
@Test
- @DisableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
- public void isTrue_devOptionFlagDisabled_overrideOff_featureFlagOn_returnsTrue() {
+ public void isTrue_overrideOff_featureFlagOn() throws Exception {
setOverride(OVERRIDE_OFF_SETTING);
- // In absence of dev options, follow flag
- assertThat(ENABLE_DESKTOP_WINDOWING_MODE.isTrue()).isTrue();
+
+ if (showDesktopWindowingDevOpts()) {
+ // DW Dev Opts turns off flags when ON
+ assertThat(ENABLE_DESKTOP_WINDOWING_MODE.isTrue()).isFalse();
+ } else {
+ // DE Dev Opts doesn't turn flags OFF
+ assertThat(ENABLE_DESKTOP_WINDOWING_MODE.isTrue()).isTrue();
+ }
}
@Test
- @DisableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
- public void isTrue_devOptionFlagDisabled_overrideOn_featureFlagOff_returnsFalse() {
+ @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+ public void isTrue_overrideOn_featureFlagOff() throws Exception {
setOverride(OVERRIDE_ON_SETTING);
- assertThat(ENABLE_DESKTOP_WINDOWING_MODE.isTrue()).isFalse();
+ if (showAnyDevOpts()) {
+ assertThat(ENABLE_DESKTOP_WINDOWING_MODE.isTrue()).isTrue();
+ } else {
+ assertThat(ENABLE_DESKTOP_WINDOWING_MODE.isTrue()).isFalse();
+ }
}
@Test
- @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
- public void isTrue_overrideUnset_featureFlagOn_returnsTrue() {
+ @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+ public void isTrue_overrideUnset_featureFlagOn() throws Exception {
setOverride(OVERRIDE_UNSET_SETTING);
// For overridableFlag, for unset overrides, follow flag
@@ -107,9 +143,8 @@ public class DesktopModeFlagsTest {
}
@Test
- @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
@DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
- public void isTrue_overrideUnset_featureFlagOff_returnsFalse() {
+ public void isTrue_overrideUnset_featureFlagOff() throws Exception {
setOverride(OVERRIDE_UNSET_SETTING);
// For overridableFlag, for unset overrides, follow flag
@@ -117,8 +152,8 @@ public class DesktopModeFlagsTest {
}
@Test
- @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
- public void isTrue_noOverride_featureFlagOn_returnsTrue() {
+ @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+ public void isTrue_noOverride_featureFlagOn_returnsTrue() throws Exception {
setOverride(null);
// For overridableFlag, in absence of overrides, follow flag
@@ -126,9 +161,8 @@ public class DesktopModeFlagsTest {
}
@Test
- @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
@DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
- public void isTrue_noOverride_featureFlagOff_returnsFalse() {
+ public void isTrue_noOverride_featureFlagOff_returnsFalse() throws Exception {
setOverride(null);
// For overridableFlag, in absence of overrides, follow flag
@@ -136,8 +170,8 @@ public class DesktopModeFlagsTest {
}
@Test
- @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
- public void isTrue_unrecognizableOverride_featureFlagOn_returnsTrue() {
+ @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+ public void isTrue_unrecognizableOverride_featureFlagOn_returnsTrue() throws Exception {
setOverride(-2);
// For overridableFlag, for unrecognized overrides, follow flag
@@ -145,9 +179,8 @@ public class DesktopModeFlagsTest {
}
@Test
- @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
@DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
- public void isTrue_unrecognizableOverride_featureFlagOff_returnsFalse() {
+ public void isTrue_unrecognizableOverride_featureFlagOff_returnsFalse() throws Exception {
setOverride(-2);
// For overridableFlag, for unrecognizable overrides, follow flag
@@ -155,27 +188,10 @@ public class DesktopModeFlagsTest {
}
@Test
- @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
- public void isTrue_overrideOff_featureFlagOn_returnsFalse() {
- setOverride(OVERRIDE_OFF_SETTING);
-
- // For overridableFlag, follow override if they exist
- assertThat(ENABLE_DESKTOP_WINDOWING_MODE.isTrue()).isFalse();
- }
-
- @Test
- @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
- @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
- public void isTrue_overrideOn_featureFlagOff_returnsTrue() {
- setOverride(OVERRIDE_ON_SETTING);
-
- // For overridableFlag, follow override if they exist
- assertThat(ENABLE_DESKTOP_WINDOWING_MODE.isTrue()).isTrue();
- }
+ @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+ public void isTrue_overrideOffThenOn_featureFlagOn_returnsFalseAndFalse() throws Exception {
+ assumeTrue(showDesktopWindowingDevOpts());
- @Test
- @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
- public void isTrue_overrideOffThenOn_featureFlagOn_returnsFalseAndFalse() {
setOverride(OVERRIDE_OFF_SETTING);
// For overridableFlag, follow override if they exist
@@ -188,9 +204,9 @@ public class DesktopModeFlagsTest {
}
@Test
- @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
@DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
- public void isTrue_overrideOnThenOff_featureFlagOff_returnsTrueAndTrue() {
+ public void isTrue_overrideOnThenOff_featureFlagOff_returnsTrueAndTrue() throws Exception {
+ assumeTrue(showAnyDevOpts());
setOverride(OVERRIDE_ON_SETTING);
// For overridableFlag, follow override if they exist
@@ -203,146 +219,144 @@ public class DesktopModeFlagsTest {
}
@Test
- @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
- FLAG_ENABLE_DESKTOP_WINDOWING_TRANSITIONS})
- public void isTrue_dwFlagOn_overrideUnset_featureFlagOn_returnsTrue() {
+ @EnableFlags({FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+ public void isTrue_dwFlagOn_overrideUnset_featureFlagOn() throws Exception {
+ mLocalFlagValue = true;
setOverride(OVERRIDE_UNSET_SETTING);
// For unset overrides, follow flag
- assertThat(ENABLE_DESKTOP_WINDOWING_TRANSITIONS.isTrue()).isTrue();
+ assertThat(mOverriddenLocalFlag.isTrue()).isTrue();
+ assertThat(mNotOverriddenLocalFlag.isTrue()).isTrue();
}
@Test
- @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
- @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_TRANSITIONS)
- public void isTrue_dwFlagOn_overrideUnset_featureFlagOff_returnsFalse() {
+ @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+ public void isTrue_dwFlagOn_overrideUnset_featureFlagOff() throws Exception {
+ mLocalFlagValue = false;
setOverride(OVERRIDE_UNSET_SETTING);
// For unset overrides, follow flag
- assertThat(ENABLE_DESKTOP_WINDOWING_TRANSITIONS.isTrue()).isFalse();
+ assertThat(mOverriddenLocalFlag.isTrue()).isFalse();
+ assertThat(mNotOverriddenLocalFlag.isTrue()).isFalse();
}
@Test
- @EnableFlags({
- FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION,
- FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
- FLAG_ENABLE_DESKTOP_WINDOWING_TRANSITIONS
- })
- public void isTrue_dwFlagOn_overrideOn_featureFlagOn_returnsTrue() {
+ @EnableFlags({FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+ public void isTrue_dwFlagOn_overrideOn_featureFlagOn() throws Exception {
+ mLocalFlagValue = true;
setOverride(OVERRIDE_ON_SETTING);
// When toggle override matches its default state (dw flag), don't override flags
- assertThat(ENABLE_DESKTOP_WINDOWING_TRANSITIONS.isTrue()).isTrue();
+ assertThat(mOverriddenLocalFlag.isTrue()).isTrue();
+ assertThat(mNotOverriddenLocalFlag.isTrue()).isTrue();
}
@Test
- @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
- @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_TRANSITIONS)
- public void isTrue_dwFlagOn_overrideOn_featureFlagOff_returnsFalse() {
+ @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+ public void isTrue_dwFlagOn_overrideOn_featureFlagOff() throws Exception {
+ mLocalFlagValue = false;
setOverride(OVERRIDE_ON_SETTING);
- // When toggle override matches its default state (dw flag), don't override flags
- assertThat(ENABLE_DESKTOP_WINDOWING_TRANSITIONS.isTrue()).isFalse();
+ if (showDesktopExperienceDevOpts()) {
+ assertThat(mOverriddenLocalFlag.isTrue()).isTrue();
+ } else {
+ assertThat(mOverriddenLocalFlag.isTrue()).isFalse();
+ }
+ assertThat(mNotOverriddenLocalFlag.isTrue()).isFalse();
}
@Test
- @EnableFlags({
- FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION,
- FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
- FLAG_ENABLE_DESKTOP_WINDOWING_TRANSITIONS
- })
- public void isTrue_dwFlagOn_overrideOff_featureFlagOn_returnsTrue() {
+ @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+ public void isTrue_dwFlagOn_overrideOff_featureFlagOn() throws Exception {
+ mLocalFlagValue = true;
setOverride(OVERRIDE_OFF_SETTING);
- // Follow override if they exist, and is not equal to default toggle state (dw flag)
- assertThat(ENABLE_DESKTOP_WINDOWING_TRANSITIONS.isTrue()).isTrue();
+ if (showDesktopWindowingDevOpts()) {
+ // Follow override if they exist, and is not equal to default toggle state (dw flag)
+ assertThat(mOverriddenLocalFlag.isTrue()).isFalse();
+ } else {
+ assertThat(mOverriddenLocalFlag.isTrue()).isTrue();
+ }
+ assertThat(mNotOverriddenLocalFlag.isTrue()).isTrue();
}
@Test
- @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
- @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_TRANSITIONS)
- public void isTrue_dwFlagOn_overrideOff_featureFlagOff_returnsFalse() {
+ @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+ public void isTrue_dwFlagOn_overrideOff_featureFlagOff_returnsFalse() throws Exception {
+ mLocalFlagValue = false;
setOverride(OVERRIDE_OFF_SETTING);
// Follow override if they exist, and is not equal to default toggle state (dw flag)
- assertThat(ENABLE_DESKTOP_WINDOWING_TRANSITIONS.isTrue()).isFalse();
+ assertThat(mOverriddenLocalFlag.isTrue()).isFalse();
+ assertThat(mNotOverriddenLocalFlag.isTrue()).isFalse();
}
@Test
- @EnableFlags({
- FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION,
- FLAG_ENABLE_DESKTOP_WINDOWING_TRANSITIONS
- })
@DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
- public void isTrue_dwFlagOff_overrideUnset_featureFlagOn_returnsTrue() {
+ public void isTrue_dwFlagOff_overrideUnset_featureFlagOn_returnsTrue() throws Exception {
+ mLocalFlagValue = true;
setOverride(OVERRIDE_UNSET_SETTING);
// For unset overrides, follow flag
- assertThat(ENABLE_DESKTOP_WINDOWING_TRANSITIONS.isTrue()).isTrue();
+ assertThat(mOverriddenLocalFlag.isTrue()).isTrue();
+ assertThat(mNotOverriddenLocalFlag.isTrue()).isTrue();
}
@Test
- @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
- @DisableFlags({
- FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
- FLAG_ENABLE_DESKTOP_WINDOWING_TRANSITIONS
- })
- public void isTrue_dwFlagOff_overrideUnset_featureFlagOff_returnsFalse() {
+ @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+ public void isTrue_dwFlagOff_overrideUnset_featureFlagOff_returnsFalse() throws Exception {
+ mLocalFlagValue = false;
setOverride(OVERRIDE_UNSET_SETTING);
// For unset overrides, follow flag
- assertThat(ENABLE_DESKTOP_WINDOWING_TRANSITIONS.isTrue()).isFalse();
+ assertThat(mOverriddenLocalFlag.isTrue()).isFalse();
+ assertThat(mNotOverriddenLocalFlag.isTrue()).isFalse();
}
@Test
- @EnableFlags({
- FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION,
- FLAG_ENABLE_DESKTOP_WINDOWING_TRANSITIONS
- })
@DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
- public void isTrue_dwFlagOff_overrideOn_featureFlagOn_returnsTrue() {
+ public void isTrue_dwFlagOff_overrideOn_featureFlagOn_returnsTrue() throws Exception {
+ mLocalFlagValue = true;
setOverride(OVERRIDE_ON_SETTING);
// Follow override if they exist, and is not equal to default toggle state (dw flag)
- assertThat(ENABLE_DESKTOP_WINDOWING_TRANSITIONS.isTrue()).isTrue();
+ assertThat(mOverriddenLocalFlag.isTrue()).isTrue();
+ assertThat(mNotOverriddenLocalFlag.isTrue()).isTrue();
}
@Test
- @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
- @DisableFlags({
- FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
- FLAG_ENABLE_DESKTOP_WINDOWING_TRANSITIONS
- })
- public void isTrue_dwFlagOff_overrideOn_featureFlagOff_returnFalse() {
+ @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+ public void isTrue_dwFlagOff_overrideOn_featureFlagOff() throws Exception {
+ mLocalFlagValue = false;
setOverride(OVERRIDE_ON_SETTING);
- // Follow override if they exist, and is not equal to default toggle state (dw flag)
- assertThat(ENABLE_DESKTOP_WINDOWING_TRANSITIONS.isTrue()).isFalse();
+ if (showAnyDevOpts()) {
+ assertThat(mOverriddenLocalFlag.isTrue()).isTrue();
+ } else {
+ // Follow override if they exist, and is not equal to default toggle state (dw flag)
+ assertThat(mOverriddenLocalFlag.isTrue()).isFalse();
+ }
+ assertThat(mNotOverriddenLocalFlag.isTrue()).isFalse();
}
@Test
- @EnableFlags({
- FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION,
- FLAG_ENABLE_DESKTOP_WINDOWING_TRANSITIONS
- })
@DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
- public void isTrue_dwFlagOff_overrideOff_featureFlagOn_returnsTrue() {
+ public void isTrue_dwFlagOff_overrideOff_featureFlagOn_returnsTrue() throws Exception {
+ mLocalFlagValue = true;
setOverride(OVERRIDE_OFF_SETTING);
// When toggle override matches its default state (dw flag), don't override flags
- assertThat(ENABLE_DESKTOP_WINDOWING_TRANSITIONS.isTrue()).isTrue();
+ assertThat(mOverriddenLocalFlag.isTrue()).isTrue();
+ assertThat(mNotOverriddenLocalFlag.isTrue()).isTrue();
}
@Test
- @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
- @DisableFlags({
- FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
- FLAG_ENABLE_DESKTOP_WINDOWING_TRANSITIONS
- })
- public void isTrue_dwFlagOff_overrideOff_featureFlagOff_returnsFalse() {
+ @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+ public void isTrue_dwFlagOff_overrideOff_featureFlagOff_returnsFalse() throws Exception {
+ mLocalFlagValue = false;
setOverride(OVERRIDE_OFF_SETTING);
- // When toggle override matches its default state (dw flag), don't override flags
- assertThat(ENABLE_DESKTOP_WINDOWING_TRANSITIONS.isTrue()).isFalse();
+ assertThat(mOverriddenLocalFlag.isTrue()).isFalse();
+ assertThat(mNotOverriddenLocalFlag.isTrue()).isFalse();
}
@Test
@@ -365,7 +379,9 @@ public class DesktopModeFlagsTest {
assertThat(OVERRIDE_UNSET.getSetting()).isEqualTo(-1);
}
- private void setOverride(Integer setting) {
+ private void setOverride(Integer setting) throws Exception {
+ setSysProp(setting);
+
ContentResolver contentResolver = mContext.getContentResolver();
String key = Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES;
@@ -376,11 +392,35 @@ public class DesktopModeFlagsTest {
}
}
+ private void setSysProp(Integer value) throws Exception {
+ if (value == null) {
+ resetSysProp();
+ } else {
+ mUiDevice.executeShellCommand(
+ "setprop " + DesktopModeFlags.SYSTEM_PROPERTY_NAME + " " + value);
+ }
+ }
+
+ private void resetSysProp() throws Exception {
+ mUiDevice.executeShellCommand("setprop " + DesktopModeFlags.SYSTEM_PROPERTY_NAME + " ''");
+ }
+
private void resetCache() throws Exception {
Field cachedToggleOverride = DesktopModeFlags.class.getDeclaredField(
"sCachedToggleOverride");
cachedToggleOverride.setAccessible(true);
cachedToggleOverride.set(null, null);
- setOverride(OVERRIDE_UNSET_SETTING);
+ }
+
+ private boolean showDesktopWindowingDevOpts() {
+ return Flags.showDesktopWindowingDevOption() && !Flags.showDesktopExperienceDevOption();
+ }
+
+ private boolean showDesktopExperienceDevOpts() {
+ return Flags.showDesktopExperienceDevOption();
+ }
+
+ private boolean showAnyDevOpts() {
+ return Flags.showDesktopWindowingDevOption() || Flags.showDesktopExperienceDevOption();
}
}
diff --git a/data/etc/com.android.systemui.xml b/data/etc/com.android.systemui.xml
index 45952ea75b6f..3eadf3b94515 100644
--- a/data/etc/com.android.systemui.xml
+++ b/data/etc/com.android.systemui.xml
@@ -95,5 +95,6 @@
<permission name="android.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND" />
<permission name="android.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW"/>
<permission name="android.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE" />
+ <permission name="android.permission.SET_UNRESTRICTED_GESTURE_EXCLUSION" />
</privapp-permissions>
</permissions>
diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java
index b332cf0d751f..3d4dccf095f5 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -2198,10 +2198,12 @@ public class Paint {
* is configured as {@code 'wght' 500, 'ital' 1}, and if the override is specified as
* {@code 'wght' 700, `wdth` 150}, then the effective font variation setting is
* {@code `wght' 700, 'ital' 1, 'wdth' 150}. The `wght` value is updated by override, 'ital'
- * value is preserved because no overrides, and `wdth` value is added by override.
+ * value is preserved because no overrides, and `wdth` value is added by override. If the font
+ * variation override is empty or null, nothing overrides and original font variation settings
+ * assigned to the font instance is used as it is.
*
- * @param fontVariationOverride font variation settings. You can pass null or empty string as
- * no variation settings.
+ * @param fontVariationOverride font variation override. You can pass null or empty string for
+ * clearing font variation override.
*
* @return true if the provided font variation settings is valid. Otherwise returns false.
*
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index d0d1721115cb..1bcb0bb91515 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -18,6 +18,7 @@ package androidx.window.extensions.embedding;
import static android.app.ActivityManager.START_SUCCESS;
import static android.app.ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN;
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.view.Display.DEFAULT_DISPLAY;
@@ -3154,15 +3155,22 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
final WindowContainerTransaction wct = transactionRecord.getTransaction();
final TaskFragmentContainer launchedInTaskFragment;
if (launchingActivity != null) {
- final int taskId = getTaskId(launchingActivity);
final String overlayTag = options.getString(KEY_OVERLAY_TAG);
if (Flags.activityEmbeddingOverlayPresentationFlag()
&& overlayTag != null) {
launchedInTaskFragment = createOrUpdateOverlayTaskFragmentIfNeeded(wct,
options, intent, launchingActivity);
} else {
- launchedInTaskFragment = resolveStartActivityIntent(wct, taskId, intent,
- launchingActivity);
+ final int taskId = getTaskId(launchingActivity);
+ if (taskId != INVALID_TASK_ID) {
+ launchedInTaskFragment = resolveStartActivityIntent(wct, taskId, intent,
+ launchingActivity);
+ } else {
+ // We cannot get a valid task id of launchingActivity so we fall back to
+ // treat it as a non-Activity context.
+ launchedInTaskFragment =
+ resolveStartActivityIntentFromNonActivityContext(wct, intent);
+ }
}
} else {
launchedInTaskFragment = resolveStartActivityIntentFromNonActivityContext(wct,
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt
index dd387b382dc6..09a93d501f8e 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt
@@ -296,5 +296,9 @@ class BubbleControllerBubbleBarTest {
override fun onBubbleStateChange(update: BubbleBarUpdate?) {}
override fun animateBubbleBarLocation(location: BubbleBarLocation?) {}
+
+ override fun onDragItemOverBubbleBarDragZone(location: BubbleBarLocation) {}
+
+ override fun onItemDraggedOutsideBubbleBarDropZone() {}
}
}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java
index 2ca011bfe000..0a1e3b9495a0 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java
@@ -111,7 +111,7 @@ public class GroupedTaskInfo implements Parcelable {
* Create new for a pair of tasks in split screen
*/
public static GroupedTaskInfo forSplitTasks(@NonNull TaskInfo task1,
- @NonNull TaskInfo task2, @Nullable SplitBounds splitBounds) {
+ @NonNull TaskInfo task2, @NonNull SplitBounds splitBounds) {
return new GroupedTaskInfo(List.of(task1, task2), splitBounds, TYPE_SPLIT,
null /* minimizedFreeformTasks */);
}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java
index 840de2c86f92..4d00c74155a8 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java
@@ -127,6 +127,46 @@ public class TransitionUtil {
}
/**
+ * Check if all changes in this transition are only ordering changes. If so, we won't animate.
+ */
+ public static boolean isAllOrderOnly(TransitionInfo info) {
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ if (!isOrderOnly(info.getChanges().get(i))) return false;
+ }
+ return true;
+ }
+
+ /**
+ * Look through a transition and see if all non-closing changes are no-animation. If so, no
+ * animation should play.
+ */
+ public static boolean isAllNoAnimation(TransitionInfo info) {
+ if (isClosingType(info.getType())) {
+ // no-animation is only relevant for launching (open) activities.
+ return false;
+ }
+ boolean hasNoAnimation = false;
+ final int changeSize = info.getChanges().size();
+ for (int i = changeSize - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ if (isClosingType(change.getMode())) {
+ // ignore closing apps since they are a side-effect of the transition and don't
+ // animate.
+ continue;
+ }
+ if (change.hasFlags(TransitionInfo.FLAG_NO_ANIMATION)) {
+ hasNoAnimation = true;
+ } else if (!isOrderOnly(change) && !change.hasFlags(TransitionInfo.FLAG_IS_OCCLUDED)) {
+ // Ignore the order only or occluded changes since they shouldn't be visible during
+ // animation. For anything else, we need to animate if at-least one relevant
+ // participant *is* animated,
+ return false;
+ }
+ }
+ return hasNoAnimation;
+ }
+
+ /**
* Filter that selects leaf-tasks only. THIS IS ORDER-DEPENDENT! For it to work properly, you
* MUST call `test` in the same order that the changes appear in the TransitionInfo.
*/
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt
new file mode 100644
index 000000000000..0ea3c2a80fb4
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.shared.desktopmode
+
+import android.app.TaskInfo
+import android.content.Context
+import android.window.DesktopModeFlags
+import com.android.internal.R
+
+/**
+ * Class to decide whether to apply app compat policies in desktop mode.
+ */
+// TODO(b/347289970): Consider replacing with API
+class DesktopModeCompatPolicy(context: Context) {
+
+ private val systemUiPackage: String = context.resources.getString(R.string.config_systemUi)
+
+ /**
+ * If the top activity should be exempt from desktop windowing and forced back to fullscreen.
+ * Currently includes all system ui activities and modal dialogs. However if the top activity is
+ * not being displayed, regardless of its configuration, we will not exempt it as to remain in
+ * the desktop windowing environment.
+ */
+ fun isTopActivityExemptFromDesktopWindowing(task: TaskInfo) =
+ isTopActivityExemptFromDesktopWindowing(task.baseActivity?.packageName,
+ task.numActivities, task.isTopActivityNoDisplay, task.isActivityStackTransparent)
+
+ fun isTopActivityExemptFromDesktopWindowing(packageName: String?,
+ numActivities: Int, isTopActivityNoDisplay: Boolean, isActivityStackTransparent: Boolean) =
+ DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue
+ && ((isSystemUiTask(packageName)
+ || isTransparentTask(isActivityStackTransparent, numActivities))
+ && !isTopActivityNoDisplay)
+
+ /**
+ * Returns true if all activities in a tasks stack are transparent. If there are no activities
+ * will return false.
+ */
+ fun isTransparentTask(task: TaskInfo): Boolean =
+ isTransparentTask(task.isActivityStackTransparent, task.numActivities)
+
+ private fun isTransparentTask(isActivityStackTransparent: Boolean, numActivities: Int) =
+ isActivityStackTransparent && numActivities > 0
+
+ private fun isSystemUiTask(packageName: String?) = packageName == systemUiPackage
+}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
index 2fed1380b635..1ee71ca78815 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
@@ -218,6 +218,13 @@ public class DesktopModeStatus {
return isDeviceEligibleForDesktopMode(context) && Flags.showDesktopWindowingDevOption();
}
+ /**
+ * Return {@code true} if desktop mode dev option should be shown on current device
+ */
+ public static boolean canShowDesktopExperienceDevOption(@NonNull Context context) {
+ return Flags.showDesktopExperienceDevOption();
+ }
+
/** Returns if desktop mode dev option should be enabled if there is no user override. */
public static boolean shouldDevOptionBeEnabledByDefault() {
return Flags.enableDesktopWindowingMode();
@@ -290,7 +297,7 @@ public class DesktopModeStatus {
/**
* Return {@code true} if desktop mode is unrestricted and is supported in the device.
*/
- private static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) {
+ public static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) {
return !enforceDeviceRestrictions() || isDesktopModeSupported(context);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index e8e25e20d8d8..a65e69eee5fe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -28,6 +28,7 @@ import android.annotation.Nullable;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Person;
+import android.app.TaskInfo;
import android.content.Context;
import android.content.Intent;
import android.content.LocusId;
@@ -57,6 +58,7 @@ import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.shared.bubbles.BubbleInfo;
import com.android.wm.shell.shared.bubbles.ParcelableFlyoutMessage;
+import com.android.wm.shell.taskview.TaskView;
import java.io.PrintWriter;
import java.util.List;
@@ -204,6 +206,13 @@ public class Bubble implements BubbleViewProvider {
private Intent mAppIntent;
/**
+ * Set while preparing a transition for animation. Several steps are needed before animation
+ * starts, so this is used to detect and route associated events to the coordinating transition.
+ */
+ @Nullable
+ private BubbleTransitions.BubbleTransition mPreparingTransition;
+
+ /**
* Create a bubble with limited information based on given {@link ShortcutInfo}.
* Note: Currently this is only being used when the bubble is persisted to disk.
*/
@@ -280,6 +289,30 @@ public class Bubble implements BubbleViewProvider {
mShortcutInfo = info;
}
+ private Bubble(
+ TaskInfo task,
+ UserHandle user,
+ @Nullable Icon icon,
+ String key,
+ @ShellMainThread Executor mainExecutor,
+ @ShellBackgroundThread Executor bgExecutor) {
+ mGroupKey = null;
+ mLocusId = null;
+ mFlags = 0;
+ mUser = user;
+ mIcon = icon;
+ mIsAppBubble = true;
+ mKey = key;
+ mShowBubbleUpdateDot = false;
+ mMainExecutor = mainExecutor;
+ mBgExecutor = bgExecutor;
+ mTaskId = task.taskId;
+ mAppIntent = null;
+ mDesiredHeight = Integer.MAX_VALUE;
+ mPackageName = task.baseActivity.getPackageName();
+ }
+
+
/** Creates an app bubble. */
public static Bubble createAppBubble(Intent intent, UserHandle user, @Nullable Icon icon,
@ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) {
@@ -291,6 +324,16 @@ public class Bubble implements BubbleViewProvider {
mainExecutor, bgExecutor);
}
+ /** Creates a task bubble. */
+ public static Bubble createTaskBubble(TaskInfo info, UserHandle user, @Nullable Icon icon,
+ @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) {
+ return new Bubble(info,
+ user,
+ icon,
+ getAppBubbleKeyForTask(info),
+ mainExecutor, bgExecutor);
+ }
+
/** Creates a shortcut bubble. */
public static Bubble createShortcutBubble(
ShortcutInfo info,
@@ -316,6 +359,15 @@ public class Bubble implements BubbleViewProvider {
return info.getPackage() + ":" + info.getUserId() + ":" + info.getId();
}
+ /**
+ * Returns the key for an app bubble from an app with package name, {@code packageName} on an
+ * Android user, {@code user}.
+ */
+ public static String getAppBubbleKeyForTask(TaskInfo taskInfo) {
+ Objects.requireNonNull(taskInfo);
+ return KEY_APP_BUBBLE + ":" + taskInfo.taskId;
+ }
+
@VisibleForTesting(visibility = PRIVATE)
public Bubble(@NonNull final BubbleEntry entry,
final Bubbles.BubbleMetadataFlagListener listener,
@@ -469,6 +521,10 @@ public class Bubble implements BubbleViewProvider {
return mBubbleTaskView;
}
+ public TaskView getTaskView() {
+ return mBubbleTaskView.getTaskView();
+ }
+
/**
* @return the ShortcutInfo id if it exists, or the metadata shortcut id otherwise.
*/
@@ -486,6 +542,10 @@ public class Bubble implements BubbleViewProvider {
return (mMetadataShortcutId != null && !mMetadataShortcutId.isEmpty());
}
+ public BubbleTransitions.BubbleTransition getPreparingTransition() {
+ return mPreparingTransition;
+ }
+
/**
* Call this to clean up the task for the bubble. Ensure this is always called when done with
* the bubble.
@@ -512,7 +572,8 @@ public class Bubble implements BubbleViewProvider {
mIntentActive = false;
}
- private void cleanupTaskView() {
+ /** Cleans-up the taskview associated with this bubble (possibly removing the task from wm) */
+ public void cleanupTaskView() {
if (mBubbleTaskView != null) {
mBubbleTaskView.cleanup();
mBubbleTaskView = null;
@@ -533,7 +594,7 @@ public class Bubble implements BubbleViewProvider {
* <p>If we're switching between bar and floating modes, pass {@code false} on
* {@code cleanupTaskView} to avoid recreating it in the new mode.
*/
- void cleanupViews(boolean cleanupTaskView) {
+ public void cleanupViews(boolean cleanupTaskView) {
cleanupExpandedView(cleanupTaskView);
mIconView = null;
}
@@ -556,6 +617,13 @@ public class Bubble implements BubbleViewProvider {
}
/**
+ * Sets the current bubble-transition that is coordinating a change in this bubble.
+ */
+ void setPreparingTransition(BubbleTransitions.BubbleTransition transit) {
+ mPreparingTransition = transit;
+ }
+
+ /**
* Sets whether this bubble is considered text changed. This method is purely for
* testing.
*/
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 4f9028e8aaf3..5f2b95f7b137 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -40,9 +40,11 @@ import android.annotation.BinderThread;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
+import android.app.ActivityOptions;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.PendingIntent;
+import android.app.TaskInfo;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -78,6 +80,8 @@ import android.view.WindowInsets;
import android.view.WindowManager;
import android.window.ScreenCapture;
import android.window.ScreenCapture.SynchronousScreenCaptureListener;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
import androidx.annotation.MainThread;
import androidx.annotation.Nullable;
@@ -110,6 +114,7 @@ import com.android.wm.shell.onehanded.OneHandedController;
import com.android.wm.shell.onehanded.OneHandedTransitionCallback;
import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
import com.android.wm.shell.shared.annotations.ShellMainThread;
+import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper;
import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
import com.android.wm.shell.shared.bubbles.BubbleBarUpdate;
import com.android.wm.shell.sysui.ConfigurationChangeListener;
@@ -287,6 +292,8 @@ public class BubbleController implements ConfigurationChangeListener,
/** Used to send updates to the views from {@link #mBubbleDataListener}. */
private BubbleViewCallback mBubbleViewCallback;
+ private final BubbleTransitions mBubbleTransitions;
+
public BubbleController(Context context,
ShellInit shellInit,
ShellCommandHandler shellCommandHandler,
@@ -350,12 +357,16 @@ public class BubbleController implements ConfigurationChangeListener,
context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.importance_ring_stroke_width));
mDisplayController = displayController;
+ final TaskViewTransitions tvTransitions;
if (TaskViewTransitions.useRepo()) {
- mTaskViewController = new TaskViewTransitions(transitions, taskViewRepository,
- organizer, syncQueue);
+ tvTransitions = new TaskViewTransitions(transitions, taskViewRepository, organizer,
+ syncQueue);
} else {
- mTaskViewController = taskViewTransitions;
+ tvTransitions = taskViewTransitions;
}
+ mTaskViewController = new BubbleTaskViewController(tvTransitions);
+ mBubbleTransitions = new BubbleTransitions(transitions, organizer, taskViewRepository, data,
+ tvTransitions, context);
mTransitions = transitions;
mOneHandedOptional = oneHandedOptional;
mDragAndDropController = dragAndDropController;
@@ -1456,7 +1467,19 @@ public class BubbleController implements ConfigurationChangeListener,
* @param taskInfo the task.
*/
public void expandStackAndSelectBubble(ActivityManager.RunningTaskInfo taskInfo) {
- // TODO(384976265): Not implemented yet
+ if (!BubbleAnythingFlagHelper.enableBubbleToFullscreen()) return;
+ Bubble b = mBubbleData.getOrCreateBubble(taskInfo); // Removes from overflow
+ ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - intent=%s", taskInfo.taskId);
+ if (b.isInflated()) {
+ mBubbleData.setSelectedBubbleAndExpandStack(b);
+ } else {
+ b.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE);
+ // Lazy init stack view when a bubble is created
+ ensureBubbleViewsAndWindowCreated();
+ mBubbleTransitions.startConvertToBubble(b, taskInfo, mExpandedViewManager,
+ mBubbleTaskViewFactory, mBubblePositioner, mLogger, mStackView, mLayerView,
+ mBubbleIconFactory, mInflateSynchronously);
+ }
}
/**
@@ -2057,7 +2080,12 @@ public class BubbleController implements ConfigurationChangeListener,
@Override
public void removeBubble(Bubble removedBubble) {
if (mLayerView != null) {
+ final BubbleTransitions.BubbleTransition bubbleTransit =
+ removedBubble.getPreparingTransition();
mLayerView.removeBubble(removedBubble, () -> {
+ if (bubbleTransit != null) {
+ bubbleTransit.continueCollapse();
+ }
if (!mBubbleData.hasBubbles() && !isStackExpanded()) {
mLayerView.setVisibility(INVISIBLE);
removeFromWindowManagerMaybe();
@@ -2261,9 +2289,16 @@ public class BubbleController implements ConfigurationChangeListener,
private void showExpandedViewForBubbleBar() {
BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble();
- if (selectedBubble != null && mLayerView != null) {
- mLayerView.showExpandedView(selectedBubble);
+ if (selectedBubble == null) return;
+ if (selectedBubble instanceof Bubble) {
+ final Bubble bubble = (Bubble) selectedBubble;
+ if (bubble.getPreparingTransition() != null) {
+ bubble.getPreparingTransition().continueExpand();
+ return;
+ }
}
+ if (mLayerView == null) return;
+ mLayerView.showExpandedView(selectedBubble);
}
private void collapseExpandedViewForBubbleBar() {
@@ -2613,6 +2648,17 @@ public class BubbleController implements ConfigurationChangeListener,
public void animateBubbleBarLocation(BubbleBarLocation location) {
mListener.call(l -> l.animateBubbleBarLocation(location));
}
+
+ @Override
+ public void onDragItemOverBubbleBarDragZone(
+ @NonNull BubbleBarLocation location) {
+ mListener.call(l -> l.onDragItemOverBubbleBarDragZone(location));
+ }
+
+ @Override
+ public void onItemDraggedOutsideBubbleBarDropZone() {
+ mListener.call(IBubblesListener::onItemDraggedOutsideBubbleBarDropZone);
+ }
};
IBubblesImpl(BubbleController controller) {
@@ -2665,7 +2711,18 @@ public class BubbleController implements ConfigurationChangeListener,
@Override
public void collapseBubbles() {
- mMainExecutor.execute(() -> mController.collapseStack());
+ mMainExecutor.execute(() -> {
+ if (mBubbleData.getSelectedBubble() instanceof Bubble) {
+ if (((Bubble) mBubbleData.getSelectedBubble()).getPreparingTransition()
+ != null) {
+ // Currently preparing a transition which will, itself, collapse the bubble.
+ // For transition preparation, the timing of bubble-collapse must be in
+ // sync with the rest of the set-up.
+ return;
+ }
+ }
+ mController.collapseStack();
+ });
}
@Override
@@ -3057,4 +3114,84 @@ public class BubbleController implements ConfigurationChangeListener,
return mKeyToShownInShadeMap.get(key);
}
}
+
+ private class BubbleTaskViewController implements TaskViewController {
+ private final TaskViewTransitions mBaseTransitions;
+
+ BubbleTaskViewController(TaskViewTransitions baseTransitions) {
+ mBaseTransitions = baseTransitions;
+ }
+
+ @Override
+ public void registerTaskView(TaskViewTaskController tv) {
+ mBaseTransitions.registerTaskView(tv);
+ }
+
+ @Override
+ public void unregisterTaskView(TaskViewTaskController tv) {
+ mBaseTransitions.unregisterTaskView(tv);
+ }
+
+ @Override
+ public void startShortcutActivity(@NonNull TaskViewTaskController destination,
+ @NonNull ShortcutInfo shortcut, @NonNull ActivityOptions options,
+ @Nullable Rect launchBounds) {
+ mBaseTransitions.startShortcutActivity(destination, shortcut, options, launchBounds);
+ }
+
+ @Override
+ public void startActivity(@NonNull TaskViewTaskController destination,
+ @NonNull PendingIntent pendingIntent, @Nullable Intent fillInIntent,
+ @NonNull ActivityOptions options, @Nullable Rect launchBounds) {
+ mBaseTransitions.startActivity(destination, pendingIntent, fillInIntent,
+ options, launchBounds);
+ }
+
+ @Override
+ public void startRootTask(@NonNull TaskViewTaskController destination,
+ ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash,
+ @Nullable WindowContainerTransaction wct) {
+ mBaseTransitions.startRootTask(destination, taskInfo, leash, wct);
+ }
+
+ @Override
+ public void removeTaskView(@NonNull TaskViewTaskController taskView,
+ @Nullable WindowContainerToken taskToken) {
+ mBaseTransitions.removeTaskView(taskView, taskToken);
+ }
+
+ @Override
+ public void moveTaskViewToFullscreen(@NonNull TaskViewTaskController taskView) {
+ final TaskInfo tinfo = taskView.getTaskInfo();
+ if (tinfo == null) {
+ return;
+ }
+ Bubble bub = null;
+ for (Bubble b : mBubbleData.getBubbles()) {
+ if (b.getTaskId() == tinfo.taskId) {
+ bub = b;
+ break;
+ }
+ }
+ if (bub == null) {
+ return;
+ }
+ mBubbleTransitions.startConvertFromBubble(bub, tinfo);
+ }
+
+ @Override
+ public void setTaskViewVisible(TaskViewTaskController taskView, boolean visible) {
+ mBaseTransitions.setTaskViewVisible(taskView, visible);
+ }
+
+ @Override
+ public void setTaskBounds(TaskViewTaskController taskView, Rect boundsOnScreen) {
+ mBaseTransitions.setTaskBounds(taskView, boundsOnScreen);
+ }
+
+ @Override
+ public boolean isUsingShellTransitions() {
+ return mBaseTransitions.isUsingShellTransitions();
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
index 74302094a296..96d0f6d5654e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
@@ -22,6 +22,7 @@ import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES;
import android.annotation.NonNull;
import android.app.PendingIntent;
+import android.app.TaskInfo;
import android.content.Context;
import android.content.Intent;
import android.content.LocusId;
@@ -470,6 +471,17 @@ public class BubbleData {
return bubbleToReturn;
}
+ Bubble getOrCreateBubble(TaskInfo taskInfo) {
+ UserHandle user = UserHandle.of(mCurrentUserId);
+ String bubbleKey = Bubble.getAppBubbleKeyForTask(taskInfo);
+ Bubble bubbleToReturn = findAndRemoveBubbleFromOverflow(bubbleKey);
+ if (bubbleToReturn == null) {
+ bubbleToReturn = Bubble.createTaskBubble(taskInfo, user, null, mMainExecutor,
+ mBgExecutor);
+ }
+ return bubbleToReturn;
+ }
+
@Nullable
private Bubble findAndRemoveBubbleFromOverflow(String key) {
Bubble bubbleToReturn = getBubbleInStackWithKey(key);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt
index 62995319db80..086c91985ae3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt
@@ -137,14 +137,15 @@ class BubbleOverflow(private val context: Context, private val positioner: Bubbl
// Update bitmap
val fg = InsetDrawable(overflowBtn?.iconDrawable, overflowIconInset)
val drawable = AdaptiveIconDrawable(ColorDrawable(colorAccent), fg)
- bitmap = iconFactory.createBadgedIconBitmap(drawable).icon
+ val bubbleBitmapScale = FloatArray(1)
+ bitmap = iconFactory.getBubbleBitmap(drawable, bubbleBitmapScale)
// Update dot path
dotPath =
PathParser.createPathFromPathData(
res.getString(com.android.internal.R.string.config_icon_mask)
)
- val scale = iconFactory.normalizer.getScale(iconView!!.iconDrawable)
+ val scale = bubbleBitmapScale[0]
val radius = BadgedImageView.DEFAULT_PATH_SIZE / 2f
val matrix = Matrix()
matrix.setScale(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
index 1a61793eab87..a725e04d3f8a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
@@ -87,6 +87,7 @@ public class BubblePositioner {
private int mExpandedViewLargeScreenWidth;
private int mExpandedViewLargeScreenInsetClosestEdge;
private int mExpandedViewLargeScreenInsetFurthestEdge;
+ private int mExpandedViewBubbleBarWidth;
private int mOverflowWidth;
private int mExpandedViewPadding;
@@ -158,12 +159,13 @@ public class BubblePositioner {
mBubbleOffscreenAmount = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen);
mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
+ mExpandedViewBubbleBarWidth = Math.min(
+ res.getDimensionPixelSize(R.dimen.bubble_bar_expanded_view_width),
+ mPositionRect.width() - 2 * mExpandedViewPadding
+ );
if (mShowingInBubbleBar) {
- mExpandedViewLargeScreenWidth = Math.min(
- res.getDimensionPixelSize(R.dimen.bubble_bar_expanded_view_width),
- mPositionRect.width() - 2 * mExpandedViewPadding
- );
+ mExpandedViewLargeScreenWidth = mExpandedViewBubbleBarWidth;
} else if (mDeviceConfig.isSmallTablet()) {
mExpandedViewLargeScreenWidth = (int) (bounds.width()
* EXPANDED_VIEW_SMALL_TABLET_WIDTH_PERCENT);
@@ -888,7 +890,7 @@ public class BubblePositioner {
* How wide the expanded view should be when showing from the bubble bar.
*/
public int getExpandedViewWidthForBubbleBar(boolean isOverflow) {
- return isOverflow ? mOverflowWidth : mExpandedViewLargeScreenWidth;
+ return isOverflow ? mOverflowWidth : mExpandedViewBubbleBarWidth;
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index 4e7f87c48a86..f1f49eda75b6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -633,8 +633,6 @@ public class BubbleStackView extends FrameLayout
mMagneticTarget,
mIndividualBubbleMagnetListener);
- hideCurrentInputMethod();
-
// Save the magnetized individual bubble so we can dispatch touch events to it.
mMagnetizedObject = mExpandedAnimationController.getMagnetizedBubbleDraggingOut();
} else {
@@ -671,6 +669,10 @@ public class BubbleStackView extends FrameLayout
return;
}
+ if (mPositioner.isImeVisible()) {
+ hideCurrentInputMethod();
+ }
+
// Show the dismiss target, if we haven't already.
mDismissView.show();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
index 89c038b4a26b..ae84f449c0e4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
@@ -109,7 +109,9 @@ public class BubbleTaskViewHelper {
MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
final boolean isShortcutBubble = (mBubble.hasMetadataShortcutId()
|| (mBubble.getShortcutInfo() != null && Flags.enableBubbleAnything()));
- if (mBubble.isAppBubble()) {
+ if (mBubble.getPreparingTransition() != null) {
+ mBubble.getPreparingTransition().surfaceCreated();
+ } else if (mBubble.isAppBubble()) {
Context context =
mContext.createContextAsUser(
mBubble.getUser(), Context.CONTEXT_RESTRICTED);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java
new file mode 100644
index 000000000000..29fb1a23017c
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java
@@ -0,0 +1,518 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.bubbles;
+
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.view.View.INVISIBLE;
+import static android.view.WindowManager.TRANSIT_CHANGE;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.TaskInfo;
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.util.Slog;
+import android.view.SurfaceControl;
+import android.view.SurfaceView;
+import android.view.View;
+import android.window.TransitionInfo;
+import android.window.TransitionRequestInfo;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.launcher3.icons.BubbleIconFactory;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView;
+import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
+import com.android.wm.shell.taskview.TaskView;
+import com.android.wm.shell.taskview.TaskViewRepository;
+import com.android.wm.shell.taskview.TaskViewTaskController;
+import com.android.wm.shell.taskview.TaskViewTransitions;
+import com.android.wm.shell.transition.Transitions;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Implements transition coordination for bubble operations.
+ */
+public class BubbleTransitions {
+ private static final String TAG = "BubbleTransitions";
+
+ /**
+ * Multiplier used to convert a view elevation to an "equivalent" shadow-radius. This is the
+ * same multiple used by skia and surface-outsets in WMS.
+ */
+ private static final float ELEVATION_TO_RADIUS = 2;
+
+ @NonNull final Transitions mTransitions;
+ @NonNull final ShellTaskOrganizer mTaskOrganizer;
+ @NonNull final TaskViewRepository mRepository;
+ @NonNull final Executor mMainExecutor;
+ @NonNull final BubbleData mBubbleData;
+ @NonNull final TaskViewTransitions mTaskViewTransitions;
+ @NonNull final Context mContext;
+
+ BubbleTransitions(@NonNull Transitions transitions, @NonNull ShellTaskOrganizer organizer,
+ @NonNull TaskViewRepository repository, @NonNull BubbleData bubbleData,
+ @NonNull TaskViewTransitions taskViewTransitions, Context context) {
+ mTransitions = transitions;
+ mTaskOrganizer = organizer;
+ mRepository = repository;
+ mMainExecutor = transitions.getMainExecutor();
+ mBubbleData = bubbleData;
+ mTaskViewTransitions = taskViewTransitions;
+ mContext = context;
+ }
+
+ /**
+ * Starts a convert-to-bubble transition.
+ *
+ * @see ConvertToBubble
+ */
+ public BubbleTransition startConvertToBubble(Bubble bubble, TaskInfo taskInfo,
+ BubbleExpandedViewManager expandedViewManager, BubbleTaskViewFactory factory,
+ BubblePositioner positioner, BubbleLogger logger, BubbleStackView stackView,
+ BubbleBarLayerView layerView, BubbleIconFactory iconFactory,
+ boolean inflateSync) {
+ ConvertToBubble convert = new ConvertToBubble(bubble, taskInfo, mContext,
+ expandedViewManager, factory, positioner, logger, stackView, layerView, iconFactory,
+ inflateSync);
+ return convert;
+ }
+
+ /**
+ * Starts a convert-from-bubble transition.
+ *
+ * @see ConvertFromBubble
+ */
+ public BubbleTransition startConvertFromBubble(Bubble bubble,
+ TaskInfo taskInfo) {
+ ConvertFromBubble convert = new ConvertFromBubble(bubble, taskInfo);
+ return convert;
+ }
+
+ /**
+ * Plucks the task-surface out of an ancestor view while making the view invisible. This helper
+ * attempts to do this seamlessly (ie. view becomes invisible in sync with task reparent).
+ */
+ private void pluck(SurfaceControl taskLeash, View fromView, SurfaceControl dest,
+ float destX, float destY, float cornerRadius, SurfaceControl.Transaction t,
+ Runnable onPlucked) {
+ SurfaceControl.Transaction pluckT = new SurfaceControl.Transaction();
+ pluckT.reparent(taskLeash, dest);
+ t.reparent(taskLeash, dest);
+ pluckT.setPosition(taskLeash, destX, destY);
+ t.setPosition(taskLeash, destX, destY);
+ pluckT.show(taskLeash);
+ pluckT.setAlpha(taskLeash, 1.f);
+ float shadowRadius = fromView.getElevation() * ELEVATION_TO_RADIUS;
+ pluckT.setShadowRadius(taskLeash, shadowRadius);
+ pluckT.setCornerRadius(taskLeash, cornerRadius);
+ t.setShadowRadius(taskLeash, shadowRadius);
+ t.setCornerRadius(taskLeash, cornerRadius);
+
+ // Need to remove the taskview AFTER applying the startTransaction because it isn't
+ // synchronized.
+ pluckT.addTransactionCommittedListener(mMainExecutor, onPlucked::run);
+ fromView.getViewRootImpl().applyTransactionOnDraw(pluckT);
+ fromView.setVisibility(INVISIBLE);
+ }
+
+ /**
+ * Interface to a bubble-specific transition. Bubble transitions have a multi-step lifecycle
+ * in order to coordinate with the bubble view logic. These steps are communicated on this
+ * interface.
+ */
+ interface BubbleTransition {
+ default void surfaceCreated() {}
+ default void continueExpand() {}
+ void skip();
+ default void continueCollapse() {}
+ }
+
+ /**
+ * BubbleTransition that coordinates the process of a non-bubble task becoming a bubble. The
+ * steps are as follows:
+ *
+ * 1. Start inflating the bubble view
+ * 2. Once inflated (but not-yet visible), tell WM to do the shell-transition.
+ * 3. Transition becomes ready, so notify Launcher
+ * 4. Launcher responds with showExpandedView which calls continueExpand() to make view visible
+ * 5. Surface is created which kicks off actual animation
+ *
+ * So, constructor -> onInflated -> startAnimation -> continueExpand -> surfaceCreated.
+ *
+ * continueExpand and surfaceCreated are set-up to happen in either order, though, to support
+ * UX/timing adjustments.
+ */
+ @VisibleForTesting
+ class ConvertToBubble implements Transitions.TransitionHandler, BubbleTransition {
+ final BubbleBarLayerView mLayerView;
+ Bubble mBubble;
+ IBinder mTransition;
+ Transitions.TransitionFinishCallback mFinishCb;
+ WindowContainerTransaction mFinishWct = null;
+ final Rect mStartBounds = new Rect();
+ SurfaceControl mSnapshot = null;
+ TaskInfo mTaskInfo;
+ boolean mFinishedExpand = false;
+ BubbleViewProvider mPriorBubble = null;
+
+ private SurfaceControl.Transaction mFinishT;
+ private SurfaceControl mTaskLeash;
+
+ ConvertToBubble(Bubble bubble, TaskInfo taskInfo, Context context,
+ BubbleExpandedViewManager expandedViewManager, BubbleTaskViewFactory factory,
+ BubblePositioner positioner, BubbleLogger logger, BubbleStackView stackView,
+ BubbleBarLayerView layerView, BubbleIconFactory iconFactory, boolean inflateSync) {
+ mBubble = bubble;
+ mTaskInfo = taskInfo;
+ mLayerView = layerView;
+ mBubble.setInflateSynchronously(inflateSync);
+ mBubble.setPreparingTransition(this);
+ mBubble.inflate(
+ this::onInflated,
+ context,
+ expandedViewManager,
+ factory,
+ positioner,
+ logger,
+ stackView,
+ layerView,
+ iconFactory,
+ false /* skipInflation */);
+ }
+
+ @VisibleForTesting
+ void onInflated(Bubble b) {
+ if (b != mBubble) {
+ throw new IllegalArgumentException("inflate callback doesn't match bubble");
+ }
+ final Rect launchBounds = new Rect();
+ mLayerView.getExpandedViewRestBounds(launchBounds);
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) {
+ if (mTaskInfo.getParentTaskId() != INVALID_TASK_ID) {
+ wct.reparent(mTaskInfo.token, null, true);
+ }
+ }
+
+ wct.setAlwaysOnTop(mTaskInfo.token, true);
+ wct.setWindowingMode(mTaskInfo.token, WINDOWING_MODE_MULTI_WINDOW);
+ wct.setBounds(mTaskInfo.token, launchBounds);
+
+ final TaskView tv = b.getTaskView();
+ tv.setSurfaceLifecycle(SurfaceView.SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT);
+ final TaskViewRepository.TaskViewState state = mRepository.byTaskView(
+ tv.getController());
+ if (state != null) {
+ state.mVisible = true;
+ }
+ mTaskViewTransitions.enqueueExternal(tv.getController(), () -> {
+ mTransition = mTransitions.startTransition(TRANSIT_CHANGE, wct, this);
+ return mTransition;
+ });
+ }
+
+ @Override
+ public void skip() {
+ mBubble.setPreparingTransition(null);
+ mFinishCb.onTransitionFinished(mFinishWct);
+ mFinishCb = null;
+ }
+
+ @Override
+ public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
+ @Nullable TransitionRequestInfo request) {
+ return null;
+ }
+
+ @Override
+ public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ }
+
+ @Override
+ public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted,
+ @NonNull SurfaceControl.Transaction finishTransaction) {
+ if (!aborted) return;
+ mTransition = null;
+ mTaskViewTransitions.onExternalDone(transition);
+ }
+
+ @Override
+ public boolean startAnimation(@NonNull IBinder transition,
+ @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ if (mTransition != transition) return false;
+ boolean found = false;
+ for (int i = 0; i < info.getChanges().size(); ++i) {
+ final TransitionInfo.Change chg = info.getChanges().get(i);
+ if (chg.getTaskInfo() == null) continue;
+ if (chg.getMode() != TRANSIT_CHANGE) continue;
+ if (!mTaskInfo.token.equals(chg.getTaskInfo().token)) continue;
+ mStartBounds.set(chg.getStartAbsBounds());
+ // Converting a task into taskview, so treat as "new"
+ mFinishWct = new WindowContainerTransaction();
+ mTaskInfo = chg.getTaskInfo();
+ mFinishT = finishTransaction;
+ mTaskLeash = chg.getLeash();
+ found = true;
+ mSnapshot = chg.getSnapshot();
+ break;
+ }
+ if (!found) {
+ Slog.w(TAG, "Expected a TaskView conversion in this transition but didn't get "
+ + "one, cleaning up the task view");
+ mBubble.getTaskView().getController().setTaskNotFound();
+ mTaskViewTransitions.onExternalDone(transition);
+ return false;
+ }
+ mFinishCb = finishCallback;
+
+ // Now update state (and talk to launcher) in parallel with snapshot stuff
+ mBubbleData.notificationEntryUpdated(mBubble, /* suppressFlyout= */ true,
+ /* showInShade= */ false);
+
+ startTransaction.show(mSnapshot);
+ // Move snapshot to root so that it remains visible while task is moved to taskview
+ startTransaction.reparent(mSnapshot, info.getRoot(0).getLeash());
+ startTransaction.setPosition(mSnapshot,
+ mStartBounds.left - info.getRoot(0).getOffset().x,
+ mStartBounds.top - info.getRoot(0).getOffset().y);
+ startTransaction.setLayer(mSnapshot, Integer.MAX_VALUE);
+ startTransaction.apply();
+
+ mTaskViewTransitions.onExternalDone(transition);
+ return true;
+ }
+
+ @Override
+ public void continueExpand() {
+ mFinishedExpand = true;
+ final boolean animate = mLayerView.canExpandView(mBubble);
+ if (animate) {
+ mPriorBubble = mLayerView.prepareConvertedView(mBubble);
+ }
+ if (mPriorBubble != null) {
+ // TODO: an animation. For now though, just remove it.
+ final BubbleBarExpandedView priorView = mPriorBubble.getBubbleBarExpandedView();
+ mLayerView.removeView(priorView);
+ mPriorBubble = null;
+ }
+ if (!animate || mBubble.getTaskView().getSurfaceControl() != null) {
+ playAnimation(animate);
+ }
+ }
+
+ @Override
+ public void surfaceCreated() {
+ mMainExecutor.execute(() -> {
+ final TaskViewTaskController tvc = mBubble.getTaskView().getController();
+ final TaskViewRepository.TaskViewState state = mRepository.byTaskView(tvc);
+ if (state == null) return;
+ state.mVisible = true;
+ if (mFinishedExpand) {
+ playAnimation(true /* animate */);
+ }
+ });
+ }
+
+ private void playAnimation(boolean animate) {
+ final TaskViewTaskController tv = mBubble.getTaskView().getController();
+ final SurfaceControl.Transaction startT = new SurfaceControl.Transaction();
+ mTaskViewTransitions.prepareOpenAnimation(tv, true /* new */, startT, mFinishT,
+ (ActivityManager.RunningTaskInfo) mTaskInfo, mTaskLeash, mFinishWct);
+
+ if (mFinishWct.isEmpty()) {
+ mFinishWct = null;
+ }
+
+ // Preparation is complete.
+ mBubble.setPreparingTransition(null);
+
+ if (animate) {
+ mLayerView.animateConvert(startT, mStartBounds, mSnapshot, mTaskLeash, () -> {
+ mFinishCb.onTransitionFinished(mFinishWct);
+ mFinishCb = null;
+ });
+ } else {
+ startT.apply();
+ mFinishCb.onTransitionFinished(mFinishWct);
+ mFinishCb = null;
+ }
+ }
+ }
+
+ /**
+ * BubbleTransition that coordinates the setup for moving a task out of a bubble. The actual
+ * animation is owned by the "receiver" of the task; however, because Bubbles uses TaskView,
+ * we need to do some extra coordination work to get the task surface out of the view
+ * "seamlessly".
+ *
+ * The process here looks like:
+ * 1. Send transition to WM for leaving bubbles mode
+ * 2. in startAnimation, set-up a "pluck" operation to pull the task surface out of taskview
+ * 3. Once "plucked", remove the view (calls continueCollapse when surfaces can be cleaned-up)
+ * 4. Then re-dispatch the transition animation so that the "receiver" can animate it.
+ *
+ * So, constructor -> startAnimation -> continueCollapse -> re-dispatch.
+ */
+ @VisibleForTesting
+ class ConvertFromBubble implements Transitions.TransitionHandler, BubbleTransition {
+ @NonNull final Bubble mBubble;
+ IBinder mTransition;
+ TaskInfo mTaskInfo;
+ SurfaceControl mTaskLeash;
+ SurfaceControl mRootLeash;
+
+ ConvertFromBubble(@NonNull Bubble bubble, TaskInfo taskInfo) {
+ mBubble = bubble;
+ mTaskInfo = taskInfo;
+
+ mBubble.setPreparingTransition(this);
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ WindowContainerToken token = mTaskInfo.getToken();
+ wct.setWindowingMode(token, WINDOWING_MODE_UNDEFINED);
+ wct.setAlwaysOnTop(token, false);
+ mTaskOrganizer.setInterceptBackPressedOnTaskRoot(token, false);
+ mTaskViewTransitions.enqueueExternal(
+ mBubble.getTaskView().getController(),
+ () -> {
+ mTransition = mTransitions.startTransition(TRANSIT_CHANGE, wct, this);
+ return mTransition;
+ });
+ }
+
+ @Override
+ public void skip() {
+ mBubble.setPreparingTransition(null);
+ final TaskViewTaskController tv =
+ mBubble.getTaskView().getController();
+ tv.notifyTaskRemovalStarted(tv.getTaskInfo());
+ mTaskLeash = null;
+ }
+
+ @Override
+ public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
+ @android.annotation.Nullable TransitionRequestInfo request) {
+ return null;
+ }
+
+ @Override
+ public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ }
+
+ @Override
+ public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted,
+ @NonNull SurfaceControl.Transaction finishTransaction) {
+ if (!aborted) return;
+ mTransition = null;
+ skip();
+ mTaskViewTransitions.onExternalDone(transition);
+ }
+
+ @Override
+ public boolean startAnimation(@NonNull IBinder transition,
+ @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ if (mTransition != transition) return false;
+
+ final TaskViewTaskController tv =
+ mBubble.getTaskView().getController();
+ if (tv == null) {
+ mTaskViewTransitions.onExternalDone(transition);
+ return false;
+ }
+
+ TransitionInfo.Change taskChg = null;
+
+ boolean found = false;
+ for (int i = 0; i < info.getChanges().size(); ++i) {
+ final TransitionInfo.Change chg = info.getChanges().get(i);
+ if (chg.getTaskInfo() == null) continue;
+ if (chg.getMode() != TRANSIT_CHANGE) continue;
+ if (!mTaskInfo.token.equals(chg.getTaskInfo().token)) continue;
+ found = true;
+ mRepository.remove(tv);
+ taskChg = chg;
+ break;
+ }
+
+ if (!found) {
+ Slog.w(TAG, "Expected a TaskView conversion in this transition but didn't get "
+ + "one, cleaning up the task view");
+ tv.setTaskNotFound();
+ skip();
+ mTaskViewTransitions.onExternalDone(transition);
+ return false;
+ }
+
+ mTaskLeash = taskChg.getLeash();
+ mRootLeash = info.getRoot(0).getLeash();
+
+ SurfaceControl dest =
+ mBubble.getBubbleBarExpandedView().getViewRootImpl().getSurfaceControl();
+ final Runnable onPlucked = () -> {
+ // Need to remove the taskview AFTER applying the startTransaction because
+ // it isn't synchronized.
+ tv.notifyTaskRemovalStarted(tv.getTaskInfo());
+ // Unset after removeView so it can be used to pick a different animation.
+ mBubble.setPreparingTransition(null);
+ mBubbleData.setExpanded(false /* expanded */);
+ };
+ if (dest != null) {
+ pluck(mTaskLeash, mBubble.getBubbleBarExpandedView(), dest,
+ taskChg.getStartAbsBounds().left - info.getRoot(0).getOffset().x,
+ taskChg.getStartAbsBounds().top - info.getRoot(0).getOffset().y,
+ mBubble.getBubbleBarExpandedView().getCornerRadius(), startTransaction,
+ onPlucked);
+ mBubble.getBubbleBarExpandedView().post(() -> mTransitions.dispatchTransition(
+ mTransition, info, startTransaction, finishTransaction, finishCallback,
+ null));
+ } else {
+ onPlucked.run();
+ mTransitions.dispatchTransition(mTransition, info, startTransaction,
+ finishTransaction, finishCallback, null);
+ }
+
+ mTaskViewTransitions.onExternalDone(transition);
+ return true;
+ }
+
+ @Override
+ public void continueCollapse() {
+ mBubble.cleanupTaskView();
+ if (mTaskLeash == null) return;
+ SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ t.reparent(mTaskLeash, mRootLeash);
+ t.apply();
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
index 62895fe7c7cc..4297fac0f6a8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
@@ -36,6 +36,7 @@ import android.window.ScreenCapture.ScreenshotHardwareBuffer;
import android.window.ScreenCapture.SynchronousScreenCaptureListener;
import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.wm.shell.shared.annotations.ExternalThread;
@@ -330,6 +331,18 @@ public interface Bubbles {
* Does not result in a state change.
*/
void animateBubbleBarLocation(BubbleBarLocation location);
+
+ /**
+ * Called when an application icon is being dragged over the Bubble Bar drop zone.
+ * The location of the Bubble Bar is provided as an argument.
+ */
+ void onDragItemOverBubbleBarDragZone(@NonNull BubbleBarLocation location);
+
+ /**
+ * Called when an application icon is being dragged outside the Bubble Bar drop zone.
+ * Always called after {@link #onDragItemOverBubbleBarDragZone(BubbleBarLocation)}
+ */
+ void onItemDraggedOutsideBubbleBarDropZone();
}
/** Listener to find out about stack expansion / collapse events. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl
index eb907dbb6597..9fc769f562a9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl
@@ -33,4 +33,16 @@ oneway interface IBubblesListener {
* Does not result in a state change.
*/
void animateBubbleBarLocation(in BubbleBarLocation location);
+
+ /**
+ * Called when an application icon is being dragged over the Bubble Bar drop zone.
+ * The location of the Bubble Bar is provided as an argument.
+ */
+ void onDragItemOverBubbleBarDragZone(in BubbleBarLocation location);
+
+ /**
+ * Called when an application icon is being dragged outside the Bubble Bar drop zone.
+ * Always called after {@link #onDragItemOverBubbleBarDragZone(BubbleBarLocation)}
+ */
+ void onItemDraggedOutsideBubbleBarDropZone();
} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
index de6d1f6c8852..52f20646fb4a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
@@ -36,17 +36,21 @@ import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
+import android.annotation.NonNull;
import android.content.Context;
import android.graphics.Point;
import android.graphics.Rect;
import android.util.Log;
import android.util.Size;
+import android.view.SurfaceControl;
import android.widget.FrameLayout;
import androidx.annotation.Nullable;
import com.android.app.animation.Interpolators;
import com.android.wm.shell.R;
+import com.android.wm.shell.animation.SizeChangeAnimation;
+import com.android.wm.shell.bubbles.Bubble;
import com.android.wm.shell.bubbles.BubbleOverflow;
import com.android.wm.shell.bubbles.BubblePositioner;
import com.android.wm.shell.bubbles.BubbleViewProvider;
@@ -571,6 +575,49 @@ public class BubbleBarAnimationHelper {
}
/**
+ * Animates converting of a non-bubble task into an expanded bubble view.
+ */
+ public void animateConvert(BubbleViewProvider expandedBubble,
+ @NonNull SurfaceControl.Transaction startT,
+ @NonNull Rect origBounds,
+ @NonNull SurfaceControl snapshot,
+ @NonNull SurfaceControl taskLeash,
+ @Nullable Runnable afterAnimation) {
+ mExpandedBubble = expandedBubble;
+ final BubbleBarExpandedView bbev = getExpandedView();
+ if (bbev == null) {
+ return;
+ }
+
+ bbev.setTaskViewAlpha(1f);
+ SurfaceControl tvSf = ((Bubble) mExpandedBubble).getTaskView().getSurfaceControl();
+
+ final Size size = getExpandedViewSize();
+ Point position = getExpandedViewRestPosition(size);
+
+ final SizeChangeAnimation sca =
+ new SizeChangeAnimation(
+ new Rect(origBounds.left - position.x, origBounds.top - position.y,
+ origBounds.right - position.x, origBounds.bottom - position.y),
+ new Rect(0, 0, size.getWidth(), size.getHeight()));
+ sca.initialize(bbev, taskLeash, snapshot, startT);
+
+ Animator a = sca.buildViewAnimator(bbev, tvSf, snapshot, /* onFinish */ (va) -> {
+ updateExpandedView(bbev);
+ snapshot.release();
+ bbev.setSurfaceZOrderedOnTop(false);
+ bbev.setAnimating(false);
+ if (afterAnimation != null) {
+ afterAnimation.run();
+ }
+ });
+
+ bbev.setSurfaceZOrderedOnTop(true);
+ a.setDuration(EXPANDED_VIEW_ANIMATE_TO_REST_DURATION);
+ a.start();
+ }
+
+ /**
* Cancel current animations
*/
public void cancelAnimations() {
@@ -627,6 +674,13 @@ public class BubbleBarAnimationHelper {
bbev.maybeShowOverflow();
}
+ void getExpandedViewRestBounds(Rect out) {
+ final int width = mPositioner.getExpandedViewWidthForBubbleBar(false /* overflow */);
+ final int height = mPositioner.getExpandedViewHeightForBubbleBar(false /* overflow */);
+ Point position = getExpandedViewRestPosition(new Size(width, height));
+ out.set(position.x, position.y, position.x + width, position.y + height);
+ }
+
private Point getExpandedViewRestPosition(Size size) {
final int padding = mPositioner.getBubbleBarExpandedViewPadding();
Point point = new Point();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
index eaa0bd250fc4..f3f8d6f96a42 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
@@ -28,6 +28,7 @@ import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.drawable.ColorDrawable;
import android.view.Gravity;
+import android.view.SurfaceControl;
import android.view.TouchDelegate;
import android.view.View;
import android.view.ViewTreeObserver;
@@ -174,14 +175,34 @@ public class BubbleBarLayerView extends FrameLayout
/** Shows the expanded view of the provided bubble. */
public void showExpandedView(BubbleViewProvider b) {
- BubbleBarExpandedView expandedView = b.getBubbleBarExpandedView();
- if (expandedView == null) {
- return;
- }
+ if (!canExpandView(b)) return;
+ animateExpand(prepareExpandedView(b));
+ }
+
+ /**
+ * @return whether it's possible to expand {@param b} right now. This is {@code false} if
+ * the bubble has no view or if the bubble is already showing.
+ */
+ public boolean canExpandView(BubbleViewProvider b) {
+ if (b.getBubbleBarExpandedView() == null) return false;
if (mExpandedBubble != null && mIsExpanded && b.getKey().equals(mExpandedBubble.getKey())) {
- // Already showing this bubble, skip animating
- return;
+ // Already showing this bubble so can't expand it.
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Prepares the expanded view of the provided bubble to be shown. This includes removing any
+ * stale content and cancelling any related animations.
+ *
+ * @return previous open bubble if there was one.
+ */
+ private BubbleViewProvider prepareExpandedView(BubbleViewProvider b) {
+ if (!canExpandView(b)) {
+ throw new IllegalStateException("Can't prepare expand. Check canExpandView(b) first.");
}
+ BubbleBarExpandedView expandedView = b.getBubbleBarExpandedView();
BubbleViewProvider previousBubble = null;
if (mExpandedBubble != null && !b.getKey().equals(mExpandedBubble.getKey())) {
if (mIsExpanded && mExpandedBubble.getBubbleBarExpandedView() != null) {
@@ -251,7 +272,20 @@ public class BubbleBarLayerView extends FrameLayout
mIsExpanded = true;
mBubbleController.getSysuiProxy().onStackExpandChanged(true);
+ showScrim(true);
+ return previousBubble;
+ }
+ /**
+ * Performs an animation to open a bubble with content that is not already visible.
+ *
+ * @param previousBubble If non-null, this is a bubble that is already showing before the new
+ * bubble is expanded.
+ */
+ public void animateExpand(BubbleViewProvider previousBubble) {
+ if (!mIsExpanded || mExpandedBubble == null) {
+ throw new IllegalStateException("Can't animateExpand without expnaded state");
+ }
final Runnable afterAnimation = () -> {
if (mExpandedView == null) return;
// Touch delegate for the menu
@@ -274,14 +308,57 @@ public class BubbleBarLayerView extends FrameLayout
} else {
mAnimationHelper.animateExpansion(mExpandedBubble, afterAnimation);
}
+ }
- showScrim(true);
+ /**
+ * Like {@link #prepareExpandedView} but also makes the current expanded bubble visible
+ * immediately so it gets a surface that can be animated. Since the surface may not be ready
+ * yet, this keeps the TaskView alpha=0.
+ */
+ public BubbleViewProvider prepareConvertedView(BubbleViewProvider b) {
+ final BubbleViewProvider prior = prepareExpandedView(b);
+
+ final BubbleBarExpandedView bbev = mExpandedBubble.getBubbleBarExpandedView();
+ if (bbev != null) {
+ updateExpandedView();
+ bbev.setAnimating(true);
+ bbev.setContentVisibility(true);
+ bbev.setSurfaceZOrderedOnTop(true);
+ bbev.setTaskViewAlpha(0.f);
+ bbev.setVisibility(VISIBLE);
+ }
+
+ return prior;
+ }
+
+ /**
+ * Starts and animates a conversion-from transition.
+ *
+ * @param startT A transaction with first-frame work. this *will* be applied here!
+ */
+ public void animateConvert(@NonNull SurfaceControl.Transaction startT,
+ @NonNull Rect startBounds, @NonNull SurfaceControl snapshot, SurfaceControl taskLeash,
+ Runnable animFinish) {
+ if (!mIsExpanded || mExpandedBubble == null) {
+ throw new IllegalStateException("Can't animateExpand without expanded state");
+ }
+ mAnimationHelper.animateConvert(mExpandedBubble, startT, startBounds, snapshot, taskLeash,
+ animFinish);
+ }
+
+ /**
+ * Populates {@param out} with the rest bounds of an expanded bubble.
+ */
+ public void getExpandedViewRestBounds(Rect out) {
+ mAnimationHelper.getExpandedViewRestBounds(out);
}
/** Removes the given {@code bubble}. */
public void removeBubble(Bubble bubble, Runnable endAction) {
+ final boolean inTransition = bubble.getPreparingTransition() != null;
Runnable cleanUp = () -> {
- bubble.cleanupViews();
+ // The transition is already managing the task/wm state.
+ bubble.cleanupViews(!inTransition);
endAction.run();
};
if (mBubbleData.getBubbles().isEmpty()) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt
index 4cd2fd04d3cf..ff3e65a247ae 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt
@@ -15,16 +15,21 @@
*/
package com.android.wm.shell.common
+import android.annotation.UserIdInt
import android.app.PendingIntent
import android.content.ComponentName
import android.content.Context
import android.content.pm.LauncherApps
import android.content.pm.PackageManager
+import android.content.pm.PackageManager.Property
import android.os.UserHandle
import android.view.WindowManager.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.R
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL
+import com.android.wm.shell.sysui.ShellCommandHandler
+import com.android.wm.shell.sysui.ShellInit
+import java.io.PrintWriter
import java.util.Arrays
/**
@@ -35,12 +40,23 @@ class MultiInstanceHelper @JvmOverloads constructor(
private val packageManager: PackageManager,
private val staticAppsSupportingMultiInstance: Array<String> = context.resources
.getStringArray(R.array.config_appsSupportMultiInstancesSplit),
- private val supportsMultiInstanceProperty: Boolean) {
+ shellInit: ShellInit,
+ private val shellCommandHandler: ShellCommandHandler,
+ private val supportsMultiInstanceProperty: Boolean
+) : ShellCommandHandler.ShellCommandActionHandler {
+
+ init {
+ shellInit.addInitCallback(this::onInit, this)
+ }
+
+ private fun onInit() {
+ shellCommandHandler.addCommandCallback("multi-instance", this, this)
+ }
/**
* Returns whether a specific component desires to be launched in multiple instances.
*/
- fun supportsMultiInstanceSplit(componentName: ComponentName?): Boolean {
+ fun supportsMultiInstanceSplit(componentName: ComponentName?, @UserIdInt userId: Int): Boolean {
if (componentName == null || componentName.packageName == null) {
// TODO(b/262864589): Handle empty component case
return false
@@ -63,8 +79,9 @@ class MultiInstanceHelper @JvmOverloads constructor(
// Check the activity property first
try {
- val activityProp = packageManager.getProperty(
- PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, componentName)
+ val activityProp = packageManager.getPropertyAsUser(
+ PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, componentName.packageName,
+ componentName.className, userId)
// If the above call doesn't throw a NameNotFoundException, then the activity property
// should override the application property value
if (activityProp.isBoolean) {
@@ -80,8 +97,9 @@ class MultiInstanceHelper @JvmOverloads constructor(
// Check the application property otherwise
try {
- val appProp = packageManager.getProperty(
- PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, packageName)
+ val appProp = packageManager.getPropertyAsUser(
+ PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, packageName, null /* className */,
+ userId)
if (appProp.isBoolean) {
ProtoLog.v(WM_SHELL, "application=%s supports multi-instance", packageName)
return appProp.boolean
@@ -96,6 +114,66 @@ class MultiInstanceHelper @JvmOverloads constructor(
return false
}
+ override fun onShellCommand(args: Array<out String>?, pw: PrintWriter?): Boolean {
+ if (pw == null || args == null || args.isEmpty()) {
+ return false
+ }
+ when (args[0]) {
+ "list" -> return dumpSupportedApps(pw)
+ }
+ return false
+ }
+
+ override fun printShellCommandHelp(pw: PrintWriter, prefix: String) {
+ pw.println("${prefix}list")
+ pw.println("$prefix Lists all the packages that support the multiinstance property")
+ }
+
+ /**
+ * Dumps the static allowlist and list of apps that have the declared property in the manifest.
+ */
+ private fun dumpSupportedApps(pw: PrintWriter): Boolean {
+ pw.println("Static allow list (for all users):")
+ staticAppsSupportingMultiInstance.forEach { pkg ->
+ pw.println(" $pkg")
+ }
+
+ // TODO(b/391693747): Dump this per-user once PM allows us to query properties
+ // for non-calling users
+ val apps = packageManager.queryApplicationProperty(
+ PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI)
+ val activities = packageManager.queryActivityProperty(
+ PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI)
+ val appsWithProperty = (apps + activities)
+ .sortedWith(object : Comparator<Property?> {
+ override fun compare(o1: Property?, o2: Property?): Int {
+ if (o1?.packageName != o2?.packageName) {
+ return o1?.packageName!!.compareTo(o2?.packageName!!)
+ } else {
+ if (o1?.className != null) {
+ return o1.className!!.compareTo(o2?.className!!)
+ } else if (o2?.className != null) {
+ return -o2.className!!.compareTo(o1?.className!!)
+ }
+ return 0
+ }
+ }
+ })
+ if (appsWithProperty.isNotEmpty()) {
+ pw.println("Apps (User ${context.userId}):")
+ appsWithProperty.forEach { prop ->
+ if (prop.isBoolean && prop.boolean) {
+ if (prop.className != null) {
+ pw.println(" ${prop.packageName}/${prop.className}")
+ } else {
+ pw.println(" ${prop.packageName}")
+ }
+ }
+ }
+ }
+ return true
+ }
+
companion object {
/** Returns the component from a PendingIntent */
@JvmStatic
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/UserProfileContexts.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/UserProfileContexts.kt
new file mode 100644
index 000000000000..0577f9e625ca
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/UserProfileContexts.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common
+
+import android.app.ActivityManager
+import android.content.Context
+import android.content.pm.UserInfo
+import android.os.UserHandle
+import android.os.UserManager
+import android.util.SparseArray
+import com.android.wm.shell.sysui.ShellController
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.sysui.UserChangeListener
+
+/** Creates and manages contexts for all the profiles of the current user. */
+class UserProfileContexts(
+ private val baseContext: Context,
+ private val shellController: ShellController,
+ shellInit: ShellInit,
+) {
+ // Contexts for all the profiles of the current user.
+ private val currentProfilesContext = SparseArray<Context>()
+
+ lateinit var userContext: Context
+ private set
+
+ init {
+ shellInit.addInitCallback(this::onInit, this)
+ }
+
+ private fun onInit() {
+ shellController.addUserChangeListener(
+ object : UserChangeListener {
+ override fun onUserChanged(newUserId: Int, userContext: Context) {
+ currentProfilesContext.clear()
+ this@UserProfileContexts.userContext = userContext
+ currentProfilesContext.put(newUserId, userContext)
+ }
+
+ override fun onUserProfilesChanged(profiles: List<UserInfo>) {
+ updateProfilesContexts(profiles)
+ }
+ }
+ )
+ val defaultUserId = ActivityManager.getCurrentUser()
+ val userManager = baseContext.getSystemService(UserManager::class.java)
+ userContext = baseContext.createContextAsUser(UserHandle.of(defaultUserId), /* flags= */ 0)
+ updateProfilesContexts(userManager.getProfiles(defaultUserId))
+ }
+
+ private fun updateProfilesContexts(profiles: List<UserInfo>) {
+ for (profile in profiles) {
+ if (profile.id in currentProfilesContext) continue
+ val profileContext = baseContext.createContextAsUser(profile.userHandle, /* flags= */ 0)
+ currentProfilesContext.put(profile.id, profileContext)
+ }
+ val profilesToRemove = buildList<Int> {
+ for (i in 0..<currentProfilesContext.size()) {
+ val userId = currentProfilesContext.keyAt(i)
+ if (profiles.none { it.id == userId }) {
+ add(userId)
+ }
+ }
+ }
+ profilesToRemove.forEach { currentProfilesContext.remove(it) }
+ }
+
+ operator fun get(userId: Int): Context? = currentProfilesContext.get(userId)
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/AppCompatUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/AppCompatUtils.kt
deleted file mode 100644
index d1dcc9b1d591..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/AppCompatUtils.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:JvmName("AppCompatUtils")
-
-package com.android.wm.shell.compatui
-
-import android.app.ActivityManager.RunningTaskInfo
-import android.content.Context
-import com.android.internal.R
-
-// TODO(b/347289970): Consider replacing with API
-/**
- * If the top activity should be exempt from desktop windowing and forced back to fullscreen.
- * Currently includes all system ui activities and modal dialogs. However if the top activity is not
- * being displayed, regardless of its configuration, we will not exempt it as to remain in the
- * desktop windowing environment.
- */
-fun isTopActivityExemptFromDesktopWindowing(context: Context, task: RunningTaskInfo) =
- (isSystemUiTask(context, task) || isTransparentTask(task))
- && !task.isTopActivityNoDisplay
-
-/**
- * Returns true if all activities in a tasks stack are transparent. If there are no activities will
- * return false.
- */
-fun isTransparentTask(task: RunningTaskInfo): Boolean = task.isActivityStackTransparent
- && task.numActivities > 0
-
-private fun isSystemUiTask(context: Context, task: RunningTaskInfo): Boolean {
- val sysUiPackageName: String =
- context.resources.getString(R.string.config_systemUi)
- return task.baseActivity?.packageName == sysUiPackageName
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 84042591ad1b..e0a829df79ad 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -43,6 +43,8 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.WindowManagerShellWrapper;
import com.android.wm.shell.activityembedding.ActivityEmbeddingController;
+import com.android.wm.shell.appzoomout.AppZoomOut;
+import com.android.wm.shell.appzoomout.AppZoomOutController;
import com.android.wm.shell.back.BackAnimation;
import com.android.wm.shell.back.BackAnimationBackground;
import com.android.wm.shell.back.BackAnimationController;
@@ -112,9 +114,8 @@ import com.android.wm.shell.shared.TransactionPool;
import com.android.wm.shell.shared.annotations.ShellAnimationThread;
import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.shared.annotations.ShellSplashscreenThread;
+import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
-import com.android.wm.shell.appzoomout.AppZoomOut;
-import com.android.wm.shell.appzoomout.AppZoomOutController;
import com.android.wm.shell.splitscreen.SplitScreen;
import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.startingsurface.StartingSurface;
@@ -258,6 +259,12 @@ public abstract class WMShellBaseModule {
@WMSingleton
@Provides
+ static DesktopModeCompatPolicy provideDesktopModeCompatPolicy(Context context) {
+ return new DesktopModeCompatPolicy(context);
+ }
+
+ @WMSingleton
+ @Provides
static Optional<CompatUIHandler> provideCompatUIController(
Context context,
ShellInit shellInit,
@@ -410,9 +417,13 @@ public abstract class WMShellBaseModule {
@WMSingleton
@Provides
- static MultiInstanceHelper provideMultiInstanceHelper(Context context) {
+ static MultiInstanceHelper provideMultiInstanceHelper(
+ Context context,
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler
+ ) {
return new MultiInstanceHelper(context, context.getPackageManager(),
- Flags.supportsMultiInstanceSystemUi());
+ shellInit, shellCommandHandler, Flags.supportsMultiInstanceSystemUi());
}
//
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index fdfaa90ac8b9..6657c9e4b9a9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -16,6 +16,7 @@
package com.android.wm.shell.dagger;
+import static android.window.DesktopModeFlags.ENABLE_DESKTOP_SYSTEM_DIALOGS_TRANSITIONS;
import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_ENTER_TRANSITIONS;
import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_ENTER_TRANSITIONS_BUGFIX;
import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY;
@@ -37,6 +38,7 @@ import android.os.UserManager;
import android.view.Choreographer;
import android.view.IWindowManager;
import android.view.WindowManager;
+import android.window.DesktopModeFlags;
import androidx.annotation.OptIn;
@@ -70,6 +72,7 @@ import com.android.wm.shell.common.MultiInstanceHelper;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.TaskStackListenerImpl;
+import com.android.wm.shell.common.UserProfileContexts;
import com.android.wm.shell.common.split.SplitState;
import com.android.wm.shell.compatui.letterbox.LetterboxCommandHandler;
import com.android.wm.shell.compatui.letterbox.LetterboxTransitionObserver;
@@ -108,6 +111,8 @@ import com.android.wm.shell.desktopmode.education.AppToWebEducationController;
import com.android.wm.shell.desktopmode.education.AppToWebEducationFilter;
import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository;
import com.android.wm.shell.desktopmode.education.data.AppToWebEducationDatastoreRepository;
+import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer;
+import com.android.wm.shell.desktopmode.multidesks.RootTaskDesksOrganizer;
import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository;
import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer;
import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializerImpl;
@@ -129,6 +134,7 @@ import com.android.wm.shell.shared.TransactionPool;
import com.android.wm.shell.shared.annotations.ShellAnimationThread;
import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
import com.android.wm.shell.shared.annotations.ShellMainThread;
+import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.sysui.ShellCommandHandler;
@@ -395,6 +401,7 @@ public abstract class WMShellModule {
ShellTaskOrganizer shellTaskOrganizer,
Optional<DesktopUserRepositories> desktopUserRepositories,
Optional<DesktopTasksController> desktopTasksController,
+ DesktopModeLoggerTransitionObserver desktopModeLoggerTransitionObserver,
LaunchAdjacentController launchAdjacentController,
WindowDecorViewModel windowDecorViewModel,
Optional<TaskChangeListener> taskChangeListener) {
@@ -407,6 +414,7 @@ public abstract class WMShellModule {
shellTaskOrganizer,
desktopUserRepositories,
desktopTasksController,
+ desktopModeLoggerTransitionObserver,
launchAdjacentController,
windowDecorViewModel,
taskChangeListener);
@@ -703,6 +711,16 @@ public abstract class WMShellModule {
@WMSingleton
@Provides
+ static DesksOrganizer provideDesksOrganizer(
+ @NonNull ShellInit shellInit,
+ @NonNull ShellCommandHandler shellCommandHandler,
+ @NonNull ShellTaskOrganizer shellTaskOrganizer
+ ) {
+ return new RootTaskDesksOrganizer(shellInit, shellCommandHandler, shellTaskOrganizer);
+ }
+
+ @WMSingleton
+ @Provides
@DynamicOverride
static DesktopTasksController provideDesktopTasksController(
Context context,
@@ -741,7 +759,10 @@ public abstract class WMShellModule {
DesktopTilingDecorViewModel desktopTilingDecorViewModel,
DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider,
Optional<BubbleController> bubbleController,
- OverviewToDesktopTransitionObserver overviewToDesktopTransitionObserver) {
+ OverviewToDesktopTransitionObserver overviewToDesktopTransitionObserver,
+ DesksOrganizer desksOrganizer,
+ UserProfileContexts userProfileContexts,
+ DesktopModeCompatPolicy desktopModeCompatPolicy) {
return new DesktopTasksController(
context,
shellInit,
@@ -775,7 +796,10 @@ public abstract class WMShellModule {
desktopTilingDecorViewModel,
desktopWallpaperActivityTokenProvider,
bubbleController,
- overviewToDesktopTransitionObserver);
+ overviewToDesktopTransitionObserver,
+ desksOrganizer,
+ userProfileContexts,
+ desktopModeCompatPolicy);
}
@WMSingleton
@@ -792,7 +816,9 @@ public abstract class WMShellModule {
ReturnToDragStartAnimator returnToDragStartAnimator,
@DynamicOverride DesktopUserRepositories desktopUserRepositories,
DesktopModeEventLogger desktopModeEventLogger,
- WindowDecorTaskResourceLoader windowDecorTaskResourceLoader) {
+ WindowDecorTaskResourceLoader windowDecorTaskResourceLoader,
+ FocusTransitionObserver focusTransitionObserver,
+ @ShellMainThread ShellExecutor mainExecutor) {
return new DesktopTilingDecorViewModel(
context,
mainDispatcher,
@@ -806,7 +832,9 @@ public abstract class WMShellModule {
returnToDragStartAnimator,
desktopUserRepositories,
desktopModeEventLogger,
- windowDecorTaskResourceLoader
+ windowDecorTaskResourceLoader,
+ focusTransitionObserver,
+ mainExecutor
);
}
@@ -907,7 +935,7 @@ public abstract class WMShellModule {
if (DesktopModeStatus.canEnterDesktopMode(context) && useKeyGestureEventHandler()
&& manageKeyGestures()
&& (Flags.enableMoveToNextDisplayShortcut()
- || Flags.enableTaskResizingKeyboardShortcuts())) {
+ || DesktopModeFlags.ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS.isTrue())) {
return Optional.of(new DesktopModeKeyGestureHandler(context,
desktopModeWindowDecorViewModel, desktopTasksController,
inputManager, shellTaskOrganizer, focusTransitionObserver,
@@ -953,7 +981,8 @@ public abstract class WMShellModule {
DesktopModeEventLogger desktopModeEventLogger,
DesktopModeUiEventLogger desktopModeUiEventLogger,
WindowDecorTaskResourceLoader taskResourceLoader,
- RecentsTransitionHandler recentsTransitionHandler
+ RecentsTransitionHandler recentsTransitionHandler,
+ DesktopModeCompatPolicy desktopModeCompatPolicy
) {
if (!DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)) {
return Optional.empty();
@@ -969,7 +998,7 @@ public abstract class WMShellModule {
desktopTasksLimiter, appHandleEducationController, appToWebEducationController,
windowDecorCaptionHandleRepository, activityOrientationChangeHandler,
focusTransitionObserver, desktopModeEventLogger, desktopModeUiEventLogger,
- taskResourceLoader, recentsTransitionHandler));
+ taskResourceLoader, recentsTransitionHandler, desktopModeCompatPolicy));
}
@WMSingleton
@@ -977,9 +1006,10 @@ public abstract class WMShellModule {
static WindowDecorTaskResourceLoader provideWindowDecorTaskResourceLoader(
@NonNull Context context, @NonNull ShellInit shellInit,
@NonNull ShellController shellController,
- @NonNull ShellCommandHandler shellCommandHandler) {
+ @NonNull ShellCommandHandler shellCommandHandler,
+ @NonNull UserProfileContexts userProfileContexts) {
return new WindowDecorTaskResourceLoader(context, shellInit, shellController,
- shellCommandHandler);
+ shellCommandHandler, userProfileContexts);
}
@WMSingleton
@@ -990,16 +1020,17 @@ public abstract class WMShellModule {
@ShellAnimationThread ShellExecutor animExecutor,
ShellInit shellInit,
Transitions transitions,
- @DynamicOverride DesktopUserRepositories desktopUserRepositories) {
+ @DynamicOverride DesktopUserRepositories desktopUserRepositories,
+ DesktopModeCompatPolicy desktopModeCompatPolicy) {
if (!DesktopModeStatus.canEnterDesktopMode(context)
|| !ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue()
- || !Flags.enableDesktopSystemDialogsTransitions()) {
+ || !ENABLE_DESKTOP_SYSTEM_DIALOGS_TRANSITIONS.isTrue()) {
return Optional.empty();
}
return Optional.of(
new SystemModalsTransitionHandler(
context, mainExecutor, animExecutor, shellInit, transitions,
- desktopUserRepositories));
+ desktopUserRepositories, desktopModeCompatPolicy));
}
@WMSingleton
@@ -1183,10 +1214,12 @@ public abstract class WMShellModule {
Transitions transitions,
DisplayController displayController,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
- IWindowManager windowManager
+ IWindowManager windowManager,
+ Optional<DesktopUserRepositories> desktopUserRepositories,
+ Optional<DesktopTasksController> desktopTasksController,
+ ShellTaskOrganizer shellTaskOrganizer
) {
- if (!DesktopModeStatus.canEnterDesktopMode(context)
- || !Flags.enableDisplayWindowingModeSwitching()) {
+ if (!DesktopModeStatus.canEnterDesktopMode(context)) {
return Optional.empty();
}
return Optional.of(
@@ -1196,7 +1229,10 @@ public abstract class WMShellModule {
transitions,
displayController,
rootTaskDisplayAreaOrganizer,
- windowManager));
+ windowManager,
+ desktopUserRepositories.get(),
+ desktopTasksController.get(),
+ shellTaskOrganizer));
}
@WMSingleton
@@ -1406,4 +1442,14 @@ public abstract class WMShellModule {
Transitions transitions, ShellInit shellInit) {
return new OverviewToDesktopTransitionObserver(transitions, shellInit);
}
+
+ @WMSingleton
+ @Provides
+ static UserProfileContexts provideUserProfilesContexts(
+ Context context,
+ ShellController shellController,
+ ShellInit shellInit) {
+ return new UserProfileContexts(context, shellController, shellInit);
+ }
+
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
index 793bdf0b5614..413300612f7d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
@@ -159,11 +159,12 @@ public abstract class Pip2Module {
PipUiEventLogger pipUiEventLogger,
PipTaskListener pipTaskListener,
@NonNull PipTransitionState pipTransitionState,
+ @NonNull PipDisplayLayoutState pipDisplayLayoutState,
@ShellMainThread ShellExecutor mainExecutor,
@ShellMainThread Handler mainHandler) {
return new PhonePipMenuController(context, pipBoundsState, pipMediaController,
- systemWindows, pipUiEventLogger, pipTaskListener, pipTransitionState, mainExecutor,
- mainHandler);
+ systemWindows, pipUiEventLogger, pipTaskListener, pipTransitionState,
+ pipDisplayLayoutState, mainExecutor, mainHandler);
}
@@ -178,6 +179,8 @@ public abstract class Pip2Module {
@NonNull PipTransitionState pipTransitionState,
@NonNull PipScheduler pipScheduler,
@NonNull SizeSpecSource sizeSpecSource,
+ @NonNull PipDisplayLayoutState pipDisplayLayoutState,
+ DisplayController displayController,
PipMotionHelper pipMotionHelper,
FloatingContentCoordinator floatingContentCoordinator,
PipUiEventLogger pipUiEventLogger,
@@ -185,8 +188,9 @@ public abstract class Pip2Module {
Optional<PipPerfHintController> pipPerfHintControllerOptional) {
return new PipTouchHandler(context, shellInit, shellCommandHandler, menuPhoneController,
pipBoundsAlgorithm, pipBoundsState, pipTransitionState, pipScheduler,
- sizeSpecSource, pipMotionHelper, floatingContentCoordinator, pipUiEventLogger,
- mainExecutor, pipPerfHintControllerOptional);
+ sizeSpecSource, pipDisplayLayoutState, displayController, pipMotionHelper,
+ floatingContentCoordinator, pipUiEventLogger, mainExecutor,
+ pipPerfHintControllerOptional);
}
@WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt
index 43e8d2a30930..6f455df6cfec 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt
@@ -16,7 +16,10 @@
package com.android.wm.shell.desktopmode
+import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
+import android.app.WindowConfiguration.windowingModeToString
import android.content.Context
import android.provider.Settings
import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS
@@ -24,9 +27,15 @@ import android.view.Display.DEFAULT_DISPLAY
import android.view.IWindowManager
import android.view.WindowManager.TRANSIT_CHANGE
import android.window.WindowContainerTransaction
+import com.android.internal.protolog.ProtoLog
+import com.android.window.flags.Flags
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener
+import com.android.wm.shell.desktopmode.multidesks.OnDeskRemovedListener
+import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.transition.Transitions
@@ -38,7 +47,13 @@ class DesktopDisplayEventHandler(
private val displayController: DisplayController,
private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
private val windowManager: IWindowManager,
-) : OnDisplaysChangedListener {
+ private val desktopUserRepositories: DesktopUserRepositories,
+ private val desktopTasksController: DesktopTasksController,
+ private val shellTaskOrganizer: ShellTaskOrganizer,
+) : OnDisplaysChangedListener, OnDeskRemovedListener {
+
+ private val desktopRepository: DesktopRepository
+ get() = desktopUserRepositories.current
init {
shellInit.addInitCallback({ onInit() }, this)
@@ -46,23 +61,48 @@ class DesktopDisplayEventHandler(
private fun onInit() {
displayController.addDisplayWindowListener(this)
+
+ if (Flags.enableMultipleDesktopsBackend()) {
+ desktopTasksController.onDeskRemovedListener = this
+ }
}
override fun onDisplayAdded(displayId: Int) {
- if (displayId == DEFAULT_DISPLAY) {
+ if (displayId != DEFAULT_DISPLAY) {
+ refreshDisplayWindowingMode()
+ }
+
+ if (!supportsDesks(displayId)) {
+ logV("Display #$displayId does not support desks")
return
}
- refreshDisplayWindowingMode()
+ logV("Creating new desk in new display#$displayId")
+ // TODO: b/362720497 - when SystemUI crashes with a freeform task open for any reason, the
+ // task is recreated and received in [FreeformTaskListener] before this display callback
+ // is invoked, which results in the repository trying to add the task to a desk before the
+ // desk has been recreated here, which may result in a crash-loop if the repository is
+ // checking that the desk exists before adding a task to it. See b/391984373.
+ desktopTasksController.createDesk(displayId)
}
override fun onDisplayRemoved(displayId: Int) {
- if (displayId == DEFAULT_DISPLAY) {
- return
+ if (displayId != DEFAULT_DISPLAY) {
+ refreshDisplayWindowingMode()
+ }
+
+ // TODO: b/362720497 - move desks in closing display to the remaining desk.
+ }
+
+ override fun onDeskRemoved(lastDisplayId: Int, deskId: Int) {
+ val remainingDesks = desktopRepository.getNumberOfDesks(lastDisplayId)
+ if (remainingDesks == 0) {
+ logV("All desks removed from display#$lastDisplayId, creating empty desk")
+ desktopTasksController.createDesk(lastDisplayId)
}
- refreshDisplayWindowingMode()
}
private fun refreshDisplayWindowingMode() {
+ if (!Flags.enableDisplayWindowingModeSwitching()) return
// TODO: b/375319538 - Replace the check with a DisplayManager API once it's available.
val isExtendedDisplayEnabled =
0 !=
@@ -89,13 +129,46 @@ class DesktopDisplayEventHandler(
}
val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)
requireNotNull(tdaInfo) { "DisplayAreaInfo of DEFAULT_DISPLAY must be non-null." }
- if (tdaInfo.configuration.windowConfiguration.windowingMode == targetDisplayWindowingMode) {
+ val currentDisplayWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
+ if (currentDisplayWindowingMode == targetDisplayWindowingMode) {
// Already in the target mode.
return
}
+ logV(
+ "As an external display is connected, changing default display's windowing mode from" +
+ " ${windowingModeToString(currentDisplayWindowingMode)}" +
+ " to ${windowingModeToString(targetDisplayWindowingMode)}"
+ )
+
val wct = WindowContainerTransaction()
wct.setWindowingMode(tdaInfo.token, targetDisplayWindowingMode)
+ shellTaskOrganizer
+ .getRunningTasks(DEFAULT_DISPLAY)
+ .filter { it.activityType == ACTIVITY_TYPE_STANDARD }
+ .forEach {
+ // TODO: b/391965153 - Reconsider the logic under multi-desk window hierarchy
+ when (it.windowingMode) {
+ currentDisplayWindowingMode -> {
+ wct.setWindowingMode(it.token, currentDisplayWindowingMode)
+ }
+ targetDisplayWindowingMode -> {
+ wct.setWindowingMode(it.token, WINDOWING_MODE_UNDEFINED)
+ }
+ }
+ }
transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
}
+
+ // TODO: b/362720497 - connected/projected display considerations.
+ private fun supportsDesks(displayId: Int): Boolean =
+ DesktopModeStatus.canEnterDesktopMode(context)
+
+ private fun logV(msg: String, vararg arguments: Any?) {
+ ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
+ }
+
+ companion object {
+ private const val TAG = "DesktopDisplayEventHandler"
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
index 7897c0aa35bc..f5a95a670036 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
@@ -24,10 +24,10 @@ import android.view.MotionEvent
import android.view.MotionEvent.TOOL_TYPE_FINGER
import android.view.MotionEvent.TOOL_TYPE_MOUSE
import android.view.MotionEvent.TOOL_TYPE_STYLUS
+import android.window.DesktopModeFlags
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.protolog.ProtoLog
import com.android.internal.util.FrameworkStatsLog
-import com.android.window.flags.Flags
import com.android.wm.shell.EventLogTags
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
@@ -185,7 +185,7 @@ class DesktopModeEventLogger {
displayController: DisplayController? = null,
displayLayoutSize: Size? = null,
) {
- if (!Flags.enableResizingMetrics()) return
+ if (!DesktopModeFlags.ENABLE_RESIZING_METRICS.isTrue) return
val sessionId = currentSessionId.get()
if (sessionId == NO_SESSION_ID) {
@@ -232,7 +232,7 @@ class DesktopModeEventLogger {
displayController: DisplayController? = null,
displayLayoutSize: Size? = null,
) {
- if (!Flags.enableResizingMetrics()) return
+ if (!DesktopModeFlags.ENABLE_RESIZING_METRICS.isTrue) return
val sessionId = currentSessionId.get()
if (sessionId == NO_SESSION_ID) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt
index 9334898fdb93..5269318943d9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt
@@ -23,10 +23,10 @@ import android.hardware.input.InputManager
import android.hardware.input.InputManager.KeyGestureEventHandler
import android.hardware.input.KeyGestureEvent
import android.os.IBinder
+import android.window.DesktopModeFlags
import com.android.hardware.input.Flags.manageKeyGestures
import com.android.internal.protolog.ProtoLog
import com.android.window.flags.Flags.enableMoveToNextDisplayShortcut
-import com.android.window.flags.Flags.enableTaskResizingKeyboardShortcuts
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.ShellExecutor
@@ -144,7 +144,8 @@ class DesktopModeKeyGestureHandler(
KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW,
KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW,
KeyGestureEvent.KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW ->
- enableTaskResizingKeyboardShortcuts() && manageKeyGestures()
+ DesktopModeFlags.ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS.isTrue &&
+ manageKeyGestures()
else -> false
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
index a43358603bc3..3b051694ae81 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
@@ -167,6 +167,29 @@ class DesktopModeLoggerTransitionObserver(
override fun onTransitionFinished(transition: IBinder, aborted: Boolean) {}
+ fun onTaskVanished(taskInfo: RunningTaskInfo) {
+ // At this point the task should have been cleared up due to transition. If it's not yet
+ // cleared up, it might be one of the edge cases where transitions don't give the correct
+ // signal.
+ if (visibleFreeformTaskInfos.containsKey(taskInfo.taskId)) {
+ val postTransitionFreeformTasks: SparseArray<TaskInfo> = SparseArray()
+ postTransitionFreeformTasks.putAll(visibleFreeformTaskInfos)
+ postTransitionFreeformTasks.remove(taskInfo.taskId)
+ ProtoLog.v(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopModeLogger: processing tasks after task vanished %s",
+ postTransitionFreeformTasks.size(),
+ )
+ identifyLogEventAndUpdateState(
+ transition = null,
+ transitionInfo = null,
+ preTransitionVisibleFreeformTasks = visibleFreeformTaskInfos,
+ postTransitionVisibleFreeformTasks = postTransitionFreeformTasks,
+ newFocusedFreeformTask = null,
+ )
+ }
+ }
+
// Returns null if there was no change in focused task
private fun getNewFocusedFreeformTask(info: TransitionInfo): TaskInfo? {
val freeformWindowChanges =
@@ -253,8 +276,8 @@ class DesktopModeLoggerTransitionObserver(
* state and update it
*/
private fun identifyLogEventAndUpdateState(
- transition: IBinder,
- transitionInfo: TransitionInfo,
+ transition: IBinder?,
+ transitionInfo: TransitionInfo?,
preTransitionVisibleFreeformTasks: SparseArray<TaskInfo>,
postTransitionVisibleFreeformTasks: SparseArray<TaskInfo>,
newFocusedFreeformTask: TaskInfo?,
@@ -310,8 +333,8 @@ class DesktopModeLoggerTransitionObserver(
/** Compare the old and new state of taskInfos and identify and log the changes */
private fun identifyAndLogTaskUpdates(
- transition: IBinder,
- transitionInfo: TransitionInfo,
+ transition: IBinder?,
+ transitionInfo: TransitionInfo?,
preTransitionVisibleFreeformTasks: SparseArray<TaskInfo>,
postTransitionVisibleFreeformTasks: SparseArray<TaskInfo>,
newFocusedFreeformTask: TaskInfo?,
@@ -384,22 +407,24 @@ class DesktopModeLoggerTransitionObserver(
}
private fun getMinimizeReason(
- transition: IBinder,
- transitionInfo: TransitionInfo,
+ transition: IBinder?,
+ transitionInfo: TransitionInfo?,
taskInfo: TaskInfo,
): MinimizeReason? {
- if (transitionInfo.type == Transitions.TRANSIT_MINIMIZE) {
+ if (transitionInfo?.type == Transitions.TRANSIT_MINIMIZE) {
return MinimizeReason.MINIMIZE_BUTTON
}
- val minimizingTask = desktopTasksLimiter.getOrNull()?.getMinimizingTask(transition)
+ val minimizingTask =
+ transition?.let { desktopTasksLimiter.getOrNull()?.getMinimizingTask(transition) }
if (minimizingTask?.taskId == taskInfo.taskId) {
return minimizingTask.minimizeReason
}
return null
}
- private fun getUnminimizeReason(transition: IBinder, taskInfo: TaskInfo): UnminimizeReason? {
- val unminimizingTask = desktopTasksLimiter.getOrNull()?.getUnminimizingTask(transition)
+ private fun getUnminimizeReason(transition: IBinder?, taskInfo: TaskInfo): UnminimizeReason? {
+ val unminimizingTask =
+ transition?.let { desktopTasksLimiter.getOrNull()?.getUnminimizingTask(transition) }
if (unminimizingTask?.taskId == taskInfo.taskId) {
return unminimizingTask.unminimizeReason
}
@@ -441,24 +466,24 @@ class DesktopModeLoggerTransitionObserver(
}
/** Get [EnterReason] for this session enter */
- private fun getEnterReason(transitionInfo: TransitionInfo): EnterReason {
+ private fun getEnterReason(transitionInfo: TransitionInfo?): EnterReason {
val enterReason =
when {
- transitionInfo.type == WindowManager.TRANSIT_WAKE
+ transitionInfo?.type == WindowManager.TRANSIT_WAKE
// If there is a screen lock, desktop window entry is after dismissing keyguard
||
- (transitionInfo.type == WindowManager.TRANSIT_TO_BACK &&
+ (transitionInfo?.type == WindowManager.TRANSIT_TO_BACK &&
wasPreviousTransitionExitByScreenOff) -> EnterReason.SCREEN_ON
- transitionInfo.type == TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP ->
+ transitionInfo?.type == TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP ->
EnterReason.APP_HANDLE_DRAG
- transitionInfo.type == TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON ->
+ transitionInfo?.type == TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON ->
EnterReason.APP_HANDLE_MENU_BUTTON
- transitionInfo.type == TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW ->
+ transitionInfo?.type == TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW ->
EnterReason.APP_FROM_OVERVIEW
- transitionInfo.type == TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT ->
+ transitionInfo?.type == TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT ->
EnterReason.KEYBOARD_SHORTCUT_ENTER
// NOTE: the below condition also applies for EnterReason quickswitch
- transitionInfo.type == WindowManager.TRANSIT_TO_FRONT -> EnterReason.OVERVIEW
+ transitionInfo?.type == WindowManager.TRANSIT_TO_FRONT -> EnterReason.OVERVIEW
// Enter desktop mode from cancelled recents has no transition. Enter is detected on
// the
// next transition involving freeform windows.
@@ -469,12 +494,13 @@ class DesktopModeLoggerTransitionObserver(
// after
// a cancelled recents.
wasPreviousTransitionExitToOverview -> EnterReason.OVERVIEW
- transitionInfo.type == WindowManager.TRANSIT_OPEN -> EnterReason.APP_FREEFORM_INTENT
+ transitionInfo?.type == WindowManager.TRANSIT_OPEN ->
+ EnterReason.APP_FREEFORM_INTENT
else -> {
ProtoLog.w(
WM_SHELL_DESKTOP_MODE,
"Unknown enter reason for transition type: %s",
- transitionInfo.type,
+ transitionInfo?.type,
)
EnterReason.UNKNOWN_ENTER
}
@@ -484,30 +510,31 @@ class DesktopModeLoggerTransitionObserver(
}
/** Get [ExitReason] for this session exit */
- private fun getExitReason(transitionInfo: TransitionInfo): ExitReason =
+ private fun getExitReason(transitionInfo: TransitionInfo?): ExitReason =
when {
- transitionInfo.type == WindowManager.TRANSIT_SLEEP -> {
+ transitionInfo?.type == WindowManager.TRANSIT_SLEEP -> {
wasPreviousTransitionExitByScreenOff = true
ExitReason.SCREEN_OFF
}
// TODO(b/384490301): differentiate back gesture / button exit from clicking the close
// button located in the window top corner.
- transitionInfo.type == WindowManager.TRANSIT_TO_BACK -> ExitReason.TASK_MOVED_TO_BACK
- transitionInfo.type == WindowManager.TRANSIT_CLOSE -> ExitReason.TASK_FINISHED
- transitionInfo.type == TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG -> ExitReason.DRAG_TO_EXIT
- transitionInfo.type == TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON ->
+ transitionInfo?.type == WindowManager.TRANSIT_TO_BACK -> ExitReason.TASK_MOVED_TO_BACK
+ transitionInfo?.type == WindowManager.TRANSIT_CLOSE -> ExitReason.TASK_FINISHED
+ transitionInfo?.type == TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG -> ExitReason.DRAG_TO_EXIT
+ transitionInfo?.type == TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON ->
ExitReason.APP_HANDLE_MENU_BUTTON_EXIT
- transitionInfo.type == TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT ->
+ transitionInfo?.type == TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT ->
ExitReason.KEYBOARD_SHORTCUT_EXIT
- transitionInfo.isExitToRecentsTransition() -> ExitReason.RETURN_HOME_OR_OVERVIEW
- transitionInfo.type == Transitions.TRANSIT_MINIMIZE -> ExitReason.TASK_MINIMIZED
+ transitionInfo?.isExitToRecentsTransition() == true ->
+ ExitReason.RETURN_HOME_OR_OVERVIEW
+ transitionInfo?.type == Transitions.TRANSIT_MINIMIZE -> ExitReason.TASK_MINIMIZED
else -> {
ProtoLog.w(
WM_SHELL_DESKTOP_MODE,
"Unknown exit reason for transition type: %s",
- transitionInfo.type,
+ transitionInfo?.type,
)
ExitReason.UNKNOWN_EXIT
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt
index 9b9988457808..164d04bbde65 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt
@@ -110,8 +110,8 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl
pw.println("Error: display id should be an integer")
return false
}
- pw.println("Not implemented.")
- return false
+ controller.createDesk(displayId)
+ return true
}
private fun runActivateDesk(args: Array<String>, pw: PrintWriter): Boolean {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
index fa696682de28..4ff1a5f1be31 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
@@ -171,6 +171,9 @@ class DesktopRepository(
/** Returns a list of all [Desk]s in the repository. */
private fun desksSequence(): Sequence<Desk> = desktopData.desksSequence()
+ /** Returns the number of desks in the given display. */
+ fun getNumberOfDesks(displayId: Int) = desktopData.getNumberOfDesks(displayId)
+
/** Adds [regionListener] to inform about changes to exclusion regions for all Desktop tasks. */
fun setExclusionRegionListener(regionListener: Consumer<Region>, executor: Executor) {
desktopGestureExclusionListener = regionListener
@@ -201,11 +204,11 @@ class DesktopRepository(
/** Adds the given desk under the given display. */
fun addDesk(displayId: Int, deskId: Int) {
- desktopData.getOrCreateDesk(displayId, deskId)
+ desktopData.createDesk(displayId, deskId)
}
/** Returns the default desk in the given display. */
- fun getDefaultDesk(displayId: Int): Int? = desktopData.getDefaultDesk(displayId)?.deskId
+ private fun getDefaultDesk(displayId: Int): Desk? = desktopData.getDefaultDesk(displayId)
/** Sets the given desk as the active one in the given display. */
fun setActiveDesk(displayId: Int, deskId: Int) {
@@ -229,15 +232,14 @@ class DesktopRepository(
* TODO: b/389960283 - add explicit [deskId] argument.
*/
private fun addActiveTask(displayId: Int, taskId: Int) {
- val activeDeskId =
- desktopData.getActiveDesk(displayId)?.deskId
- ?: error("Expected active desk in display: $displayId")
+ val activeDesk = desktopData.getDefaultDesk(displayId)
+ checkNotNull(activeDesk) { "Expected desk in display: $displayId" }
// Removes task if it is active on another desk excluding [activeDesk].
- removeActiveTask(taskId, excludedDeskId = activeDeskId)
+ removeActiveTask(taskId, excludedDeskId = activeDesk.deskId)
- if (desktopData.getOrCreateDesk(displayId, activeDeskId).activeTasks.add(taskId)) {
- logD("Adds active task=%d displayId=%d deskId=%d", taskId, displayId, activeDeskId)
+ if (activeDesk.activeTasks.add(taskId)) {
+ logD("Adds active task=%d displayId=%d deskId=%d", taskId, displayId, activeDesk.deskId)
updateActiveTasksListeners(displayId)
}
}
@@ -266,18 +268,23 @@ class DesktopRepository(
* TODO: b/389960283 - add explicit [deskId] argument.
*/
fun addClosingTask(displayId: Int, taskId: Int) {
- val activeDeskId =
- desktopData.getActiveDesk(displayId)?.deskId
+ val activeDesk =
+ desktopData.getActiveDesk(displayId)
?: error("Expected active desk in display: $displayId")
- if (desktopData.getOrCreateDesk(displayId, activeDeskId).closingTasks.add(taskId)) {
- logD("Added closing task=%d displayId=%d deskId=%d", taskId, displayId, activeDeskId)
+ if (activeDesk.closingTasks.add(taskId)) {
+ logD(
+ "Added closing task=%d displayId=%d deskId=%d",
+ taskId,
+ displayId,
+ activeDesk.deskId,
+ )
} else {
// If the task hasn't been removed from closing list after it disappeared.
logW(
"Task with taskId=%d displayId=%d deskId=%d is already closing",
taskId,
displayId,
- activeDeskId,
+ activeDesk.deskId,
)
}
}
@@ -323,7 +330,7 @@ class DesktopRepository(
/**
* Returns the active tasks in the given display's active desk.
*
- * TODO: b/389960283 - add explicit [deskId] argument.
+ * TODO: b/389960283 - migrate callers to [getActiveTaskIdsInDesk].
*/
@VisibleForTesting
fun getActiveTasks(displayId: Int): ArraySet<Int> =
@@ -332,19 +339,27 @@ class DesktopRepository(
/**
* Returns the minimized tasks in the given display's active desk.
*
- * TODO: b/389960283 - add explicit [deskId] argument.
+ * TODO: b/389960283 - migrate callers to [getMinimizedTaskIdsInDesk].
*/
fun getMinimizedTasks(displayId: Int): ArraySet<Int> =
ArraySet(desktopData.getActiveDesk(displayId)?.minimizedTasks)
+ @VisibleForTesting
+ fun getMinimizedTaskIdsInDesk(deskId: Int): ArraySet<Int> =
+ ArraySet(desktopData.getDesk(deskId)?.minimizedTasks)
+
/**
* Returns all active non-minimized tasks for [displayId] ordered from top to bottom.
*
- * TODO: b/389960283 - add explicit [deskId] argument.
+ * TODO: b/389960283 - migrate callers to [getExpandedTasksIdsInDeskOrdered].
*/
fun getExpandedTasksOrdered(displayId: Int): List<Int> =
getFreeformTasksInZOrder(displayId).filter { !isMinimizedTask(it) }
+ @VisibleForTesting
+ fun getExpandedTasksIdsInDeskOrdered(deskId: Int): List<Int> =
+ getFreeformTasksIdsInDeskInZOrder(deskId).filter { !isMinimizedTask(it) }
+
/**
* Returns the count of active non-minimized tasks for [displayId].
*
@@ -357,11 +372,15 @@ class DesktopRepository(
/**
* Returns a list of freeform tasks, ordered from top-bottom (top at index 0).
*
- * TODO: b/389960283 - add explicit [deskId] argument.
+ * TODO: b/389960283 - migrate callers to [getFreeformTasksIdsInDeskInZOrder].
*/
@VisibleForTesting
fun getFreeformTasksInZOrder(displayId: Int): ArrayList<Int> =
- ArrayList(desktopData.getActiveDesk(displayId)?.freeformTasksInZOrder ?: emptyList())
+ ArrayList(desktopData.getDefaultDesk(displayId)?.freeformTasksInZOrder ?: emptyList())
+
+ @VisibleForTesting
+ fun getFreeformTasksIdsInDeskInZOrder(deskId: Int): ArrayList<Int> =
+ ArrayList(desktopData.getDesk(deskId)?.freeformTasksInZOrder ?: emptyList())
/** Returns the tasks inside the given desk. */
fun getActiveTaskIdsInDesk(deskId: Int): Set<Int> =
@@ -401,8 +420,8 @@ class DesktopRepository(
}
val prevCount = getVisibleTaskCount(displayId)
if (isVisible) {
- desktopData.getActiveDesk(displayId)?.visibleTasks?.add(taskId)
- ?: error("Expected non-null active desk in display $displayId")
+ desktopData.getDefaultDesk(displayId)?.visibleTasks?.add(taskId)
+ ?: error("Expected non-null desk in display $displayId")
unminimizeTask(displayId, taskId)
} else {
desktopData.getActiveDesk(displayId)?.visibleTasks?.remove(taskId)
@@ -587,17 +606,15 @@ class DesktopRepository(
* TODO: b/389960283 - add explicit [deskId] argument.
*/
private fun addOrMoveFreeformTaskToTop(displayId: Int, taskId: Int) {
- val activeDesk =
- desktopData.getActiveDesk(displayId)
- ?: error("Expected a desk to be active in display: $displayId")
+ val desk = getDefaultDesk(displayId) ?: error("Expected a desk in display: $displayId")
logD(
"Add or move task to top: display=%d taskId=%d deskId=%d",
taskId,
displayId,
- activeDesk.deskId,
+ desk.deskId,
)
- desktopData.forAllDesks { _, desk -> desk.freeformTasksInZOrder.remove(taskId) }
- activeDesk.freeformTasksInZOrder.add(0, taskId)
+ desktopData.forAllDesks { _, desk1 -> desk1.freeformTasksInZOrder.remove(taskId) }
+ desk.freeformTasksInZOrder.add(0, taskId)
// Unminimize the task if it is minimized.
unminimizeTask(displayId, taskId)
if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) {
@@ -835,13 +852,8 @@ class DesktopRepository(
/** An interface for the desktop hierarchy's data managed by this repository. */
private interface DesktopData {
- /**
- * Returns the existing desk or creates a new entry if needed.
- *
- * TODO: 389787966 - consider removing this as it cannot be assumed a desk can be created in
- * all devices / form-factors.
- */
- fun getOrCreateDesk(displayId: Int, deskId: Int): Desk
+ /** Creates a desk record. */
+ fun createDesk(displayId: Int, deskId: Int)
/** Returns the desk with the given id, or null if it does not exist. */
fun getDesk(deskId: Int): Desk?
@@ -894,7 +906,8 @@ class DesktopRepository(
/**
* A [DesktopData] implementation that only supports one desk per display.
*
- * Internally, it reuses the displayId as that display's single desk's id.
+ * Internally, it reuses the displayId as that display's single desk's id. It also never truly
+ * "removes" a desk, it just clears its content.
*/
private class SingleDesktopData : DesktopData {
private val deskByDisplayId =
@@ -907,12 +920,16 @@ class DesktopRepository(
}
}
- override fun getOrCreateDesk(displayId: Int, deskId: Int): Desk {
- check(displayId == deskId)
- return deskByDisplayId.getOrCreate(displayId)
+ override fun createDesk(displayId: Int, deskId: Int) {
+ check(displayId == deskId) { "Display and desk ids must match" }
+ deskByDisplayId.getOrCreate(displayId)
}
- override fun getDesk(deskId: Int): Desk = getOrCreateDesk(deskId, deskId)
+ override fun getDesk(deskId: Int): Desk =
+ // TODO: b/362720497 - consider enforcing that the desk has been created before trying
+ // to use it. As of now, there are cases where a task may be created faster than a
+ // desk is, so just create it here if needed. See b/391984373.
+ deskByDisplayId.getOrCreate(deskId)
override fun getActiveDesk(displayId: Int): Desk {
// TODO: 389787966 - consider migrating to an "active" state instead of checking the
@@ -927,7 +944,7 @@ class DesktopRepository(
// existence of visible desktop windows, among other factors.
}
- override fun getDefaultDesk(displayId: Int): Desk = getOrCreateDesk(displayId, displayId)
+ override fun getDefaultDesk(displayId: Int): Desk = getDesk(deskId = displayId)
override fun getAllActiveDesks(): Set<Desk> =
deskByDisplayId.valueIterator().asSequence().toSet()
@@ -943,7 +960,7 @@ class DesktopRepository(
}
override fun forAllDesks(displayId: Int, consumer: (Desk) -> Unit) {
- consumer(getOrCreateDesk(displayId, displayId))
+ consumer(getDesk(deskId = displayId))
}
override fun desksSequence(): Sequence<Desk> = deskByDisplayId.valueIterator().asSequence()
@@ -962,16 +979,14 @@ class DesktopRepository(
private class MultiDesktopData : DesktopData {
private val desktopDisplays = SparseArray<DesktopDisplay>()
- override fun getOrCreateDesk(displayId: Int, deskId: Int): Desk {
+ override fun createDesk(displayId: Int, deskId: Int) {
val display =
desktopDisplays[displayId]
?: DesktopDisplay(displayId).also { desktopDisplays[displayId] = it }
- val desk =
- display.orderedDesks.find { desk -> desk.deskId == deskId }
- ?: Desk(deskId = deskId, displayId = displayId).also {
- display.orderedDesks.add(it)
- }
- return desk
+ check(display.orderedDesks.none { desk -> desk.deskId == deskId }) {
+ "Attempting to create desk#$deskId that already exists in display#$displayId"
+ }
+ display.orderedDesks.add(Desk(deskId = deskId, displayId = displayId))
}
override fun getDesk(deskId: Int): Desk? {
@@ -999,7 +1014,8 @@ class DesktopRepository(
override fun getDefaultDesk(displayId: Int): Desk? {
val display = desktopDisplays[displayId] ?: return null
- return display.orderedDesks.firstOrNull()
+ return display.orderedDesks.find { it.deskId == display.activeDeskId }
+ ?: display.orderedDesks.firstOrNull()
}
override fun getAllActiveDesks(): Set<Desk> {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 3ae553596631..5b206dedee49 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -16,6 +16,7 @@
package com.android.wm.shell.desktopmode
+import android.annotation.UserIdInt
import android.app.ActivityManager
import android.app.ActivityManager.RunningTaskInfo
import android.app.ActivityOptions
@@ -55,6 +56,7 @@ import android.view.WindowManager.TRANSIT_TO_FRONT
import android.widget.Toast
import android.window.DesktopModeFlags
import android.window.DesktopModeFlags.DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE
+import android.window.DesktopModeFlags.ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER
import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
import android.window.DesktopModeFlags.ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS
import android.window.RemoteTransition
@@ -85,8 +87,7 @@ import com.android.wm.shell.common.RemoteCallable
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.common.SingleInstanceRemoteListener
import com.android.wm.shell.common.SyncTransactionQueue
-import com.android.wm.shell.compatui.isTopActivityExemptFromDesktopWindowing
-import com.android.wm.shell.compatui.isTransparentTask
+import com.android.wm.shell.common.UserProfileContexts
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.InputMethod
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.MinimizeReason
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger
@@ -102,6 +103,8 @@ import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler.FULLSCR
import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction
import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider
import com.android.wm.shell.desktopmode.minimize.DesktopWindowLimitRemoteHandler
+import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer
+import com.android.wm.shell.desktopmode.multidesks.OnDeskRemovedListener
import com.android.wm.shell.draganddrop.DragAndDropController
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
@@ -113,6 +116,7 @@ import com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_ST
import com.android.wm.shell.shared.TransitionUtil
import com.android.wm.shell.shared.annotations.ExternalThread
import com.android.wm.shell.shared.annotations.ShellMainThread
+import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.DESKTOP_DENSITY_OVERRIDE
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.useDesktopOverrideDensity
@@ -180,6 +184,9 @@ class DesktopTasksController(
private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider,
private val bubbleController: Optional<BubbleController>,
private val overviewToDesktopTransitionObserver: OverviewToDesktopTransitionObserver,
+ private val desksOrganizer: DesksOrganizer,
+ private val userProfileContexts: UserProfileContexts,
+ private val desktopModeCompatPolicy: DesktopModeCompatPolicy,
) :
RemoteCallable<DesktopTasksController>,
Transitions.TransitionHandler,
@@ -232,6 +239,9 @@ class DesktopTasksController(
// Used to prevent handleRequest from moving the new fullscreen task to freeform.
private var dragAndDropFullscreenCookie: Binder? = null
+ // A listener that is invoked after a desk has been remove from the system. */
+ var onDeskRemovedListener: OnDeskRemovedListener? = null
+
init {
desktopMode = DesktopModeImpl()
if (DesktopModeStatus.canEnterDesktopMode(context)) {
@@ -415,6 +425,18 @@ class DesktopTasksController(
return isFreeformDisplay
}
+ /** Creates a new desk in the given display. */
+ fun createDesk(displayId: Int) {
+ if (Flags.enableMultipleDesktopsBackend()) {
+ desksOrganizer.createDesk(displayId) { deskId ->
+ taskRepository.addDesk(displayId = displayId, deskId = deskId)
+ }
+ } else {
+ // In single-desk, the desk reuses the display id.
+ taskRepository.addDesk(displayId = displayId, deskId = displayId)
+ }
+ }
+
/** Moves task to desktop mode if task is running, else launches it in desktop mode. */
@JvmOverloads
fun moveTaskToDesktop(
@@ -496,10 +518,7 @@ class DesktopTasksController(
remoteTransition: RemoteTransition? = null,
callback: IMoveToDesktopCallback? = null,
) {
- if (
- DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue() &&
- isTopActivityExemptFromDesktopWindowing(context, task)
- ) {
+ if (desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(task)) {
logW("Cannot enter desktop for taskId %d, ineligible top activity found", task.taskId)
return
}
@@ -1468,6 +1487,7 @@ class DesktopTasksController(
}
private fun addLaunchHomePendingIntent(wct: WindowContainerTransaction, displayId: Int) {
+ val userHandle = UserHandle.of(userId)
val launchHomeIntent =
Intent(Intent.ACTION_MAIN).apply {
if (displayId != DEFAULT_DISPLAY) {
@@ -1483,18 +1503,20 @@ class DesktopTasksController(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
}
val pendingIntent =
- PendingIntent.getActivity(
+ PendingIntent.getActivityAsUser(
context,
- /* requestCode = */ 0,
+ /* requestCode= */ 0,
launchHomeIntent,
PendingIntent.FLAG_IMMUTABLE,
+ /* options= */ null,
+ userHandle,
)
wct.sendPendingIntent(pendingIntent, launchHomeIntent, options.toBundle())
}
private fun addWallpaperActivity(displayId: Int, wct: WindowContainerTransaction) {
logV("addWallpaperActivity")
- if (Flags.enableDesktopWallpaperActivityForSystemUser()) {
+ if (ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER.isTrue()) {
val intent = Intent(context, DesktopWallpaperActivity::class.java)
if (
desktopWallpaperActivityTokenProvider.getToken(displayId) == null &&
@@ -1557,7 +1579,7 @@ class DesktopTasksController(
private fun removeWallpaperActivity(wct: WindowContainerTransaction, displayId: Int) {
desktopWallpaperActivityTokenProvider.getToken(displayId)?.let { token ->
logV("removeWallpaperActivity")
- if (Flags.enableDesktopWallpaperActivityForSystemUser()) {
+ if (ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER.isTrue()) {
wct.reorder(token, /* onTop= */ false)
} else {
wct.removeTask(token)
@@ -1798,8 +1820,7 @@ class DesktopTasksController(
taskRepository.isActiveTask(triggerTask.taskId))
private fun isIncompatibleTask(task: RunningTaskInfo) =
- DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue() &&
- isTopActivityExemptFromDesktopWindowing(context, task)
+ desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(task)
private fun shouldHandleTaskClosing(request: TransitionRequestInfo): Boolean =
ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue() &&
@@ -1841,7 +1862,9 @@ class DesktopTasksController(
// need updates in some cases.
val baseActivity = callingTaskInfo.baseActivity ?: return
val fillIn: Intent =
- context.packageManager.getLaunchIntentForPackage(baseActivity.packageName) ?: return
+ userProfileContexts[callingTaskInfo.userId]
+ ?.packageManager
+ ?.getLaunchIntentForPackage(baseActivity.packageName) ?: return
fillIn.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
val launchIntent =
PendingIntent.getActivity(
@@ -2068,11 +2091,11 @@ class DesktopTasksController(
*/
private fun handleIncompatibleTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? {
logV("handleIncompatibleTaskLaunch")
- if (!isDesktopModeShowing(task.displayId)) return null
+ if (!isDesktopModeShowing(task.displayId) && !forceEnterDesktop(task.displayId)) return null
// Only update task repository for transparent task.
if (
DesktopModeFlags.INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC
- .isTrue() && isTransparentTask(task)
+ .isTrue() && desktopModeCompatPolicy.isTransparentTask(task)
) {
taskRepository.setTopTransparentFullscreenTaskId(task.displayId, task.taskId)
}
@@ -2720,6 +2743,7 @@ class DesktopTasksController(
// TODO(b/358114479): Move this implementation into a separate class.
override fun onUnhandledDrag(
launchIntent: PendingIntent,
+ @UserIdInt userId: Int,
dragEvent: DragEvent,
onFinishCallback: Consumer<Boolean>,
): Boolean {
@@ -2728,8 +2752,10 @@ class DesktopTasksController(
// Not currently in desktop mode, ignore the drop
return false
}
+
+ // TODO:
val launchComponent = getComponent(launchIntent)
- if (!multiInstanceHelper.supportsMultiInstanceSplit(launchComponent)) {
+ if (!multiInstanceHelper.supportsMultiInstanceSplit(launchComponent, userId)) {
// TODO(b/320797628): Should only return early if there is an existing running task, and
// notify the user as well. But for now, just ignore the drop.
logV("Dropped intent does not support multi-instance")
@@ -2989,6 +3015,14 @@ class DesktopTasksController(
controller = null
}
+ override fun createDesk(displayId: Int) {
+ // TODO: b/362720497 - Implement this API.
+ }
+
+ override fun activateDesk(deskId: Int, remoteTransition: RemoteTransition?) {
+ // TODO: b/362720497 - Implement this API.
+ }
+
override fun showDesktopApps(displayId: Int, remoteTransition: RemoteTransition?) {
executeRemoteCallWithTaskPermission(controller, "showDesktopApps") { c ->
c.showDesktopApps(displayId, remoteTransition)
@@ -3016,17 +3050,6 @@ class DesktopTasksController(
)
}
- override fun getVisibleTaskCount(displayId: Int): Int {
- val result = IntArray(1)
- executeRemoteCallWithTaskPermission(
- controller,
- "visibleTaskCount",
- { controller -> result[0] = controller.visibleTaskCount(displayId) },
- /* blocking= */ true,
- )
- return result[0]
- }
-
override fun onDesktopSplitSelectAnimComplete(taskInfo: RunningTaskInfo) {
executeRemoteCallWithTaskPermission(controller, "onDesktopSplitSelectAnimComplete") { c
->
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
index 14c8429766cc..b3648699ed0b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
@@ -27,6 +27,7 @@ import android.view.WindowManager.TRANSIT_PIP
import android.view.WindowManager.TRANSIT_TO_BACK
import android.view.WindowManager.TRANSIT_TO_FRONT
import android.window.DesktopModeFlags
+import android.window.DesktopModeFlags.ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER
import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
import android.window.TransitionInfo
import android.window.WindowContainerTransaction
@@ -275,7 +276,7 @@ class DesktopTasksTransitionObserver(
desktopWallpaperActivityTokenProvider
.getToken(lastSeenTransitionToCloseWallpaper.displayId)
?.let { wallpaperActivityToken ->
- if (Flags.enableDesktopWallpaperActivityForSystemUser()) {
+ if (ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER.isTrue()) {
transitions.startTransition(
TRANSIT_TO_BACK,
WindowContainerTransaction()
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt
index 13576aa42737..a5ba6612bb1a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt
@@ -21,9 +21,9 @@ import android.content.Context
import android.content.pm.UserInfo
import android.os.UserManager
import android.util.SparseArray
+import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_HSUM
import androidx.core.util.forEach
import com.android.internal.protolog.ProtoLog
-import com.android.window.flags.Flags
import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository
import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
@@ -68,7 +68,7 @@ class DesktopUserRepositories(
if (DesktopModeStatus.canEnterDesktopMode(context)) {
shellInit.addInitCallback(::onInit, this)
}
- if (Flags.enableDesktopWindowingHsum()) {
+ if (ENABLE_DESKTOP_WINDOWING_HSUM.isTrue()) {
userIdToProfileIdsMap[userId] = userManager.getProfiles(userId).map { it.id }
}
}
@@ -80,7 +80,7 @@ class DesktopUserRepositories(
/** Returns [DesktopRepository] for the parent user id. */
fun getProfile(profileId: Int): DesktopRepository {
- if (Flags.enableDesktopWindowingHsum()) {
+ if (ENABLE_DESKTOP_WINDOWING_HSUM.isTrue()) {
for ((uid, profileIds) in userIdToProfileIdsMap) {
if (profileId in profileIds) {
return desktopRepoByUserId.getOrCreate(uid)
@@ -101,14 +101,14 @@ class DesktopUserRepositories(
override fun onUserChanged(newUserId: Int, userContext: Context) {
logD("onUserChanged previousUserId=%d, newUserId=%d", userId, newUserId)
userId = newUserId
- if (Flags.enableDesktopWindowingHsum()) {
+ if (ENABLE_DESKTOP_WINDOWING_HSUM.isTrue()) {
sanitizeUsers()
}
}
override fun onUserProfilesChanged(profiles: MutableList<UserInfo>) {
logD("onUserProfilesChanged profiles=%s", profiles.toString())
- if (Flags.enableDesktopWindowingHsum()) {
+ if (ENABLE_DESKTOP_WINDOWING_HSUM.isTrue()) {
// TODO(b/366397912): Remove all persisted profile data when the profile changes.
userIdToProfileIdsMap[userId] = profiles.map { it.id }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
index a135e4462150..44f7e16e98c3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
@@ -29,6 +29,11 @@ import com.android.wm.shell.desktopmode.IMoveToDesktopCallback;
* Interface that is exposed to remote callers to manipulate desktop mode features.
*/
interface IDesktopMode {
+ /** If possible, creates a new desk on the display whose ID is `displayId`. */
+ oneway void createDesk(int displayId);
+
+ /** Activates the desk whose ID is `deskId` on whatever display it currently exists on. */
+ oneway void activateDesk(int deskId, in RemoteTransition remoteTransition);
/** Show apps on the desktop on the given display */
void showDesktopApps(int displayId, in RemoteTransition remoteTransition);
@@ -48,9 +53,6 @@ interface IDesktopMode {
oneway void showDesktopApp(int taskId, in @nullable RemoteTransition remoteTransition,
in DesktopTaskToFrontReason toFrontReason);
- /** Get count of visible desktop tasks on the given display */
- int getVisibleTaskCount(int displayId);
-
/** Perform cleanup transactions after the animation to split select is complete */
oneway void onDesktopSplitSelectAnimComplete(in RunningTaskInfo taskInfo);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/compatui/SystemModalsTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/compatui/SystemModalsTransitionHandler.kt
index a428ce18a49e..224ff37a1dca 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/compatui/SystemModalsTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/compatui/SystemModalsTransitionHandler.kt
@@ -17,6 +17,7 @@
package com.android.wm.shell.desktopmode.compatui
import android.animation.ValueAnimator
+import android.app.ActivityManager.RunningTaskInfo
import android.content.Context
import android.os.IBinder
import android.view.Display.DEFAULT_DISPLAY
@@ -28,13 +29,14 @@ import androidx.core.animation.addListener
import com.android.app.animation.Interpolators
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.common.ShellExecutor
-import com.android.wm.shell.compatui.isTopActivityExemptFromDesktopWindowing
import com.android.wm.shell.desktopmode.DesktopUserRepositories
+import com.android.wm.shell.desktopmode.DesktopWallpaperActivity
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
import com.android.wm.shell.shared.TransitionUtil.isClosingMode
import com.android.wm.shell.shared.TransitionUtil.isClosingType
import com.android.wm.shell.shared.TransitionUtil.isOpeningMode
import com.android.wm.shell.shared.TransitionUtil.isOpeningType
+import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.transition.Transitions.TransitionHandler
@@ -47,6 +49,7 @@ class SystemModalsTransitionHandler(
private val shellInit: ShellInit,
private val transitions: Transitions,
private val desktopUserRepositories: DesktopUserRepositories,
+ private val desktopModeCompatPolicy: DesktopModeCompatPolicy,
) : TransitionHandler {
private val showingSystemModalsIds = mutableSetOf<Int>()
@@ -128,7 +131,7 @@ class SystemModalsTransitionHandler(
return@find false
}
val taskInfo = change.taskInfo ?: return@find false
- return@find isTopActivityExemptFromDesktopWindowing(context, taskInfo)
+ return@find isSystemModal(taskInfo)
}
private fun getClosingSystemModal(info: TransitionInfo): TransitionInfo.Change? =
@@ -137,10 +140,13 @@ class SystemModalsTransitionHandler(
return@find false
}
val taskInfo = change.taskInfo ?: return@find false
- return@find isTopActivityExemptFromDesktopWindowing(context, taskInfo) ||
- showingSystemModalsIds.contains(taskInfo.taskId)
+ return@find isSystemModal(taskInfo) || showingSystemModalsIds.contains(taskInfo.taskId)
}
+ private fun isSystemModal(taskInfo: RunningTaskInfo): Boolean =
+ !DesktopWallpaperActivity.isWallpaperTask(taskInfo) &&
+ desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(taskInfo)
+
private fun createAlphaAnimator(
transaction: SurfaceControl.Transaction,
leash: SurfaceControl,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt
index 5757c6afd196..b614b3f4d025 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt
@@ -22,7 +22,6 @@ import android.content.Context
import android.content.res.Resources
import android.graphics.Point
import android.os.SystemProperties
-import android.util.Slog
import com.android.window.flags.Flags
import com.android.wm.shell.R
import com.android.wm.shell.desktopmode.CaptionState
@@ -32,27 +31,17 @@ import com.android.wm.shell.shared.annotations.ShellBackgroundThread
import com.android.wm.shell.shared.annotations.ShellMainThread
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopMode
import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource
-import com.android.wm.shell.windowdecor.common.DecorThemeUtil
import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController
import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController.TooltipColorScheme
import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController.TooltipEducationViewConfig
-import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainCoroutineDispatcher
-import kotlinx.coroutines.TimeoutCancellationException
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.take
-import kotlinx.coroutines.flow.timeout
import kotlinx.coroutines.launch
/**
@@ -72,59 +61,75 @@ class AppHandleEducationController(
@ShellMainThread private val applicationCoroutineScope: CoroutineScope,
@ShellBackgroundThread private val backgroundDispatcher: MainCoroutineDispatcher,
) {
- private val decorThemeUtil = DecorThemeUtil(context)
private lateinit var openHandleMenuCallback: (Int) -> Unit
private lateinit var toDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit
+ private val onTertiaryFixedColor =
+ context.getColor(com.android.internal.R.color.materialColorOnTertiaryFixed)
+ private val tertiaryFixedColor =
+ context.getColor(com.android.internal.R.color.materialColorTertiaryFixed)
init {
runIfEducationFeatureEnabled {
+ // Coroutine block for the first hint that appears on a full-screen app's app handle to
+ // encourage users to open the app handle menu.
applicationCoroutineScope.launch {
- // Central block handling the app handle's educational flow end-to-end.
- isAppHandleHintViewedFlow()
- .flatMapLatest { isAppHandleHintViewed ->
- if (isAppHandleHintViewed) {
- // If the education is viewed then return emptyFlow() that completes
- // immediately.
- // This will help us to not listen to [captionHandleStateFlow] after the
- // education
- // has been viewed already.
- emptyFlow()
- } else {
- // Listen for changes to window decor's caption handle.
- windowDecorCaptionHandleRepository.captionStateFlow
- // Wait for few seconds before emitting the latest state.
- .debounce(APP_HANDLE_EDUCATION_DELAY_MILLIS)
- .filter { captionState ->
- captionState is CaptionState.AppHandle &&
- appHandleEducationFilter.shouldShowAppHandleEducation(
- captionState
- )
- }
- }
+ if (isAppHandleHintViewed()) return@launch
+ windowDecorCaptionHandleRepository.captionStateFlow
+ .debounce(APP_HANDLE_EDUCATION_DELAY_MILLIS)
+ .filter { captionState ->
+ captionState is CaptionState.AppHandle &&
+ !captionState.isHandleMenuExpanded &&
+ !isAppHandleHintViewed() &&
+ appHandleEducationFilter.shouldShowDesktopModeEducation(captionState)
}
+ .take(1)
.flowOn(backgroundDispatcher)
.collectLatest { captionState ->
- val tooltipColorScheme = tooltipColorScheme(captionState)
-
- showEducation(captionState, tooltipColorScheme)
- // After showing first tooltip, mark education as viewed
+ showEducation(captionState)
appHandleEducationDatastoreRepository
.updateAppHandleHintViewedTimestampMillis(true)
}
}
+ // Coroutine block for the hint that appears when an app handle is expanded to
+ // encourage users to enter desktop mode.
applicationCoroutineScope.launch {
- if (isAppHandleHintUsed()) return@launch
+ if (isEnterDesktopModeHintViewed()) return@launch
windowDecorCaptionHandleRepository.captionStateFlow
+ .debounce(ENTER_DESKTOP_MODE_EDUCATION_DELAY_MILLIS)
.filter { captionState ->
- captionState is CaptionState.AppHandle && captionState.isHandleMenuExpanded
+ captionState is CaptionState.AppHandle &&
+ captionState.isHandleMenuExpanded &&
+ !isEnterDesktopModeHintViewed() &&
+ appHandleEducationFilter.shouldShowDesktopModeEducation(captionState)
}
.take(1)
.flowOn(backgroundDispatcher)
- .collect {
- // If user expands app handle, mark user has used the app handle hint
+ .collectLatest { captionState ->
+ showWindowingImageButtonTooltip(captionState as CaptionState.AppHandle)
appHandleEducationDatastoreRepository
- .updateAppHandleHintUsedTimestampMillis(true)
+ .updateEnterDesktopModeHintViewedTimestampMillis(true)
+ }
+ }
+
+ // Coroutine block for the hint that appears on the window app header in freeform mode
+ // to let users know how to exit desktop mode.
+ applicationCoroutineScope.launch {
+ if (isExitDesktopModeHintViewed()) return@launch
+ windowDecorCaptionHandleRepository.captionStateFlow
+ .debounce(APP_HANDLE_EDUCATION_DELAY_MILLIS)
+ .filter { captionState ->
+ captionState is CaptionState.AppHeader &&
+ !captionState.isHeaderMenuExpanded &&
+ !isExitDesktopModeHintViewed() &&
+ appHandleEducationFilter.shouldShowDesktopModeEducation(captionState)
+ }
+ .take(1)
+ .flowOn(backgroundDispatcher)
+ .collectLatest { captionState ->
+ showExitWindowingTooltip(captionState as CaptionState.AppHeader)
+ appHandleEducationDatastoreRepository
+ .updateExitDesktopModeHintViewedTimestampMillis(true)
}
}
}
@@ -135,7 +140,7 @@ class AppHandleEducationController(
block()
}
- private fun showEducation(captionState: CaptionState, tooltipColorScheme: TooltipColorScheme) {
+ private fun showEducation(captionState: CaptionState) {
val appHandleBounds = (captionState as CaptionState.AppHandle).globalAppHandleBounds
val tooltipGlobalCoordinates =
Point(appHandleBounds.left + appHandleBounds.width() / 2, appHandleBounds.bottom)
@@ -145,21 +150,21 @@ class AppHandleEducationController(
val appHandleTooltipConfig =
TooltipEducationViewConfig(
tooltipViewLayout = R.layout.desktop_windowing_education_top_arrow_tooltip,
- tooltipColorScheme = tooltipColorScheme,
+ tooltipColorScheme =
+ TooltipColorScheme(
+ tertiaryFixedColor,
+ onTertiaryFixedColor,
+ onTertiaryFixedColor,
+ ),
tooltipViewGlobalCoordinates = tooltipGlobalCoordinates,
tooltipText = getString(R.string.windowing_app_handle_education_tooltip),
arrowDirection =
DesktopWindowingEducationTooltipController.TooltipArrowDirection.UP,
onEducationClickAction = {
- launchWithExceptionHandling {
- showWindowingImageButtonTooltip(tooltipColorScheme)
- }
openHandleMenuCallback(captionState.runningTaskInfo.taskId)
},
onDismissAction = {
- launchWithExceptionHandling {
- showWindowingImageButtonTooltip(tooltipColorScheme)
- }
+ // TODO: b/341320146 - Log previous tooltip was dismissed
},
)
@@ -170,7 +175,7 @@ class AppHandleEducationController(
}
/** Show tooltip that points to windowing image button in app handle menu */
- private suspend fun showWindowingImageButtonTooltip(tooltipColorScheme: TooltipColorScheme) {
+ private suspend fun showWindowingImageButtonTooltip(captionState: CaptionState.AppHandle) {
val appInfoPillHeight = getSize(R.dimen.desktop_mode_handle_menu_app_info_pill_height)
val windowingOptionPillHeight =
getSize(R.dimen.desktop_mode_handle_menu_windowing_pill_height)
@@ -181,128 +186,81 @@ class AppHandleEducationController(
getSize(R.dimen.desktop_mode_handle_menu_margin_top) +
getSize(R.dimen.desktop_mode_handle_menu_pill_spacing_margin)
- windowDecorCaptionHandleRepository.captionStateFlow
- // After the first tooltip was dismissed, wait for 400 ms and see if the app handle menu
- // has been expanded.
- .timeout(APP_HANDLE_EDUCATION_TIMEOUT_MILLIS.milliseconds)
- .catchTimeoutAndLog {
- // TODO: b/341320146 - Log previous tooltip was dismissed
- }
- // Wait for few milliseconds before emitting the latest state.
- .debounce(APP_HANDLE_EDUCATION_DELAY_MILLIS)
- .filter { captionState ->
- // Filter out states when app handle is not visible or not expanded.
- captionState is CaptionState.AppHandle && captionState.isHandleMenuExpanded
- }
- // Before showing this tooltip, stop listening to further emissions to avoid
- // accidentally
- // showing the same tooltip on future emissions.
- .take(1)
- .flowOn(backgroundDispatcher)
- .collectLatest { captionState ->
- captionState as CaptionState.AppHandle
- val appHandleBounds = captionState.globalAppHandleBounds
- val tooltipGlobalCoordinates =
- Point(
- appHandleBounds.left + appHandleBounds.width() / 2 + appHandleMenuWidth / 2,
- appHandleBounds.top +
- appHandleMenuMargins +
- appInfoPillHeight +
- windowingOptionPillHeight / 2,
- )
- // Populate information important to inflate windowing image button education
- // tooltip.
- val windowingImageButtonTooltipConfig =
- TooltipEducationViewConfig(
- tooltipViewLayout = R.layout.desktop_windowing_education_left_arrow_tooltip,
- tooltipColorScheme = tooltipColorScheme,
- tooltipViewGlobalCoordinates = tooltipGlobalCoordinates,
- tooltipText =
- getString(
- R.string.windowing_desktop_mode_image_button_education_tooltip
- ),
- arrowDirection =
- DesktopWindowingEducationTooltipController.TooltipArrowDirection.LEFT,
- onEducationClickAction = {
- launchWithExceptionHandling {
- showExitWindowingTooltip(tooltipColorScheme)
- }
- toDesktopModeCallback(
- captionState.runningTaskInfo.taskId,
- DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON,
- )
- },
- onDismissAction = {
- launchWithExceptionHandling {
- showExitWindowingTooltip(tooltipColorScheme)
- }
- },
+ val appHandleBounds = captionState.globalAppHandleBounds
+ val tooltipGlobalCoordinates =
+ Point(
+ appHandleBounds.left + appHandleBounds.width() / 2 + appHandleMenuWidth / 2,
+ appHandleBounds.top +
+ appHandleMenuMargins +
+ appInfoPillHeight +
+ windowingOptionPillHeight / 2,
+ )
+ // Populate information important to inflate windowing image button education
+ // tooltip.
+ val windowingImageButtonTooltipConfig =
+ TooltipEducationViewConfig(
+ tooltipViewLayout = R.layout.desktop_windowing_education_left_arrow_tooltip,
+ tooltipColorScheme =
+ TooltipColorScheme(
+ tertiaryFixedColor,
+ onTertiaryFixedColor,
+ onTertiaryFixedColor,
+ ),
+ tooltipViewGlobalCoordinates = tooltipGlobalCoordinates,
+ tooltipText =
+ getString(R.string.windowing_desktop_mode_image_button_education_tooltip),
+ arrowDirection =
+ DesktopWindowingEducationTooltipController.TooltipArrowDirection.LEFT,
+ onEducationClickAction = {
+ toDesktopModeCallback(
+ captionState.runningTaskInfo.taskId,
+ DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON,
)
+ },
+ onDismissAction = {
+ // TODO: b/341320146 - Log previous tooltip was dismissed
+ },
+ )
- windowingEducationViewController.showEducationTooltip(
- taskId = captionState.runningTaskInfo.taskId,
- tooltipViewConfig = windowingImageButtonTooltipConfig,
- )
- }
+ windowingEducationViewController.showEducationTooltip(
+ taskId = captionState.runningTaskInfo.taskId,
+ tooltipViewConfig = windowingImageButtonTooltipConfig,
+ )
}
/** Show tooltip that points to app chip button and educates user on how to exit desktop mode */
- private suspend fun showExitWindowingTooltip(tooltipColorScheme: TooltipColorScheme) {
- windowDecorCaptionHandleRepository.captionStateFlow
- // After the previous tooltip was dismissed, wait for 400 ms and see if the user entered
- // desktop mode.
- .timeout(APP_HANDLE_EDUCATION_TIMEOUT_MILLIS.milliseconds)
- .catchTimeoutAndLog {
- // TODO: b/341320146 - Log previous tooltip was dismissed
- }
- // Wait for few milliseconds before emitting the latest state.
- .debounce(APP_HANDLE_EDUCATION_DELAY_MILLIS)
- .filter { captionState ->
- // Filter out states when app header is not visible or expanded.
- captionState is CaptionState.AppHeader && !captionState.isHeaderMenuExpanded
- }
- // Before showing this tooltip, stop listening to further emissions to avoid
- // accidentally
- // showing the same tooltip on future emissions.
- .take(1)
- .flowOn(backgroundDispatcher)
- .collectLatest { captionState ->
- captionState as CaptionState.AppHeader
- val globalAppChipBounds = captionState.globalAppChipBounds
- val tooltipGlobalCoordinates =
- Point(
- globalAppChipBounds.right,
- globalAppChipBounds.top + globalAppChipBounds.height() / 2,
- )
- // Populate information important to inflate exit desktop mode education tooltip.
- val exitWindowingTooltipConfig =
- TooltipEducationViewConfig(
- tooltipViewLayout = R.layout.desktop_windowing_education_left_arrow_tooltip,
- tooltipColorScheme = tooltipColorScheme,
- tooltipViewGlobalCoordinates = tooltipGlobalCoordinates,
- tooltipText =
- getString(R.string.windowing_desktop_mode_exit_education_tooltip),
- arrowDirection =
- DesktopWindowingEducationTooltipController.TooltipArrowDirection.LEFT,
- onDismissAction = {},
- onEducationClickAction = {
- openHandleMenuCallback(captionState.runningTaskInfo.taskId)
- },
- )
- windowingEducationViewController.showEducationTooltip(
- taskId = captionState.runningTaskInfo.taskId,
- tooltipViewConfig = exitWindowingTooltipConfig,
- )
- }
- }
-
- private fun tooltipColorScheme(captionState: CaptionState): TooltipColorScheme {
- val onTertiaryFixed =
- context.getColor(com.android.internal.R.color.materialColorOnTertiaryFixed)
- val tertiaryFixed =
- context.getColor(com.android.internal.R.color.materialColorTertiaryFixed)
-
- return TooltipColorScheme(tertiaryFixed, onTertiaryFixed, onTertiaryFixed)
+ private suspend fun showExitWindowingTooltip(captionState: CaptionState.AppHeader) {
+ val globalAppChipBounds = captionState.globalAppChipBounds
+ val tooltipGlobalCoordinates =
+ Point(
+ globalAppChipBounds.right,
+ globalAppChipBounds.top + globalAppChipBounds.height() / 2,
+ )
+ // Populate information important to inflate exit desktop mode education tooltip.
+ val exitWindowingTooltipConfig =
+ TooltipEducationViewConfig(
+ tooltipViewLayout = R.layout.desktop_windowing_education_left_arrow_tooltip,
+ tooltipColorScheme =
+ TooltipColorScheme(
+ tertiaryFixedColor,
+ onTertiaryFixedColor,
+ onTertiaryFixedColor,
+ ),
+ tooltipViewGlobalCoordinates = tooltipGlobalCoordinates,
+ tooltipText = getString(R.string.windowing_desktop_mode_exit_education_tooltip),
+ arrowDirection =
+ DesktopWindowingEducationTooltipController.TooltipArrowDirection.LEFT,
+ onDismissAction = {
+ // TODO: b/341320146 - Log previous tooltip was dismissed
+ },
+ onEducationClickAction = {
+ openHandleMenuCallback(captionState.runningTaskInfo.taskId)
+ },
+ )
+ windowingEducationViewController.showEducationTooltip(
+ taskId = captionState.runningTaskInfo.taskId,
+ tooltipViewConfig = exitWindowingTooltipConfig,
+ )
}
/**
@@ -319,43 +277,20 @@ class AppHandleEducationController(
this.toDesktopModeCallback = toDesktopModeCallback
}
- private inline fun <T> Flow<T>.catchTimeoutAndLog(crossinline block: () -> Unit) =
- catch { exception ->
- if (exception is TimeoutCancellationException) block() else throw exception
- }
-
- private fun launchWithExceptionHandling(block: suspend () -> Unit) =
- applicationCoroutineScope.launch {
- try {
- block()
- } catch (e: Throwable) {
- Slog.e(TAG, "Error: ", e)
- }
- }
+ private suspend fun isAppHandleHintViewed(): Boolean =
+ appHandleEducationDatastoreRepository.dataStoreFlow
+ .first()
+ .hasAppHandleHintViewedTimestampMillis() && !FORCE_SHOW_DESKTOP_MODE_EDUCATION
- /**
- * Listens to the changes to [WindowingEducationProto#hasAppHandleHintViewedTimestampMillis()]
- * in datastore proto object.
- *
- * If [SHOULD_OVERRIDE_EDUCATION_CONDITIONS] is true, this flow will always emit false. That
- * means it will always emit app handle hint has not been viewed yet.
- */
- private fun isAppHandleHintViewedFlow(): Flow<Boolean> =
+ private suspend fun isEnterDesktopModeHintViewed(): Boolean =
appHandleEducationDatastoreRepository.dataStoreFlow
- .map { preferences ->
- preferences.hasAppHandleHintViewedTimestampMillis() &&
- !SHOULD_OVERRIDE_EDUCATION_CONDITIONS
- }
- .distinctUntilChanged()
+ .first()
+ .hasEnterDesktopModeHintViewedTimestampMillis() && !FORCE_SHOW_DESKTOP_MODE_EDUCATION
- /**
- * Listens to the changes to [WindowingEducationProto#hasAppHandleHintUsedTimestampMillis()] in
- * datastore proto object.
- */
- private suspend fun isAppHandleHintUsed(): Boolean =
+ private suspend fun isExitDesktopModeHintViewed(): Boolean =
appHandleEducationDatastoreRepository.dataStoreFlow
.first()
- .hasAppHandleHintUsedTimestampMillis()
+ .hasExitDesktopModeHintViewedTimestampMillis() && !FORCE_SHOW_DESKTOP_MODE_EDUCATION
private fun getSize(@DimenRes resourceId: Int): Int {
if (resourceId == Resources.ID_NULL) return 0
@@ -369,13 +304,17 @@ class AppHandleEducationController(
val APP_HANDLE_EDUCATION_DELAY_MILLIS: Long
get() = SystemProperties.getLong("persist.windowing_app_handle_education_delay", 3000L)
- val APP_HANDLE_EDUCATION_TIMEOUT_MILLIS: Long
- get() = SystemProperties.getLong("persist.windowing_app_handle_education_timeout", 400L)
+ val ENTER_DESKTOP_MODE_EDUCATION_DELAY_MILLIS: Long
+ get() =
+ SystemProperties.getLong(
+ "persist.windowing_enter_desktop_mode_education_timeout",
+ 400L,
+ )
- val SHOULD_OVERRIDE_EDUCATION_CONDITIONS: Boolean
+ val FORCE_SHOW_DESKTOP_MODE_EDUCATION: Boolean
get() =
SystemProperties.getBoolean(
- "persist.desktop_windowing_app_handle_education_override_conditions",
+ "persist.windowing_force_show_desktop_mode_education",
false,
)
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilter.kt
index 9990846fc92e..4d219b5544aa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilter.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilter.kt
@@ -17,13 +17,14 @@
package com.android.wm.shell.desktopmode.education
import android.annotation.IntegerRes
+import android.app.ActivityManager.RunningTaskInfo
import android.app.usage.UsageStatsManager
import android.content.Context
import android.os.SystemClock
import android.provider.Settings.Secure
import com.android.wm.shell.R
import com.android.wm.shell.desktopmode.CaptionState
-import com.android.wm.shell.desktopmode.education.AppHandleEducationController.Companion.SHOULD_OVERRIDE_EDUCATION_CONDITIONS
+import com.android.wm.shell.desktopmode.education.AppHandleEducationController.Companion.FORCE_SHOW_DESKTOP_MODE_EDUCATION
import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository
import com.android.wm.shell.desktopmode.education.data.WindowingEducationProto
import java.time.Duration
@@ -37,26 +38,28 @@ class AppHandleEducationFilter(
private val usageStatsManager =
context.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager
+ suspend fun shouldShowDesktopModeEducation(captionState: CaptionState.AppHeader): Boolean =
+ shouldShowDesktopModeEducation(captionState.runningTaskInfo)
+
+ suspend fun shouldShowDesktopModeEducation(captionState: CaptionState.AppHandle): Boolean =
+ shouldShowDesktopModeEducation(captionState.runningTaskInfo)
+
/**
- * Returns true if conditions to show app handle education are met, returns false otherwise.
+ * Returns true if conditions to show app handle, enter desktop mode and exit desktop mode
+ * education are met based on the app info and usage, returns false otherwise.
*
- * If [SHOULD_OVERRIDE_EDUCATION_CONDITIONS] is true, this method will always return
- * ![captionState.isHandleMenuExpanded].
+ * If [FORCE_SHOW_DESKTOP_MODE_EDUCATION] is true, this method will always return true.
*/
- suspend fun shouldShowAppHandleEducation(captionState: CaptionState): Boolean {
- if ((captionState as CaptionState.AppHandle).isHandleMenuExpanded) return false
- if (SHOULD_OVERRIDE_EDUCATION_CONDITIONS) return true
+ private suspend fun shouldShowDesktopModeEducation(taskInfo: RunningTaskInfo): Boolean {
+ if (FORCE_SHOW_DESKTOP_MODE_EDUCATION) return true
- val focusAppPackageName =
- captionState.runningTaskInfo.topActivityInfo?.packageName ?: return false
+ val focusAppPackageName = taskInfo.topActivityInfo?.packageName ?: return false
val windowingEducationProto =
appHandleEducationDatastoreRepository.windowingEducationProto()
return isFocusAppInAllowlist(focusAppPackageName) &&
!isOtherEducationShowing() &&
hasSufficientTimeSinceSetup() &&
- !isAppHandleHintViewedBefore(windowingEducationProto) &&
- !isAppHandleHintUsedBefore(windowingEducationProto) &&
hasMinAppUsage(windowingEducationProto, focusAppPackageName)
}
@@ -79,14 +82,6 @@ class AppHandleEducationFilter(
R.integer.desktop_windowing_education_required_time_since_setup_seconds
)
- private fun isAppHandleHintViewedBefore(
- windowingEducationProto: WindowingEducationProto
- ): Boolean = windowingEducationProto.hasAppHandleHintViewedTimestampMillis()
-
- private fun isAppHandleHintUsedBefore(
- windowingEducationProto: WindowingEducationProto
- ): Boolean = windowingEducationProto.hasAppHandleHintUsedTimestampMillis()
-
private suspend fun hasMinAppUsage(
windowingEducationProto: WindowingEducationProto,
focusAppPackageName: String,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt
index 3e120b09a0b6..d061e03b9be5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt
@@ -91,6 +91,40 @@ constructor(private val dataStore: DataStore<WindowingEducationProto>) {
}
/**
+ * Updates [WindowingEducationProto.enterDesktopModeHintViewedTimestampMillis_] field in
+ * datastore with current timestamp if [isViewed] is true, if not then clears the field.
+ */
+ suspend fun updateEnterDesktopModeHintViewedTimestampMillis(isViewed: Boolean) {
+ dataStore.updateData { preferences ->
+ if (isViewed) {
+ preferences
+ .toBuilder()
+ .setEnterDesktopModeHintViewedTimestampMillis(System.currentTimeMillis())
+ .build()
+ } else {
+ preferences.toBuilder().clearEnterDesktopModeHintViewedTimestampMillis().build()
+ }
+ }
+ }
+
+ /**
+ * Updates [WindowingEducationProto.exitDesktopModeHintViewedTimestampMillis_] field in
+ * datastore with current timestamp if [isViewed] is true, if not then clears the field.
+ */
+ suspend fun updateExitDesktopModeHintViewedTimestampMillis(isViewed: Boolean) {
+ dataStore.updateData { preferences ->
+ if (isViewed) {
+ preferences
+ .toBuilder()
+ .setExitDesktopModeHintViewedTimestampMillis(System.currentTimeMillis())
+ .build()
+ } else {
+ preferences.toBuilder().clearExitDesktopModeHintViewedTimestampMillis().build()
+ }
+ }
+ }
+
+ /**
* Updates [WindowingEducationProto.appHandleHintUsedTimestampMillis_] field in datastore with
* current timestamp if [isViewed] is true, if not then clears the field.
*/
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt
new file mode 100644
index 000000000000..5cbb59fbf323
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.desktopmode.multidesks
+
+import android.app.ActivityManager
+import android.window.TransitionInfo
+import android.window.WindowContainerTransaction
+
+/** An organizer of desk containers in which to host child desktop windows. */
+interface DesksOrganizer {
+ /** Creates a new desk container in the given display. */
+ fun createDesk(displayId: Int, callback: OnCreateCallback)
+
+ /** Activates the given desk, making it visible in its display. */
+ fun activateDesk(wct: WindowContainerTransaction, deskId: Int)
+
+ /** Removes the given desk and its desktop windows. */
+ fun removeDesk(wct: WindowContainerTransaction, deskId: Int)
+
+ /** Moves the given task to the given desk. */
+ fun moveTaskToDesk(
+ wct: WindowContainerTransaction,
+ deskId: Int,
+ task: ActivityManager.RunningTaskInfo,
+ )
+
+ /**
+ * Returns the desk id in which the task in the given change is located at the end of a
+ * transition, if any.
+ */
+ fun getDeskAtEnd(change: TransitionInfo.Change): Int?
+
+ /** A callback that is invoked when the desk container is created. */
+ fun interface OnCreateCallback {
+ /** Calls back when the [deskId] has been created. */
+ fun onCreated(deskId: Int)
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/OnDeskRemovedListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/OnDeskRemovedListener.kt
new file mode 100644
index 000000000000..452ddb1ff8fb
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/OnDeskRemovedListener.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.desktopmode.multidesks
+
+/** A listener for removals of desks. */
+fun interface OnDeskRemovedListener {
+ /** Called when a desk has been removed from the system. */
+ fun onDeskRemoved(lastDisplayId: Int, deskId: Int)
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt
new file mode 100644
index 000000000000..79c48c5e9594
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.desktopmode.multidesks
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
+import android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
+import android.util.SparseArray
+import android.view.SurfaceControl
+import android.window.TransitionInfo
+import android.window.WindowContainerTransaction
+import androidx.core.util.forEach
+import com.android.internal.annotations.VisibleForTesting
+import com.android.internal.protolog.ProtoLog
+import com.android.window.flags.Flags
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer.OnCreateCallback
+import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
+import com.android.wm.shell.sysui.ShellCommandHandler
+import com.android.wm.shell.sysui.ShellInit
+import java.io.PrintWriter
+
+/** A [DesksOrganizer] that uses root tasks as the container of each desk. */
+class RootTaskDesksOrganizer(
+ shellInit: ShellInit,
+ shellCommandHandler: ShellCommandHandler,
+ private val shellTaskOrganizer: ShellTaskOrganizer,
+) : DesksOrganizer, ShellTaskOrganizer.TaskListener {
+
+ private val deskCreateRequests = mutableListOf<CreateRequest>()
+ @VisibleForTesting val roots = SparseArray<DeskRoot>()
+
+ init {
+ if (Flags.enableMultipleDesktopsBackend()) {
+ shellInit.addInitCallback(
+ { shellCommandHandler.addDumpCallback(this::dump, this) },
+ this,
+ )
+ }
+ }
+
+ override fun createDesk(displayId: Int, callback: OnCreateCallback) {
+ logV("createDesk in display: %d", displayId)
+ deskCreateRequests += CreateRequest(displayId, callback)
+ shellTaskOrganizer.createRootTask(
+ displayId,
+ WINDOWING_MODE_FREEFORM,
+ /* listener = */ this,
+ /* removeWithTaskOrganizer = */ true,
+ )
+ }
+
+ override fun removeDesk(wct: WindowContainerTransaction, deskId: Int) {
+ logV("removeDesk %d", deskId)
+ val desk = checkNotNull(roots[deskId]) { "Root not found for desk: $deskId" }
+ wct.removeRootTask(desk.taskInfo.token)
+ }
+
+ override fun activateDesk(wct: WindowContainerTransaction, deskId: Int) {
+ logV("activateDesk %d", deskId)
+ val root = checkNotNull(roots[deskId]) { "Root not found for desk: $deskId" }
+ wct.reorder(root.taskInfo.token, /* onTop= */ true)
+ wct.setLaunchRoot(
+ /* container= */ root.taskInfo.token,
+ /* windowingModes= */ intArrayOf(WINDOWING_MODE_FREEFORM, WINDOWING_MODE_UNDEFINED),
+ /* activityTypes= */ intArrayOf(ACTIVITY_TYPE_UNDEFINED, ACTIVITY_TYPE_STANDARD),
+ )
+ }
+
+ override fun moveTaskToDesk(
+ wct: WindowContainerTransaction,
+ deskId: Int,
+ task: RunningTaskInfo,
+ ) {
+ val root = roots[deskId] ?: error("Root not found for desk: $deskId")
+ wct.reparent(task.token, root.taskInfo.token, /* onTop= */ true)
+ }
+
+ override fun getDeskAtEnd(change: TransitionInfo.Change): Int? =
+ change.taskInfo?.parentTaskId?.takeIf { it in roots }
+
+ override fun onTaskAppeared(taskInfo: RunningTaskInfo, leash: SurfaceControl) {
+ if (taskInfo.parentTaskId in roots) {
+ val deskId = taskInfo.parentTaskId
+ val taskId = taskInfo.taskId
+ logV("Task #$taskId appeared in desk #$deskId")
+ addChildToDesk(taskId = taskId, deskId = deskId)
+ return
+ }
+ val deskId = taskInfo.taskId
+ check(deskId !in roots) { "A root already exists for desk: $deskId" }
+ val request =
+ checkNotNull(deskCreateRequests.firstOrNull { it.displayId == taskInfo.displayId }) {
+ "Task ${taskInfo.taskId} appeared without pending create request"
+ }
+ logV("Desk #$deskId appeared")
+ roots[deskId] = DeskRoot(deskId, taskInfo, leash)
+ deskCreateRequests.remove(request)
+ request.onCreateCallback.onCreated(deskId)
+ }
+
+ override fun onTaskInfoChanged(taskInfo: RunningTaskInfo) {
+ if (roots.contains(taskInfo.taskId)) {
+ val deskId = taskInfo.taskId
+ roots[deskId] = roots[deskId].copy(taskInfo = taskInfo)
+ }
+ }
+
+ override fun onTaskVanished(taskInfo: RunningTaskInfo) {
+ if (roots.contains(taskInfo.taskId)) {
+ val deskId = taskInfo.taskId
+ val deskRoot = roots[deskId]
+ // Use the last saved taskInfo to obtain the displayId. Using the local one here will
+ // return -1 since the task is not unassociated with a display.
+ val displayId = deskRoot.taskInfo.displayId
+ logV("Desk #$deskId vanished from display #$displayId")
+ roots.remove(deskId)
+ return
+ }
+ // At this point, [parentTaskId] may be unset even if this is a task vanishing from a desk,
+ // so search through each root to remove this if it's a child.
+ roots.forEach { deskId, deskRoot ->
+ if (deskRoot.children.remove(taskInfo.taskId)) {
+ logV("Task #${taskInfo.taskId} vanished from desk #$deskId")
+ return
+ }
+ }
+ }
+
+ @VisibleForTesting
+ data class DeskRoot(
+ val deskId: Int,
+ val taskInfo: RunningTaskInfo,
+ val leash: SurfaceControl,
+ val children: MutableSet<Int> = mutableSetOf(),
+ )
+
+ override fun dump(pw: PrintWriter, prefix: String) {
+ val innerPrefix = "$prefix "
+ pw.println("$prefix$TAG")
+ pw.println("${innerPrefix}Desk Roots:")
+ roots.forEach { deskId, root ->
+ pw.println("$innerPrefix #$deskId visible=${root.taskInfo.isVisible}")
+ pw.println("$innerPrefix children=${root.children}")
+ }
+ }
+
+ private fun addChildToDesk(taskId: Int, deskId: Int) {
+ roots.forEach { _, deskRoot ->
+ if (deskRoot.deskId == deskId) {
+ deskRoot.children.add(taskId)
+ } else {
+ deskRoot.children.remove(taskId)
+ }
+ }
+ }
+
+ private data class CreateRequest(val displayId: Int, val onCreateCallback: OnCreateCallback)
+
+ private fun logV(msg: String, vararg arguments: Any?) {
+ ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
+ }
+
+ companion object {
+ private const val TAG = "RootTaskDesksOrganizer"
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt
index 58a49a035bb6..5a89451ffdbc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt
@@ -18,6 +18,7 @@ package com.android.wm.shell.desktopmode.persistence
import android.content.Context
import android.window.DesktopModeFlags
+import com.android.window.flags.Flags
import com.android.wm.shell.desktopmode.DesktopRepository
import com.android.wm.shell.desktopmode.DesktopUserRepositories
import com.android.wm.shell.shared.annotations.ShellMainThread
@@ -54,10 +55,22 @@ class DesktopRepositoryInitializerImpl(
DesktopModeStatus.getMaxTaskLimit(context).takeIf { it > 0 }
?: persistentDesktop.zOrderedTasksCount
var visibleTasksCount = 0
+ repository.addDesk(
+ displayId = persistentDesktop.displayId,
+ deskId =
+ if (Flags.enableMultipleDesktopsBackend()) {
+ persistentDesktop.desktopId
+ } else {
+ // When disabled, desk ids are always the display id.
+ persistentDesktop.displayId
+ },
+ )
persistentDesktop.zOrderedTasksList
// Reverse it so we initialize the repo from bottom to top.
.reversed()
.mapNotNull { taskId -> persistentDesktop.tasksByTaskIdMap[taskId] }
+ // TODO: b/362720497 - add tasks to their respective desk when multi-desk
+ // persistence is implemented.
.forEach { task ->
if (
task.desktopTaskState == DesktopTaskState.VISIBLE &&
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
index e24b2c5f0134..e8996bc03eeb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
@@ -31,6 +31,7 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMA
import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.ActivityTaskManager;
import android.app.PendingIntent;
@@ -125,6 +126,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll
* drag.
*/
default boolean onUnhandledDrag(@NonNull PendingIntent launchIntent,
+ @UserIdInt int userId,
@NonNull DragEvent dragEvent,
@NonNull Consumer<Boolean> onFinishCallback) {
return false;
@@ -444,8 +446,10 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll
return;
}
+ // TODO(b/391624027): Consider piping through launch intent user if needed later
+ final int userId = launchIntent.getCreatorUserHandle().getIdentifier();
final boolean handled = notifyListeners(
- l -> l.onUnhandledDrag(launchIntent, dragEvent, onFinishCallback));
+ l -> l.onUnhandledDrag(launchIntent, userId, dragEvent, onFinishCallback));
if (!handled) {
// Nobody handled this, we still have to notify WM
onFinishCallback.accept(false);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
index b38a853321a7..897e2d1601a5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -29,6 +29,7 @@ import android.window.DesktopModeFlags;
import com.android.internal.protolog.ProtoLog;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.LaunchAdjacentController;
+import com.android.wm.shell.desktopmode.DesktopModeLoggerTransitionObserver;
import com.android.wm.shell.desktopmode.DesktopRepository;
import com.android.wm.shell.desktopmode.DesktopTasksController;
import com.android.wm.shell.desktopmode.DesktopUserRepositories;
@@ -52,6 +53,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
private final ShellTaskOrganizer mShellTaskOrganizer;
private final Optional<DesktopUserRepositories> mDesktopUserRepositories;
private final Optional<DesktopTasksController> mDesktopTasksController;
+ private final DesktopModeLoggerTransitionObserver mDesktopModeLoggerTransitionObserver;
private final WindowDecorViewModel mWindowDecorationViewModel;
private final LaunchAdjacentController mLaunchAdjacentController;
private final Optional<TaskChangeListener> mTaskChangeListener;
@@ -64,6 +66,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
ShellTaskOrganizer shellTaskOrganizer,
Optional<DesktopUserRepositories> desktopUserRepositories,
Optional<DesktopTasksController> desktopTasksController,
+ DesktopModeLoggerTransitionObserver desktopModeLoggerTransitionObserver,
LaunchAdjacentController launchAdjacentController,
WindowDecorViewModel windowDecorationViewModel,
Optional<TaskChangeListener> taskChangeListener) {
@@ -72,6 +75,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
mWindowDecorationViewModel = windowDecorationViewModel;
mDesktopUserRepositories = desktopUserRepositories;
mDesktopTasksController = desktopTasksController;
+ mDesktopModeLoggerTransitionObserver = desktopModeLoggerTransitionObserver;
mLaunchAdjacentController = launchAdjacentController;
mTaskChangeListener = taskChangeListener;
if (shellInit != null) {
@@ -130,6 +134,9 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
repository.removeTask(taskInfo.displayId, taskInfo.taskId);
}
}
+ // TODO: b/367268649 - This listener shouldn't need to call the transition observer directly
+ // for logging once the logic in the observer is moved.
+ mDesktopModeLoggerTransitionObserver.onTaskVanished(taskInfo);
mWindowDecorationViewModel.onTaskVanished(taskInfo);
updateLaunchAdjacentController();
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java
index 44900ce1db8a..65099c2dfb9d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java
@@ -38,6 +38,7 @@ import com.android.internal.protolog.ProtoLog;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SystemWindows;
import com.android.wm.shell.common.pip.PipBoundsState;
+import com.android.wm.shell.common.pip.PipDisplayLayoutState;
import com.android.wm.shell.common.pip.PipMediaController;
import com.android.wm.shell.common.pip.PipMediaController.ActionListener;
import com.android.wm.shell.common.pip.PipMenuController;
@@ -121,6 +122,9 @@ public class PhonePipMenuController implements PipMenuController,
@NonNull
private final PipTransitionState mPipTransitionState;
+ @NonNull
+ private final PipDisplayLayoutState mPipDisplayLayoutState;
+
private SurfaceControl mLeash;
private ActionListener mMediaActionListener = new ActionListener() {
@@ -134,7 +138,8 @@ public class PhonePipMenuController implements PipMenuController,
public PhonePipMenuController(Context context, PipBoundsState pipBoundsState,
PipMediaController mediaController, SystemWindows systemWindows,
PipUiEventLogger pipUiEventLogger, PipTaskListener pipTaskListener,
- @NonNull PipTransitionState pipTransitionState, ShellExecutor mainExecutor,
+ @NonNull PipTransitionState pipTransitionState,
+ @NonNull PipDisplayLayoutState pipDisplayLayoutState, ShellExecutor mainExecutor,
Handler mainHandler) {
mContext = context;
mPipBoundsState = pipBoundsState;
@@ -142,6 +147,7 @@ public class PhonePipMenuController implements PipMenuController,
mSystemWindows = systemWindows;
mPipTaskListener = pipTaskListener;
mPipTransitionState = pipTransitionState;
+ mPipDisplayLayoutState = pipDisplayLayoutState;
mMainExecutor = mainExecutor;
mMainHandler = mainHandler;
mPipUiEventLogger = pipUiEventLogger;
@@ -218,7 +224,7 @@ public class PhonePipMenuController implements PipMenuController,
mSystemWindows.addView(mPipMenuView,
getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, 0 /* width */, 0 /* height */),
- 0, SHELL_ROOT_LAYER_PIP);
+ mPipDisplayLayoutState.getDisplayId(), SHELL_ROOT_LAYER_PIP);
setShellRootAccessibilityWindow();
// Make sure the initial actions are set
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
index b1984ccef4cb..99c9302edb75 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
@@ -19,6 +19,7 @@ package com.android.wm.shell.pip2.phone;
import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE;
+import static android.view.Display.DEFAULT_DISPLAY;
import android.annotation.NonNull;
import android.app.ActivityManager;
@@ -219,6 +220,7 @@ public class PipController implements ConfigurationChangeListener,
mPipDisplayLayoutState.setDisplayLayout(layout);
mDisplayController.addDisplayChangingController(this);
+ mDisplayController.addDisplayWindowListener(this);
mDisplayInsetsController.addInsetsChangedListener(mPipDisplayLayoutState.getDisplayId(),
new ImeListener(mDisplayController, mPipDisplayLayoutState.getDisplayId()) {
@Override
@@ -297,6 +299,22 @@ public class PipController implements ConfigurationChangeListener,
setDisplayLayout(mDisplayController.getDisplayLayout(displayId));
}
+ @Override
+ public void onDisplayRemoved(int displayId) {
+ // If PiP was active on an external display that is removed, clean up states and set
+ // {@link PipDisplayLayoutState} to DEFAULT_DISPLAY.
+ if (Flags.enableConnectedDisplaysPip() && mPipTransitionState.isInPip()
+ && displayId == mPipDisplayLayoutState.getDisplayId()
+ && displayId != DEFAULT_DISPLAY) {
+ mPipTransitionState.setState(PipTransitionState.EXITING_PIP);
+ mPipTransitionState.setState(PipTransitionState.EXITED_PIP);
+
+ mPipDisplayLayoutState.setDisplayId(DEFAULT_DISPLAY);
+ mPipDisplayLayoutState.setDisplayLayout(
+ mDisplayController.getDisplayLayout(DEFAULT_DISPLAY));
+ }
+ }
+
/**
* A callback for any observed transition that contains a display change in its
* {@link android.window.TransitionRequestInfo},
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java
index b3070f29c6e2..71697596afd3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java
@@ -23,6 +23,7 @@ import android.content.res.Resources;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.Rect;
+import android.view.Display;
import android.view.MotionEvent;
import android.view.SurfaceControl;
import android.view.View;
@@ -34,7 +35,9 @@ import androidx.annotation.NonNull;
import com.android.wm.shell.R;
import com.android.wm.shell.bubbles.DismissViewUtils;
+import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.pip.PipDisplayLayoutState;
import com.android.wm.shell.common.pip.PipUiEventLogger;
import com.android.wm.shell.shared.bubbles.DismissCircleView;
import com.android.wm.shell.shared.bubbles.DismissView;
@@ -50,6 +53,9 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen
/* The multiplier to apply scale the target size by when applying the magnetic field radius */
private static final float MAGNETIC_FIELD_RADIUS_MULTIPLIER = 1.25f;
+ /* The window type to apply to the display */
+ private static final int WINDOW_TYPE = WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
+
/**
* MagnetizedObject wrapper for PIP. This allows the magnetic target library to locate and move
* PIP.
@@ -84,16 +90,22 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen
private final Context mContext;
private final PipMotionHelper mMotionHelper;
private final PipUiEventLogger mPipUiEventLogger;
- private final WindowManager mWindowManager;
+ private WindowManager mWindowManager;
+ /** The display id for the display that is associated with mWindowManager. */
+ private int mWindowManagerDisplayId = -1;
+ private final PipDisplayLayoutState mPipDisplayLayoutState;
+ private final DisplayController mDisplayController;
private final ShellExecutor mMainExecutor;
public PipDismissTargetHandler(Context context, PipUiEventLogger pipUiEventLogger,
- PipMotionHelper motionHelper, ShellExecutor mainExecutor) {
+ PipMotionHelper motionHelper, PipDisplayLayoutState pipDisplayLayoutState,
+ DisplayController displayController, ShellExecutor mainExecutor) {
mContext = context;
mPipUiEventLogger = pipUiEventLogger;
mMotionHelper = motionHelper;
+ mPipDisplayLayoutState = pipDisplayLayoutState;
+ mDisplayController = displayController;
mMainExecutor = mainExecutor;
- mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
}
void init() {
@@ -240,6 +252,8 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen
/** Adds the magnetic target view to the WindowManager so it's ready to be animated in. */
public void createOrUpdateDismissTarget() {
+ getWindowManager();
+
if (mTargetViewContainer.getParent() == null) {
mTargetViewContainer.cancelAnimators();
@@ -262,7 +276,7 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen
WindowManager.LayoutParams.MATCH_PARENT,
height,
0, windowSize.y - height,
- WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
+ WINDOW_TYPE,
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
@@ -308,4 +322,16 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen
mWindowManager.removeViewImmediate(mTargetViewContainer);
}
}
+
+ /** Sets mWindowManager to WindowManager associated with the display where PiP is active on. */
+ private void getWindowManager() {
+ final int pipDisplayId = mPipDisplayLayoutState.getDisplayId();
+ if (mWindowManager != null && pipDisplayId == mWindowManagerDisplayId) {
+ return;
+ }
+ mWindowManagerDisplayId = pipDisplayId;
+ Display display = mDisplayController.getDisplay(mWindowManagerDisplayId);
+ Context uiContext = mContext.createWindowContext(display, WINDOW_TYPE, null);
+ mWindowManager = uiContext.getSystemService(WindowManager.class);
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipInputConsumer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipInputConsumer.java
index ffda56d89276..0a0ecffbea1f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipInputConsumer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipInputConsumer.java
@@ -16,8 +16,6 @@
package com.android.wm.shell.pip2.phone;
-import static android.view.Display.DEFAULT_DISPLAY;
-
import android.os.Binder;
import android.os.IBinder;
import android.os.Looper;
@@ -30,6 +28,7 @@ import android.view.InputEvent;
import com.android.internal.protolog.ProtoLog;
import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.pip.PipDisplayLayoutState;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import java.io.PrintWriter;
@@ -84,6 +83,7 @@ public class PipInputConsumer {
private final IWindowManager mWindowManager;
private final IBinder mToken;
private final String mName;
+ private final PipDisplayLayoutState mPipDisplayLayoutState;
private final ShellExecutor mMainExecutor;
private InputEventReceiver mInputEventReceiver;
@@ -94,10 +94,11 @@ public class PipInputConsumer {
* @param name the name corresponding to the input consumer that is defined in the system.
*/
public PipInputConsumer(IWindowManager windowManager, String name,
- ShellExecutor mainExecutor) {
+ PipDisplayLayoutState pipDisplayLayoutState, ShellExecutor mainExecutor) {
mWindowManager = windowManager;
mToken = new Binder();
mName = name;
+ mPipDisplayLayoutState = pipDisplayLayoutState;
mMainExecutor = mainExecutor;
}
@@ -138,9 +139,9 @@ public class PipInputConsumer {
}
final InputChannel inputChannel = new InputChannel();
try {
- // TODO(b/113087003): Support Picture-in-picture in multi-display.
- mWindowManager.destroyInputConsumer(mToken, DEFAULT_DISPLAY);
- mWindowManager.createInputConsumer(mToken, mName, DEFAULT_DISPLAY, inputChannel);
+ final int displayId = mPipDisplayLayoutState.getDisplayId();
+ mWindowManager.destroyInputConsumer(mToken, displayId);
+ mWindowManager.createInputConsumer(mToken, mName, displayId, inputChannel);
} catch (RemoteException e) {
ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: Failed to create input consumer, %s", TAG, e);
@@ -162,8 +163,7 @@ public class PipInputConsumer {
return;
}
try {
- // TODO(b/113087003): Support Picture-in-picture in multi-display.
- mWindowManager.destroyInputConsumer(mToken, DEFAULT_DISPLAY);
+ mWindowManager.destroyInputConsumer(mToken, mPipDisplayLayoutState.getDisplayId());
} catch (RemoteException e) {
ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: Failed to destroy input consumer, %s", TAG, e);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java
index d98be55f28e1..e4be3f60f86e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java
@@ -44,6 +44,7 @@ import com.android.wm.shell.R;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
import com.android.wm.shell.common.pip.PipBoundsState;
+import com.android.wm.shell.common.pip.PipDisplayLayoutState;
import com.android.wm.shell.common.pip.PipPerfHintController;
import com.android.wm.shell.common.pip.PipPinchResizingAlgorithm;
import com.android.wm.shell.common.pip.PipUiEventLogger;
@@ -70,9 +71,9 @@ public class PipResizeGestureHandler implements
private final PipScheduler mPipScheduler;
private final PipTransitionState mPipTransitionState;
private final PhonePipMenuController mPhonePipMenuController;
+ private final PipDisplayLayoutState mPipDisplayLayoutState;
private final PipUiEventLogger mPipUiEventLogger;
private final PipPinchResizingAlgorithm mPinchResizingAlgorithm;
- private final int mDisplayId;
private final ShellExecutor mMainExecutor;
private final PointF mDownPoint = new PointF();
@@ -120,10 +121,10 @@ public class PipResizeGestureHandler implements
PipTransitionState pipTransitionState,
PipUiEventLogger pipUiEventLogger,
PhonePipMenuController menuActivityController,
+ PipDisplayLayoutState pipDisplayLayoutState,
ShellExecutor mainExecutor,
@Nullable PipPerfHintController pipPerfHintController) {
mContext = context;
- mDisplayId = context.getDisplayId();
mMainExecutor = mainExecutor;
mPipPerfHintController = pipPerfHintController;
mPipBoundsAlgorithm = pipBoundsAlgorithm;
@@ -135,6 +136,7 @@ public class PipResizeGestureHandler implements
mPipTransitionState.addPipTransitionStateChangedListener(this);
mPhonePipMenuController = menuActivityController;
+ mPipDisplayLayoutState = pipDisplayLayoutState;
mPipUiEventLogger = pipUiEventLogger;
mPinchResizingAlgorithm = new PipPinchResizingAlgorithm();
}
@@ -197,7 +199,7 @@ public class PipResizeGestureHandler implements
if (mIsEnabled) {
// Register input event receiver
mInputMonitor = mContext.getSystemService(InputManager.class).monitorGestureInput(
- "pip-resize", mDisplayId);
+ "pip-resize", mPipDisplayLayoutState.getDisplayId());
try {
mMainExecutor.executeBlocking(() -> {
mInputEventReceiver = new PipResizeInputEventReceiver(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java
index fc3fbe299605..35cd1a2e681f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java
@@ -54,10 +54,12 @@ import android.view.accessibility.AccessibilityWindowInfo;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.ProtoLog;
import com.android.wm.shell.R;
+import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.FloatingContentCoordinator;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
import com.android.wm.shell.common.pip.PipBoundsState;
+import com.android.wm.shell.common.pip.PipDisplayLayoutState;
import com.android.wm.shell.common.pip.PipDoubleTapHelper;
import com.android.wm.shell.common.pip.PipPerfHintController;
import com.android.wm.shell.common.pip.PipUiEventLogger;
@@ -91,6 +93,7 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha
@NonNull private final PipTransitionState mPipTransitionState;
@NonNull private final PipScheduler mPipScheduler;
@NonNull private final SizeSpecSource mSizeSpecSource;
+ @NonNull private final PipDisplayLayoutState mPipDisplayLayoutState;
private final PipUiEventLogger mPipUiEventLogger;
private final PipDismissTargetHandler mPipDismissTargetHandler;
private final ShellExecutor mMainExecutor;
@@ -183,6 +186,8 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha
@NonNull PipTransitionState pipTransitionState,
@NonNull PipScheduler pipScheduler,
@NonNull SizeSpecSource sizeSpecSource,
+ @NonNull PipDisplayLayoutState pipDisplayLayoutState,
+ DisplayController displayController,
PipMotionHelper pipMotionHelper,
FloatingContentCoordinator floatingContentCoordinator,
PipUiEventLogger pipUiEventLogger,
@@ -200,6 +205,7 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha
mPipTransitionState.addPipTransitionStateChangedListener(this::onPipTransitionStateChanged);
mPipScheduler = pipScheduler;
mSizeSpecSource = sizeSpecSource;
+ mPipDisplayLayoutState = pipDisplayLayoutState;
mMenuController = menuController;
mPipUiEventLogger = pipUiEventLogger;
mFloatingContentCoordinator = floatingContentCoordinator;
@@ -208,7 +214,7 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha
mMotionHelper = pipMotionHelper;
mPipScheduler.setUpdateMovementBoundsRunnable(this::updateMovementBounds);
mPipDismissTargetHandler = new PipDismissTargetHandler(context, pipUiEventLogger,
- mMotionHelper, mainExecutor);
+ mMotionHelper, mPipDisplayLayoutState, displayController, mainExecutor);
mTouchState = new PipTouchState(ViewConfiguration.get(context),
() -> {
mMenuController.showMenuWithPossibleDelay(MENU_STATE_FULL,
@@ -220,8 +226,7 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha
mainExecutor);
mPipResizeGestureHandler = new PipResizeGestureHandler(context, pipBoundsAlgorithm,
pipBoundsState, mTouchState, mPipScheduler, mPipTransitionState, pipUiEventLogger,
- menuController, mainExecutor,
- mPipPerfHintController);
+ menuController, mPipDisplayLayoutState, mainExecutor, mPipPerfHintController);
mPipBoundsState.addOnAspectRatioChangedCallback(aspectRatio -> {
updateMinMaxSize(aspectRatio);
onAspectRatioChanged();
@@ -264,7 +269,7 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha
mPipDismissTargetHandler.init();
mPipInputConsumer = new PipInputConsumer(WindowManagerGlobal.getWindowManagerService(),
- INPUT_CONSUMER_PIP, mMainExecutor);
+ INPUT_CONSUMER_PIP, mPipDisplayLayoutState, mMainExecutor);
mPipInputConsumer.setInputListener(this::handleTouchEvent);
mPipInputConsumer.setRegistrationListener(this::onRegistrationChanged);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
index 272cb4372acf..03327bf463e3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
@@ -70,6 +70,7 @@ import com.android.wm.shell.desktopmode.DesktopRepository;
import com.android.wm.shell.desktopmode.DesktopUserRepositories;
import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider;
import com.android.wm.shell.pip.PipTransitionController;
+import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
import com.android.wm.shell.pip2.animation.PipAlphaAnimator;
import com.android.wm.shell.pip2.animation.PipEnterAnimator;
import com.android.wm.shell.pip2.animation.PipExpandAnimator;
@@ -115,6 +116,7 @@ public class PipTransition extends PipTransitionController implements
private final PipTransitionState mPipTransitionState;
private final PipDisplayLayoutState mPipDisplayLayoutState;
private final DisplayController mDisplayController;
+ private final PipSurfaceTransactionHelper mPipSurfaceTransactionHelper;
private final Optional<DesktopUserRepositories> mDesktopUserRepositoriesOptional;
private final Optional<DesktopWallpaperActivityTokenProvider>
mDesktopWallpaperActivityTokenProviderOptional;
@@ -171,6 +173,7 @@ public class PipTransition extends PipTransitionController implements
mPipTransitionState.addPipTransitionStateChangedListener(this);
mPipDisplayLayoutState = pipDisplayLayoutState;
mDisplayController = displayController;
+ mPipSurfaceTransactionHelper = new PipSurfaceTransactionHelper(mContext);
mDesktopUserRepositoriesOptional = desktopUserRepositoriesOptional;
mDesktopWallpaperActivityTokenProviderOptional =
desktopWallpaperActivityTokenProviderOptional;
@@ -343,6 +346,25 @@ public class PipTransition extends PipTransitionController implements
}
}
+ @Override
+ public boolean syncPipSurfaceState(@NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction) {
+ final TransitionInfo.Change pipChange = getPipChange(info);
+ if (pipChange == null) return false;
+
+ // add shadow and corner radii
+ final SurfaceControl leash = pipChange.getLeash();
+ final boolean isInPip = mPipTransitionState.isInPip();
+
+ mPipSurfaceTransactionHelper.round(startTransaction, leash, isInPip)
+ .shadow(startTransaction, leash, isInPip);
+ mPipSurfaceTransactionHelper.round(finishTransaction, leash, isInPip)
+ .shadow(finishTransaction, leash, isInPip);
+
+ return true;
+ }
+
//
// Animation schedulers and entry points
//
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index 2d4d458292ea..4f2e028a1df0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -311,6 +311,9 @@ public class RecentTasksController implements TaskStackListenerCallback,
public void onTaskAdded(RunningTaskInfo taskInfo) {
notifyRunningTaskAppeared(taskInfo);
+ if (!enableShellTopTaskTracking()) {
+ notifyRecentTasksChanged();
+ }
}
public void onTaskRemoved(RunningTaskInfo taskInfo) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
index afc6fee2eca3..55133780f517 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
@@ -222,7 +222,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
RecentsMixedHandler mixer = null;
Consumer<IBinder> setTransitionForMixer = null;
for (int i = 0; i < mMixers.size(); ++i) {
- setTransitionForMixer = mMixers.get(i).handleRecentsRequest(wct);
+ setTransitionForMixer = mMixers.get(i).handleRecentsRequest();
if (setTransitionForMixer != null) {
mixer = mMixers.get(i);
break;
@@ -1455,6 +1455,11 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
}
}
+ // Notify the mixers of the pending finish
+ for (int i = 0; i < mMixers.size(); ++i) {
+ mMixers.get(i).handleFinishRecents(returningToApp, wct, t);
+ }
+
if (Flags.enableRecentsBookendTransition()) {
if (!wct.isEmpty()) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
@@ -1653,15 +1658,22 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
*/
public interface RecentsMixedHandler extends Transitions.TransitionHandler {
/**
- * Called when a recents request comes in. The handler can add operations to outWCT. If
- * the handler wants to "accept" the transition, it should return a Consumer accepting the
- * IBinder for the transition. If not, it should return `null`.
+ * Called when a recents request comes in. If the handler wants to "accept" the transition,
+ * it should return a Consumer accepting the IBinder for the transition. If not, it should
+ * return `null`.
*
* If a mixed-handler accepts this recents, it will be the de-facto handler for this
* transition and is required to call the associated {@link #startAnimation},
* {@link #mergeAnimation}, and {@link #onTransitionConsumed} methods.
*/
@Nullable
- Consumer<IBinder> handleRecentsRequest(WindowContainerTransaction outWCT);
+ Consumer<IBinder> handleRecentsRequest();
+
+ /**
+ * Called when a recents transition has finished, with a WCT and SurfaceControl Transaction
+ * that can be used to add to any changes needed to restore the state.
+ */
+ void handleFinishRecents(boolean returnToApp, @NonNull WindowContainerTransaction finishWct,
+ @NonNull SurfaceControl.Transaction finishT);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index 99a89a6b884f..ae0159263364 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -649,11 +649,12 @@ public class SplitScreenController implements SplitDragPolicy.Starter,
@Nullable Bundle options, UserHandle user) {
if (options == null) options = new Bundle();
final ActivityOptions activityOptions = ActivityOptions.fromBundle(options);
+ final int userId = user.getIdentifier();
if (samePackage(packageName, getPackageName(reverseSplitPosition(position), null),
- user.getIdentifier(), getUserId(reverseSplitPosition(position), null))) {
+ userId, getUserId(reverseSplitPosition(position), null))) {
if (mMultiInstanceHelpher.supportsMultiInstanceSplit(
- getShortcutComponent(packageName, shortcutId, user, mLauncherApps))) {
+ getShortcutComponent(packageName, shortcutId, user, mLauncherApps), userId)) {
activityOptions.setApplyMultipleTaskFlagForShortcut(true);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
} else if (isSplitScreenVisible()) {
@@ -687,7 +688,8 @@ public class SplitScreenController implements SplitDragPolicy.Starter,
final int userId1 = shortcutInfo.getUserId();
final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer);
if (samePackage(packageName1, packageName2, userId1, userId2)) {
- if (mMultiInstanceHelpher.supportsMultiInstanceSplit(shortcutInfo.getActivity())) {
+ if (mMultiInstanceHelpher.supportsMultiInstanceSplit(shortcutInfo.getActivity(),
+ userId1)) {
activityOptions.setApplyMultipleTaskFlagForShortcut(true);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
} else {
@@ -735,7 +737,8 @@ public class SplitScreenController implements SplitDragPolicy.Starter,
final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer);
boolean setSecondIntentMultipleTask = false;
if (samePackage(packageName1, packageName2, userId1, userId2)) {
- if (mMultiInstanceHelpher.supportsMultiInstanceSplit(getComponent(pendingIntent))) {
+ if (mMultiInstanceHelpher.supportsMultiInstanceSplit(getComponent(pendingIntent),
+ userId1)) {
setSecondIntentMultipleTask = true;
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
} else {
@@ -775,7 +778,8 @@ public class SplitScreenController implements SplitDragPolicy.Starter,
? ActivityOptions.fromBundle(options2) : ActivityOptions.makeBasic();
boolean setSecondIntentMultipleTask = false;
if (samePackage(packageName1, packageName2, userId1, userId2)) {
- if (mMultiInstanceHelpher.supportsMultiInstanceSplit(getComponent(pendingIntent1))) {
+ if (mMultiInstanceHelpher.supportsMultiInstanceSplit(getComponent(pendingIntent1),
+ userId1)) {
fillInIntent1 = new Intent();
fillInIntent1.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
setSecondIntentMultipleTask = true;
@@ -858,7 +862,7 @@ public class SplitScreenController implements SplitDragPolicy.Starter,
return;
}
if (samePackage(packageName1, packageName2, userId1, userId2)) {
- if (mMultiInstanceHelpher.supportsMultiInstanceSplit(getComponent(intent))) {
+ if (mMultiInstanceHelpher.supportsMultiInstanceSplit(getComponent(intent), userId1)) {
// Flag with MULTIPLE_TASK if this is launching the same activity into both sides of
// the split and there is no reusable background task.
fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 511e426cc681..722494c05e32 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -129,6 +129,7 @@ import com.android.internal.logging.InstanceId;
import com.android.internal.policy.FoldLockSettingsObserver;
import com.android.internal.protolog.ProtoLog;
import com.android.launcher3.icons.IconProvider;
+import com.android.wm.shell.Flags;
import com.android.wm.shell.R;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTaskOrganizer;
@@ -3766,13 +3767,31 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mTaskOrganizer.applyTransaction(wct);
}
+ public void onRecentsInSplitAnimationFinishing(boolean returnToApp,
+ @NonNull WindowContainerTransaction finishWct,
+ @NonNull SurfaceControl.Transaction finishT) {
+ if (!Flags.enableRecentsBookendTransition()) {
+ // The non-bookend recents transition case will be handled by
+ // RecentsMixedTransition wrapping the finish callback and calling
+ // onRecentsInSplitAnimationFinish()
+ return;
+ }
+
+ onRecentsInSplitAnimationFinishInner(returnToApp, finishWct, finishT);
+ }
+
/** Call this when the recents animation during split-screen finishes. */
- public void onRecentsInSplitAnimationFinish(WindowContainerTransaction finishWct,
- SurfaceControl.Transaction finishT) {
- ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRecentsInSplitAnimationFinish");
- mPausingTasks.clear();
+ public void onRecentsInSplitAnimationFinish(@NonNull WindowContainerTransaction finishWct,
+ @NonNull SurfaceControl.Transaction finishT) {
+ if (Flags.enableRecentsBookendTransition()) {
+ // The bookend recents transition case will be handled by
+ // onRecentsInSplitAnimationFinishing above
+ return;
+ }
+
// Check if the recent transition is finished by returning to the current
// split, so we can restore the divider bar.
+ boolean returnToApp = false;
for (int i = 0; i < finishWct.getHierarchyOps().size(); ++i) {
final WindowContainerTransaction.HierarchyOp op =
finishWct.getHierarchyOps().get(i);
@@ -3787,13 +3806,26 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
if (op.getType() == HIERARCHY_OP_TYPE_REORDER && op.getToTop()
&& anyStageContainsContainer) {
- updateSurfaceBounds(mSplitLayout, finishT,
- false /* applyResizingOffset */);
- finishT.reparent(mSplitLayout.getDividerLeash(), mRootTaskLeash);
- setDividerVisibility(true, finishT);
- return;
+ returnToApp = true;
}
}
+ onRecentsInSplitAnimationFinishInner(returnToApp, finishWct, finishT);
+ }
+
+ /** Call this when the recents animation during split-screen finishes. */
+ public void onRecentsInSplitAnimationFinishInner(boolean returnToApp,
+ @NonNull WindowContainerTransaction finishWct,
+ @NonNull SurfaceControl.Transaction finishT) {
+ ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRecentsInSplitAnimationFinish: returnToApp=%b",
+ returnToApp);
+ mPausingTasks.clear();
+ if (returnToApp) {
+ updateSurfaceBounds(mSplitLayout, finishT,
+ false /* applyResizingOffset */);
+ finishT.reparent(mSplitLayout.getDividerLeash(), mRootTaskLeash);
+ setDividerVisibility(true, finishT);
+ return;
+ }
setSplitsVisible(false);
finishWct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java
index 0445add9cba9..13d87eda085b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java
@@ -92,6 +92,10 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
getHolder().addCallback(this);
}
+ public TaskViewTaskController getController() {
+ return mTaskViewTaskController;
+ }
+
/**
* Launch a new activity.
*
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java
index d19a7eac6ad2..a0cc2bc8887b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java
@@ -417,7 +417,8 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener {
}
}
- void notifyTaskRemovalStarted(@NonNull ActivityManager.RunningTaskInfo taskInfo) {
+ /** Notifies listeners of a task being removed. */
+ public void notifyTaskRemovalStarted(@NonNull ActivityManager.RunningTaskInfo taskInfo) {
if (mListener == null) return;
final int taskId = taskInfo.taskId;
mListenerExecutor.execute(() -> mListener.onTaskRemovalStarted(taskId));
@@ -448,7 +449,7 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener {
* have the pending info, we'll do it when we receive it in
* {@link #onTaskAppeared(ActivityManager.RunningTaskInfo, SurfaceControl)}.
*/
- void setTaskNotFound() {
+ public void setTaskNotFound() {
mTaskNotFound = true;
if (mPendingInfo != null) {
cleanUpPendingTask();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java
index 6c90a9060523..1eaae7ec83d9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java
@@ -20,6 +20,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_NONE;
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
@@ -66,7 +67,7 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV
static final String TAG = "TaskViewTransitions";
/**
- * Map of {@link TaskViewTaskController} to {@link TaskViewRequestedState}.
+ * Map of {@link TaskViewTaskController} to {@link TaskViewRepository.TaskViewState}.
* <p>
* {@link TaskView} keeps a reference to the {@link TaskViewTaskController} instance and
* manages its lifecycle.
@@ -95,6 +96,7 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV
final @WindowManager.TransitionType int mType;
final WindowContainerTransaction mWct;
final @NonNull TaskViewTaskController mTaskView;
+ ExternalTransition mExternalTransition;
IBinder mClaimed;
/**
@@ -182,6 +184,32 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV
}
/**
+ * Starts or queues an "external" runnable into the pending queue. This means it will run
+ * in order relative to the local transitions.
+ *
+ * The external operation *must* call {@link #onExternalDone} once it has finished.
+ *
+ * In practice, the external is usually another transition on a different handler.
+ */
+ public void enqueueExternal(@NonNull TaskViewTaskController taskView, ExternalTransition ext) {
+ final PendingTransition pending = new PendingTransition(
+ TRANSIT_NONE, null /* wct */, taskView, null /* cookie */);
+ pending.mExternalTransition = ext;
+ mPending.add(pending);
+ startNextTransition();
+ }
+
+ /**
+ * An external transition run in this "queue" is required to call this once it becomes ready.
+ */
+ public void onExternalDone(IBinder key) {
+ final PendingTransition pending = findPending(key);
+ if (pending == null) return;
+ mPending.remove(pending);
+ startNextTransition();
+ }
+
+ /**
* Looks through the pending transitions for a opening transaction that matches the provided
* `taskView`.
*
@@ -191,6 +219,7 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV
PendingTransition findPendingOpeningTransition(TaskViewTaskController taskView) {
for (int i = mPending.size() - 1; i >= 0; --i) {
if (mPending.get(i).mTaskView != taskView) continue;
+ if (mPending.get(i).mExternalTransition != null) continue;
if (TransitionUtil.isOpeningType(mPending.get(i).mType)) {
return mPending.get(i);
}
@@ -207,6 +236,7 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV
PendingTransition findPending(TaskViewTaskController taskView, int type) {
for (int i = mPending.size() - 1; i >= 0; --i) {
if (mPending.get(i).mTaskView != taskView) continue;
+ if (mPending.get(i).mExternalTransition != null) continue;
if (mPending.get(i).mType == type) {
return mPending.get(i);
}
@@ -518,7 +548,11 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV
// Wait for this to start animating.
return;
}
- pending.mClaimed = mTransitions.startTransition(pending.mType, pending.mWct, this);
+ if (pending.mExternalTransition != null) {
+ pending.mClaimed = pending.mExternalTransition.start();
+ } else {
+ pending.mClaimed = mTransitions.startTransition(pending.mType, pending.mWct, this);
+ }
}
@Override
@@ -641,7 +675,7 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV
}
@VisibleForTesting
- void prepareOpenAnimation(TaskViewTaskController taskView,
+ public void prepareOpenAnimation(TaskViewTaskController taskView,
final boolean newTask,
SurfaceControl.Transaction startTransaction,
SurfaceControl.Transaction finishTransaction,
@@ -695,4 +729,10 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV
taskView.notifyAppeared(newTask);
}
+
+ /** Interface for running an external transition in this object's pending queue. */
+ public interface ExternalTransition {
+ /** Starts a transition and returns an identifying key for lookup. */
+ IBinder start();
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
index 2177986bccd5..d8e7c2ccb15f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -367,7 +367,7 @@ public class DefaultMixedHandler implements MixedTransitionHandler,
}
@Override
- public Consumer<IBinder> handleRecentsRequest(WindowContainerTransaction outWCT) {
+ public Consumer<IBinder> handleRecentsRequest() {
if (mRecentsHandler != null) {
if (mSplitHandler.isSplitScreenVisible()) {
return this::setRecentsTransitionDuringSplit;
@@ -383,6 +383,21 @@ public class DefaultMixedHandler implements MixedTransitionHandler,
return null;
}
+ @Override
+ public void handleFinishRecents(boolean returnToApp,
+ @NonNull WindowContainerTransaction finishWct,
+ @NonNull SurfaceControl.Transaction finishT) {
+ if (mRecentsHandler != null) {
+ for (int i = mActiveTransitions.size() - 1; i >= 0; --i) {
+ final MixedTransition mixed = mActiveTransitions.get(i);
+ if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_SPLIT) {
+ ((RecentsMixedTransition) mixed).onAnimateRecentsDuringSplitFinishing(
+ returnToApp, finishWct, finishT);
+ }
+ }
+ }
+ }
+
private void setRecentsTransitionDuringSplit(IBinder transition) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a recents request while "
+ "Split-Screen is foreground, so treat it as Mixed.");
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java
index b0547a2a47b1..29a58d7f75dc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java
@@ -407,8 +407,6 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition {
mLeftoversHandler.mergeAnimation(
transition, info, t, mergeTarget, finishCallback);
}
- } else {
- mPipHandler.end();
}
return;
case TYPE_KEYGUARD:
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index 36c3e9711f5c..ac6e4c5cd69e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -306,10 +306,10 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
}
// Early check if the transition doesn't warrant an animation.
- if (Transitions.isAllNoAnimation(info) || Transitions.isAllOrderOnly(info)
+ if (TransitionUtil.isAllNoAnimation(info) || TransitionUtil.isAllOrderOnly(info)
|| (info.getFlags() & WindowManager.TRANSIT_FLAG_INVISIBLE) != 0) {
startTransaction.apply();
- finishTransaction.apply();
+ // As a contract, finishTransaction should only be applied in Transitions#onFinish
finishCallback.onTransitionFinished(null /* wct */);
return true;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java
index 30ffdac5cbba..357861cc3ca7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java
@@ -161,13 +161,10 @@ public class MixedTransitionHelper {
pipHandler.startEnterAnimation(pipChange, startTransaction, finishTransaction,
finishCB);
}
- // make a new finishTransaction because pip's startEnterAnimation "consumes" it so
- // we need a separate one to send over to launcher.
- SurfaceControl.Transaction otherFinishT = new SurfaceControl.Transaction();
// Dispatch the rest of the transition normally. This will most-likely be taken by
// recents or default handler.
mixed.mLeftoversHandler = player.dispatchTransition(mixed.mTransition, everythingElse,
- otherStartT, otherFinishT, finishCB, mixedHandler);
+ otherStartT, finishTransaction, finishCB, mixedHandler);
} else {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Not leaving split, so just "
+ "forward animation to Pip-Handler.");
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java
index 8cdbe26a2c76..1847af07f275 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java
@@ -159,6 +159,8 @@ class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition {
// If pair-to-pair switching, the post-recents clean-up isn't needed.
wct = wct != null ? wct : new WindowContainerTransaction();
if (mAnimType != ANIM_TYPE_PAIR_TO_PAIR) {
+ // TODO(b/346588978): Only called if !enableRecentsBookendTransition(), can remove
+ // once that rolls out
mSplitHandler.onRecentsInSplitAnimationFinish(wct, finishTransaction);
} else {
// notify pair-to-pair recents animation finish
@@ -177,6 +179,17 @@ class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition {
return handled;
}
+ /**
+ * Called when the recents animation during split is about to finish.
+ */
+ void onAnimateRecentsDuringSplitFinishing(boolean returnToApp,
+ @NonNull WindowContainerTransaction finishWct,
+ @NonNull SurfaceControl.Transaction finishT) {
+ if (mAnimType != ANIM_TYPE_PAIR_TO_PAIR) {
+ mSplitHandler.onRecentsInSplitAnimationFinishing(returnToApp, finishWct, finishT);
+ }
+ }
+
@Override
void mergeAnimation(
@NonNull IBinder transition, @NonNull TransitionInfo info,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 8c9407b38d9e..b83b7e2f07a3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -672,46 +672,6 @@ public class Transitions implements RemoteCallable<Transitions>,
return -1;
}
- /**
- * Look through a transition and see if all non-closing changes are no-animation. If so, no
- * animation should play.
- */
- static boolean isAllNoAnimation(TransitionInfo info) {
- if (isClosingType(info.getType())) {
- // no-animation is only relevant for launching (open) activities.
- return false;
- }
- boolean hasNoAnimation = false;
- final int changeSize = info.getChanges().size();
- for (int i = changeSize - 1; i >= 0; --i) {
- final TransitionInfo.Change change = info.getChanges().get(i);
- if (isClosingType(change.getMode())) {
- // ignore closing apps since they are a side-effect of the transition and don't
- // animate.
- continue;
- }
- if (change.hasFlags(FLAG_NO_ANIMATION)) {
- hasNoAnimation = true;
- } else if (!TransitionUtil.isOrderOnly(change) && !change.hasFlags(FLAG_IS_OCCLUDED)) {
- // Ignore the order only or occluded changes since they shouldn't be visible during
- // animation. For anything else, we need to animate if at-least one relevant
- // participant *is* animated,
- return false;
- }
- }
- return hasNoAnimation;
- }
-
- /**
- * Check if all changes in this transition are only ordering changes. If so, we won't animate.
- */
- static boolean isAllOrderOnly(TransitionInfo info) {
- for (int i = info.getChanges().size() - 1; i >= 0; --i) {
- if (!TransitionUtil.isOrderOnly(info.getChanges().get(i))) return false;
- }
- return true;
- }
-
private Track getOrCreateTrack(int trackId) {
while (trackId >= mTracks.size()) {
mTracks.add(new Track());
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecorViewModel.java
index 7948eadb28f4..2b2cdf84005c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecorViewModel.java
@@ -16,13 +16,17 @@
package com.android.wm.shell.windowdecor;
import android.app.ActivityManager.RunningTaskInfo;
+import android.app.ActivityTaskManager;
+import android.app.IActivityTaskManager;
import android.content.Context;
import android.hardware.input.InputManager;
+import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
import android.util.Log;
import android.util.SparseArray;
import android.view.InputDevice;
+import android.view.InsetsState;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.SurfaceControl;
@@ -33,6 +37,7 @@ import android.window.WindowContainerTransaction;
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
@@ -49,7 +54,8 @@ import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSuppl
* Works with decorations that extend {@link CarWindowDecoration}.
*/
public abstract class CarWindowDecorViewModel
- implements WindowDecorViewModel, FocusTransitionListener {
+ implements WindowDecorViewModel, FocusTransitionListener,
+ DisplayInsetsController.OnInsetsChangedListener {
private static final String TAG = "CarWindowDecorViewModel";
private final ShellTaskOrganizer mTaskOrganizer;
@@ -57,31 +63,37 @@ public abstract class CarWindowDecorViewModel
private final @ShellBackgroundThread ShellExecutor mBgExecutor;
private final ShellExecutor mMainExecutor;
private final DisplayController mDisplayController;
+ private final DisplayInsetsController mDisplayInsetsController;
private final FocusTransitionObserver mFocusTransitionObserver;
private final SyncTransactionQueue mSyncQueue;
private final SparseArray<CarWindowDecoration> mWindowDecorByTaskId = new SparseArray<>();
private final WindowDecorViewHostSupplier<WindowDecorViewHost> mWindowDecorViewHostSupplier;
+ private final IActivityTaskManager mActivityTaskManager;
public CarWindowDecorViewModel(
Context context,
+ @ShellMainThread ShellExecutor mainExecutor,
@ShellBackgroundThread ShellExecutor bgExecutor,
- @ShellMainThread ShellExecutor shellExecutor,
ShellInit shellInit,
ShellTaskOrganizer taskOrganizer,
DisplayController displayController,
+ DisplayInsetsController displayInsetsController,
SyncTransactionQueue syncQueue,
FocusTransitionObserver focusTransitionObserver,
WindowDecorViewHostSupplier<WindowDecorViewHost> windowDecorViewHostSupplier) {
mContext = context;
- mMainExecutor = shellExecutor;
+ mMainExecutor = mainExecutor;
mBgExecutor = bgExecutor;
mTaskOrganizer = taskOrganizer;
mDisplayController = displayController;
+ mDisplayInsetsController = displayInsetsController;
mFocusTransitionObserver = focusTransitionObserver;
mSyncQueue = syncQueue;
mWindowDecorViewHostSupplier = windowDecorViewHostSupplier;
+ mActivityTaskManager = ActivityTaskManager.getService();
shellInit.addInitCallback(this::onInit, this);
+ displayInsetsController.addGlobalInsetsChangedListener(this);
}
private void onInit() {
@@ -187,6 +199,26 @@ public abstract class CarWindowDecorViewModel
decoration.close();
}
+ @Override
+ public void insetsChanged(int displayId, InsetsState insetsState) {
+ final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ try {
+ mActivityTaskManager.getTasks(/* maxNum= */ Integer.MAX_VALUE,
+ /* filterOnlyVisibleRecents= */ false, /* keepIntentExtra= */ false,
+ displayId)
+ .stream().filter(taskInfo -> taskInfo.isVisible && taskInfo.isRunning)
+ .forEach(taskInfo -> {
+ final CarWindowDecoration decoration = mWindowDecorByTaskId.get(
+ taskInfo.taskId);
+ if (decoration != null) {
+ decoration.relayout(taskInfo, t, t);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Cannot update decoration on inset change on displayId: " + displayId);
+ }
+ }
+
/**
* @return {@code true} if the task/activity associated with {@code taskInfo} should show
* window decoration.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecoration.java
index 39437845301e..3182745d813e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecoration.java
@@ -30,7 +30,6 @@ import android.view.WindowInsets;
import android.window.WindowContainerTransaction;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
@@ -47,6 +46,7 @@ public class CarWindowDecoration extends WindowDecoration<WindowDecorLinearLayou
private WindowDecorLinearLayout mRootView;
private @ShellBackgroundThread final ShellExecutor mBgExecutor;
private final View.OnClickListener mClickListener;
+ private final RelayoutParams mRelayoutParams = new RelayoutParams();
private final RelayoutResult<WindowDecorLinearLayout> mResult = new RelayoutResult<>();
CarWindowDecoration(
@@ -75,7 +75,8 @@ public class CarWindowDecoration extends WindowDecoration<WindowDecorLinearLayou
@SuppressLint("MissingPermission")
void relayout(ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) {
- relayout(taskInfo, startT, finishT, /* isCaptionVisible= */ true);
+ relayout(taskInfo, startT, finishT,
+ /* isCaptionVisible= */ mRelayoutParams.mIsCaptionVisible);
}
@SuppressLint("MissingPermission")
@@ -84,12 +85,9 @@ public class CarWindowDecoration extends WindowDecoration<WindowDecorLinearLayou
boolean isCaptionVisible) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
- RelayoutParams relayoutParams = new RelayoutParams();
+ updateRelayoutParams(mRelayoutParams, taskInfo, isCaptionVisible);
- updateRelayoutParams(relayoutParams, taskInfo,
- mDisplayController.getInsetsState(taskInfo.displayId), isCaptionVisible);
-
- relayout(relayoutParams, startT, finishT, wct, mRootView, mResult);
+ relayout(mRelayoutParams, startT, finishT, wct, mRootView, mResult);
// After this line, mTaskInfo is up-to-date and should be used instead of taskInfo
mBgExecutor.execute(() -> mTaskOrganizer.applyTransaction(wct));
@@ -118,7 +116,6 @@ public class CarWindowDecoration extends WindowDecoration<WindowDecorLinearLayou
private void updateRelayoutParams(
RelayoutParams relayoutParams,
ActivityManager.RunningTaskInfo taskInfo,
- @Nullable InsetsState displayInsetsState,
boolean isCaptionVisible) {
relayoutParams.reset();
relayoutParams.mRunningTaskInfo = taskInfo;
@@ -127,16 +124,19 @@ public class CarWindowDecoration extends WindowDecoration<WindowDecorLinearLayou
relayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_height;
relayoutParams.mIsCaptionVisible =
isCaptionVisible && mIsStatusBarVisible && !mIsKeyguardVisibleAndOccluded;
- if (displayInsetsState != null) {
- relayoutParams.mCaptionTopPadding = getTopPadding(
- taskInfo.getConfiguration().windowConfiguration.getBounds(),
- displayInsetsState);
- }
+ relayoutParams.mCaptionTopPadding = getTopPadding(taskInfo, relayoutParams);
+
relayoutParams.mInsetSourceFlags |= FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR;
relayoutParams.mApplyStartTransactionOnDraw = true;
}
- private static int getTopPadding(Rect taskBounds, @NonNull InsetsState insetsState) {
+ private int getTopPadding(ActivityManager.RunningTaskInfo taskInfo,
+ RelayoutParams relayoutParams) {
+ Rect taskBounds = taskInfo.getConfiguration().windowConfiguration.getBounds();
+ InsetsState insetsState = mDisplayController.getInsetsState(taskInfo.displayId);
+ if (insetsState == null) {
+ return relayoutParams.mCaptionTopPadding;
+ }
Insets systemDecor = insetsState.calculateInsets(taskBounds,
WindowInsets.Type.systemBars() & ~WindowInsets.Type.captionBar(),
false /* ignoreVisibility */);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index fad1c9f848ea..7e7a793298e2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -32,7 +32,6 @@ import static android.view.WindowInsets.Type.statusBars;
import static com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU;
import static com.android.window.flags.Flags.enableDisplayFocusInShellTransitions;
-import static com.android.wm.shell.compatui.AppCompatUtils.isTopActivityExemptFromDesktopWindowing;
import static com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.InputMethod;
import static com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.MinimizeReason;
import static com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger;
@@ -133,6 +132,7 @@ import com.android.wm.shell.recents.RecentsTransitionStateListener;
import com.android.wm.shell.shared.FocusTransitionListener;
import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
import com.android.wm.shell.shared.annotations.ShellMainThread;
+import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource;
import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition;
@@ -254,6 +254,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
private final DesktopModeUiEventLogger mDesktopModeUiEventLogger;
private final WindowDecorTaskResourceLoader mTaskResourceLoader;
private final RecentsTransitionHandler mRecentsTransitionHandler;
+ private final DesktopModeCompatPolicy mDesktopModeCompatPolicy;
public DesktopModeWindowDecorViewModel(
Context context,
@@ -290,7 +291,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
DesktopModeEventLogger desktopModeEventLogger,
DesktopModeUiEventLogger desktopModeUiEventLogger,
WindowDecorTaskResourceLoader taskResourceLoader,
- RecentsTransitionHandler recentsTransitionHandler) {
+ RecentsTransitionHandler recentsTransitionHandler,
+ DesktopModeCompatPolicy desktopModeCompatPolicy) {
this(
context,
shellExecutor,
@@ -332,7 +334,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
desktopModeEventLogger,
desktopModeUiEventLogger,
taskResourceLoader,
- recentsTransitionHandler);
+ recentsTransitionHandler,
+ desktopModeCompatPolicy);
}
@VisibleForTesting
@@ -377,7 +380,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
DesktopModeEventLogger desktopModeEventLogger,
DesktopModeUiEventLogger desktopModeUiEventLogger,
WindowDecorTaskResourceLoader taskResourceLoader,
- RecentsTransitionHandler recentsTransitionHandler) {
+ RecentsTransitionHandler recentsTransitionHandler,
+ DesktopModeCompatPolicy desktopModeCompatPolicy) {
mContext = context;
mMainExecutor = shellExecutor;
mMainHandler = mainHandler;
@@ -447,6 +451,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
mDesktopModeUiEventLogger = desktopModeUiEventLogger;
mTaskResourceLoader = taskResourceLoader;
mRecentsTransitionHandler = recentsTransitionHandler;
+ mDesktopModeCompatPolicy = desktopModeCompatPolicy;
shellInit.addInitCallback(this::onInit, this);
}
@@ -461,7 +466,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
new DesktopModeOnTaskResizeAnimationListener());
mDesktopTasksController.setOnTaskRepositionAnimationListener(
new DesktopModeOnTaskRepositionAnimationListener());
- if (Flags.enableDesktopRecentsTransitionsCornersBugfix()) {
+ if (DesktopModeFlags.ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX.isTrue()) {
mRecentsTransitionHandler.addTransitionStateListener(
new DesktopModeRecentsTransitionStateListener());
}
@@ -1652,8 +1657,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
&& mSplitScreenController.isTaskRootOrStageRoot(taskInfo.taskId)) {
return false;
}
- if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue()
- && isTopActivityExemptFromDesktopWindowing(mContext, taskInfo)) {
+ if (mDesktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(taskInfo)) {
return false;
}
if (isPartOfDefaultHomePackage(taskInfo)) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index b179741b1259..4e125d001076 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -542,6 +542,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
if (appHeader != null) {
appHeader.setAppName(name);
appHeader.setAppIcon(icon);
+ if (canEnterDesktopMode(mContext) && isEducationEnabled()) {
+ notifyCaptionStateChanged();
+ }
}
});
}
@@ -969,7 +972,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
final RelayoutParams.OccludingCaptionElement controlsElement =
new RelayoutParams.OccludingCaptionElement();
controlsElement.mWidthResId = R.dimen.desktop_mode_customizable_caption_margin_end;
- if (Flags.enableMinimizeButton()) {
+ if (DesktopModeFlags.ENABLE_MINIMIZE_BUTTON.isTrue()) {
controlsElement.mWidthResId =
R.dimen.desktop_mode_customizable_caption_with_minimize_button_margin_end;
}
@@ -1355,7 +1358,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
mWebUri = assistContent == null ? null : AppToWebUtils.getSessionWebUri(assistContent);
updateGenericLink();
final boolean supportsMultiInstance = mMultiInstanceHelper
- .supportsMultiInstanceSplit(mTaskInfo.baseActivity)
+ .supportsMultiInstanceSplit(mTaskInfo.baseActivity, mTaskInfo.userId)
&& Flags.enableDesktopWindowingMultiInstanceFeatures();
final boolean shouldShowManageWindowsButton = supportsMultiInstance
&& mMinimumInstancesFound;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt
index 70c0b54462e3..22bc9782170b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt
@@ -5,6 +5,7 @@ import android.app.ActivityManager.RunningTaskInfo
import android.content.Context
import android.graphics.PointF
import android.graphics.Rect
+import android.view.Choreographer
import android.view.MotionEvent
import android.view.SurfaceControl
import android.view.VelocityTracker
@@ -48,7 +49,7 @@ class MoveToDesktopAnimator @JvmOverloads constructor(
t.setScale(taskSurface, scale, scale)
.setCornerRadius(taskSurface, cornerRadius)
.setScale(taskSurface, scale, scale)
- .setCornerRadius(taskSurface, cornerRadius)
+ .setFrameTimeline(Choreographer.getInstance().vsyncId)
.setPosition(taskSurface, position.x, position.y)
.apply()
}
@@ -96,6 +97,7 @@ class MoveToDesktopAnimator @JvmOverloads constructor(
setTaskPosition(ev.rawX, ev.rawY)
val t = transactionFactory()
t.setPosition(taskSurface, position.x, position.y)
+ t.setFrameTimeline(Choreographer.getInstance().vsyncId)
t.apply()
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt
index d87da092cccf..1bc48f89ea6d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt
@@ -16,7 +16,6 @@
package com.android.wm.shell.windowdecor.common
import android.annotation.DimenRes
-import android.app.ActivityManager
import android.app.ActivityManager.RunningTaskInfo
import android.content.Context
import android.content.pm.ActivityInfo
@@ -29,6 +28,7 @@ import com.android.launcher3.icons.BaseIconFactory
import com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT
import com.android.launcher3.icons.IconProvider
import com.android.wm.shell.R
+import com.android.wm.shell.common.UserProfileContexts
import com.android.wm.shell.shared.annotations.ShellBackgroundThread
import com.android.wm.shell.sysui.ShellCommandHandler
import com.android.wm.shell.sysui.ShellController
@@ -41,10 +41,10 @@ import java.util.concurrent.ConcurrentHashMap
* A utility and cache for window decoration UI resources.
*/
class WindowDecorTaskResourceLoader(
- private val context: Context,
shellInit: ShellInit,
private val shellController: ShellController,
private val shellCommandHandler: ShellCommandHandler,
+ private val userProfilesContexts: UserProfileContexts,
private val iconProvider: IconProvider,
private val headerIconFactory: BaseIconFactory,
private val veilIconFactory: BaseIconFactory,
@@ -54,11 +54,12 @@ class WindowDecorTaskResourceLoader(
shellInit: ShellInit,
shellController: ShellController,
shellCommandHandler: ShellCommandHandler,
+ userProfileContexts: UserProfileContexts,
) : this(
- context,
shellInit,
shellController,
shellCommandHandler,
+ userProfileContexts,
IconProvider(context),
headerIconFactory = context.createIconFactory(R.dimen.desktop_mode_caption_icon_radius),
veilIconFactory = context.createIconFactory(R.dimen.desktop_mode_resize_veil_icon_size),
@@ -79,9 +80,6 @@ class WindowDecorTaskResourceLoader(
*/
private val existingTasks = mutableSetOf<Int>()
- @VisibleForTesting
- lateinit var currentUserContext: Context
-
init {
shellInit.addInitCallback(this::onInit, this)
}
@@ -90,14 +88,10 @@ class WindowDecorTaskResourceLoader(
shellCommandHandler.addDumpCallback(this::dump, this)
shellController.addUserChangeListener(object : UserChangeListener {
override fun onUserChanged(newUserId: Int, userContext: Context) {
- currentUserContext = userContext
// No need to hold on to resources for tasks of another profile.
taskToResourceCache.clear()
}
})
- currentUserContext = context.createContextAsUser(
- UserHandle.of(ActivityManager.getCurrentUser()), /* flags= */ 0
- )
}
/** Returns the user readable name for this task. */
@@ -158,15 +152,20 @@ class WindowDecorTaskResourceLoader(
private fun loadAppResources(taskInfo: RunningTaskInfo): AppResources {
Trace.beginSection("$TAG#loadAppResources")
- val pm = currentUserContext.packageManager
- val activityInfo = getActivityInfo(taskInfo, pm)
- val appName = pm.getApplicationLabel(activityInfo.applicationInfo)
- val appIconDrawable = iconProvider.getIcon(activityInfo)
- val badgedAppIconDrawable = pm.getUserBadgedIcon(appIconDrawable, taskInfo.userHandle())
- val appIcon = headerIconFactory.createIconBitmap(badgedAppIconDrawable, /* scale= */ 1f)
- val veilIcon = veilIconFactory.createScaledBitmap(appIconDrawable, MODE_DEFAULT)
- Trace.endSection()
- return AppResources(appName = appName, appIcon = appIcon, veilIcon = veilIcon)
+ try {
+ val pm = checkNotNull(userProfilesContexts[taskInfo.userId]?.packageManager) {
+ "Could not get context for user ${taskInfo.userId}"
+ }
+ val activityInfo = getActivityInfo(taskInfo, pm)
+ val appName = pm.getApplicationLabel(activityInfo.applicationInfo)
+ val appIconDrawable = iconProvider.getIcon(activityInfo)
+ val badgedAppIconDrawable = pm.getUserBadgedIcon(appIconDrawable, taskInfo.userHandle())
+ val appIcon = headerIconFactory.createIconBitmap(badgedAppIconDrawable, /* scale= */ 1f)
+ val veilIcon = veilIconFactory.createScaledBitmap(appIconDrawable, MODE_DEFAULT)
+ return AppResources(appName = appName, appIcon = appIcon, veilIcon = veilIcon)
+ } finally {
+ Trace.endSection()
+ }
}
private fun getActivityInfo(taskInfo: RunningTaskInfo, pm: PackageManager): ActivityInfo {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt
index d72da3a08de5..8747f63e789f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt
@@ -29,6 +29,7 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.common.DisplayChangeController
import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.common.SyncTransactionQueue
import com.android.wm.shell.desktopmode.DesktopModeEventLogger
import com.android.wm.shell.desktopmode.DesktopTasksController
@@ -37,6 +38,7 @@ import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator
import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler
import com.android.wm.shell.shared.annotations.ShellBackgroundThread
import com.android.wm.shell.shared.annotations.ShellMainThread
+import com.android.wm.shell.transition.FocusTransitionObserver
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader
@@ -58,6 +60,8 @@ class DesktopTilingDecorViewModel(
private val desktopUserRepositories: DesktopUserRepositories,
private val desktopModeEventLogger: DesktopModeEventLogger,
private val taskResourceLoader: WindowDecorTaskResourceLoader,
+ private val focusTransitionObserver: FocusTransitionObserver,
+ private val mainExecutor: ShellExecutor,
) : DisplayChangeController.OnDisplayChangingListener {
@VisibleForTesting
var tilingTransitionHandlerByDisplayId = SparseArray<DesktopTilingWindowDecoration>()
@@ -94,6 +98,8 @@ class DesktopTilingDecorViewModel(
returnToDragStartAnimator,
desktopUserRepositories,
desktopModeEventLogger,
+ focusTransitionObserver,
+ mainExecutor,
)
tilingTransitionHandlerByDisplayId.put(displayId, newHandler)
newHandler
@@ -112,9 +118,10 @@ class DesktopTilingDecorViewModel(
}
fun moveTaskToFrontIfTiled(taskInfo: RunningTaskInfo): Boolean {
+ // Always pass focus=true because taskInfo.isFocused is not updated yet.
return tilingTransitionHandlerByDisplayId
.get(taskInfo.displayId)
- ?.moveTiledPairToFront(taskInfo, isTaskFocused = true) ?: false
+ ?.moveTiledPairToFront(taskInfo.taskId, isFocusedOnDisplay = true) ?: false
}
fun onOverviewAnimationStateChange(isRunning: Boolean) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt
index 6f2323347468..666d4bd046dc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt
@@ -35,11 +35,14 @@ import android.window.TransitionRequestInfo
import android.window.WindowContainerTransaction
import com.android.internal.annotations.VisibleForTesting
import com.android.launcher3.icons.BaseIconFactory
+import com.android.window.flags.Flags
+import com.android.wm.shell.shared.FocusTransitionListener
import com.android.wm.shell.R
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayLayout
+import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.common.SyncTransactionQueue
import com.android.wm.shell.desktopmode.DesktopModeEventLogger
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger
@@ -49,6 +52,7 @@ import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator
import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler
import com.android.wm.shell.shared.annotations.ShellBackgroundThread
import com.android.wm.shell.shared.annotations.ShellMainThread
+import com.android.wm.shell.transition.FocusTransitionObserver
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.transition.Transitions.TRANSIT_MINIMIZE
import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
@@ -78,13 +82,16 @@ class DesktopTilingWindowDecoration(
private val returnToDragStartAnimator: ReturnToDragStartAnimator,
private val desktopUserRepositories: DesktopUserRepositories,
private val desktopModeEventLogger: DesktopModeEventLogger,
+ private val focusTransitionObserver: FocusTransitionObserver,
+ @ShellMainThread private val mainExecutor: ShellExecutor,
private val transactionSupplier: Supplier<Transaction> = Supplier { Transaction() },
) :
Transitions.TransitionHandler,
ShellTaskOrganizer.FocusListener,
ShellTaskOrganizer.TaskVanishedListener,
DragEventListener,
- Transitions.TransitionObserver {
+ Transitions.TransitionObserver,
+ FocusTransitionListener {
companion object {
private val TAG: String = DesktopTilingWindowDecoration::class.java.simpleName
private const val TILING_DIVIDER_TAG = "Tiling Divider"
@@ -176,8 +183,13 @@ class DesktopTilingWindowDecoration(
if (!isTilingManagerInitialised) {
desktopTilingDividerWindowManager = initTilingManagerForDisplay(displayId, config)
isTilingManagerInitialised = true
- shellTaskOrganizer.addFocusListener(this)
- isTilingFocused = true
+
+ if (Flags.enableDisplayFocusInShellTransitions()) {
+ focusTransitionObserver.setLocalFocusTransitionListener(this, mainExecutor)
+ } else {
+ shellTaskOrganizer.addFocusListener(this)
+ isTilingFocused = true
+ }
}
leftTaskResizingHelper?.initIfNeeded()
rightTaskResizingHelper?.initIfNeeded()
@@ -474,23 +486,33 @@ class DesktopTilingWindowDecoration(
}
}
- // Only called if [taskInfo] relates to a focused task
- private fun isTilingFocusRemoved(taskInfo: RunningTaskInfo): Boolean {
+ // Only called if [taskId] relates to a focused task
+ private fun isTilingFocusRemoved(taskId: Int): Boolean {
return isTilingFocused &&
- taskInfo.taskId != leftTaskResizingHelper?.taskInfo?.taskId &&
- taskInfo.taskId != rightTaskResizingHelper?.taskInfo?.taskId
+ taskId != leftTaskResizingHelper?.taskInfo?.taskId &&
+ taskId != rightTaskResizingHelper?.taskInfo?.taskId
}
+ // Overriding ShellTaskOrganizer.FocusListener
override fun onFocusTaskChanged(taskInfo: RunningTaskInfo?) {
+ if (Flags.enableDisplayFocusInShellTransitions()) return
if (taskInfo != null) {
- moveTiledPairToFront(taskInfo)
+ moveTiledPairToFront(taskInfo.taskId, taskInfo.isFocused)
}
}
+ // Overriding FocusTransitionListener
+ override fun onFocusedTaskChanged(taskId: Int,
+ isFocusedOnDisplay: Boolean,
+ isFocusedGlobally: Boolean) {
+ if (!Flags.enableDisplayFocusInShellTransitions()) return
+ moveTiledPairToFront(taskId, isFocusedOnDisplay)
+ }
+
// Only called if [taskInfo] relates to a focused task
- private fun isTilingRefocused(taskInfo: RunningTaskInfo): Boolean {
- return taskInfo.taskId == leftTaskResizingHelper?.taskInfo?.taskId ||
- taskInfo.taskId == rightTaskResizingHelper?.taskInfo?.taskId
+ private fun isTilingRefocused(taskId: Int): Boolean {
+ return taskId == leftTaskResizingHelper?.taskInfo?.taskId ||
+ taskId == rightTaskResizingHelper?.taskInfo?.taskId
}
private fun buildTiledTasksMoveToFront(leftOnTop: Boolean): WindowContainerTransaction {
@@ -582,14 +604,13 @@ class DesktopTilingWindowDecoration(
* If specified, [isTaskFocused] will override [RunningTaskInfo.isFocused]. This is to be used
* when called when the task will be focused, but the [taskInfo] hasn't been updated yet.
*/
- fun moveTiledPairToFront(taskInfo: RunningTaskInfo, isTaskFocused: Boolean? = null): Boolean {
+ fun moveTiledPairToFront(taskId: Int, isFocusedOnDisplay: Boolean): Boolean {
if (!isTilingManagerInitialised) return false
- val isFocused = isTaskFocused ?: taskInfo.isFocused
- if (!isFocused) return false
+ if (!isFocusedOnDisplay) return false
// If a task that isn't tiled is being focused, let the generic handler do the work.
- if (isTilingFocusRemoved(taskInfo)) {
+ if (!Flags.enableDisplayFocusInShellTransitions() && isTilingFocusRemoved(taskId)) {
isTilingFocused = false
return false
}
@@ -597,31 +618,29 @@ class DesktopTilingWindowDecoration(
val leftTiledTask = leftTaskResizingHelper ?: return false
val rightTiledTask = rightTaskResizingHelper ?: return false
if (!allTiledTasksVisible()) return false
- val isLeftOnTop = taskInfo.taskId == leftTiledTask.taskInfo.taskId
- if (isTilingRefocused(taskInfo)) {
- val t = transactionSupplier.get()
- isTilingFocused = true
- if (taskInfo.taskId == leftTaskResizingHelper?.taskInfo?.taskId) {
- desktopTilingDividerWindowManager?.onRelativeLeashChanged(
- leftTiledTask.getLeash(),
- t,
- )
- }
- if (taskInfo.taskId == rightTaskResizingHelper?.taskInfo?.taskId) {
- desktopTilingDividerWindowManager?.onRelativeLeashChanged(
- rightTiledTask.getLeash(),
- t,
- )
- }
- transitions.startTransition(
- TRANSIT_TO_FRONT,
- buildTiledTasksMoveToFront(isLeftOnTop),
- null,
- )
- t.apply()
- return true
+ val isLeftOnTop = taskId == leftTiledTask.taskInfo.taskId
+ if (!isTilingRefocused(taskId)) return false
+ val t = transactionSupplier.get()
+ if (!Flags.enableDisplayFocusInShellTransitions()) isTilingFocused = true
+ if (taskId == leftTaskResizingHelper?.taskInfo?.taskId) {
+ desktopTilingDividerWindowManager?.onRelativeLeashChanged(
+ leftTiledTask.getLeash(),
+ t,
+ )
}
- return false
+ if (taskId == rightTaskResizingHelper?.taskInfo?.taskId) {
+ desktopTilingDividerWindowManager?.onRelativeLeashChanged(
+ rightTiledTask.getLeash(),
+ t,
+ )
+ }
+ transitions.startTransition(
+ TRANSIT_TO_FRONT,
+ buildTiledTasksMoveToFront(isLeftOnTop),
+ null,
+ )
+ t.apply()
+ return true
}
private fun allTiledTasksVisible(): Boolean {
@@ -706,7 +725,13 @@ class DesktopTilingWindowDecoration(
}
private fun tearDownTiling() {
- if (isTilingManagerInitialised) shellTaskOrganizer.removeFocusListener(this)
+ if (isTilingManagerInitialised) {
+ if (Flags.enableDisplayFocusInShellTransitions()) {
+ focusTransitionObserver.unsetLocalFocusTransitionListener(this)
+ } else {
+ shellTaskOrganizer.removeFocusListener(this)
+ }
+ }
if (leftTaskResizingHelper == null && rightTaskResizingHelper == null) {
shellTaskOrganizer.removeTaskVanishedListener(this)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
index dc4fa3788778..9f8ca7740182 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
@@ -47,7 +47,6 @@ import com.android.internal.R.color.materialColorSurfaceContainerHigh
import com.android.internal.R.color.materialColorSurfaceContainerLow
import com.android.internal.R.color.materialColorSurfaceDim
import com.android.window.flags.Flags
-import com.android.window.flags.Flags.enableMinimizeButton
import com.android.wm.shell.R
import android.window.DesktopModeFlags
import com.android.wm.shell.windowdecor.MaximizeButtonView
@@ -226,7 +225,7 @@ class AppHeaderViewHolder(
minimizeWindowButton.background = getDrawable(1)
}
maximizeButtonView.setAnimationTints(isDarkMode())
- minimizeWindowButton.isGone = !enableMinimizeButton()
+ minimizeWindowButton.isGone = !DesktopModeFlags.ENABLE_MINIMIZE_BUTTON.isTrue()
}
private fun bindDataWithThemedHeaders(
@@ -276,7 +275,7 @@ class AppHeaderViewHolder(
drawableInsets = minimizeDrawableInsets
)
}
- minimizeWindowButton.isGone = !enableMinimizeButton()
+ minimizeWindowButton.isGone = !DesktopModeFlags.ENABLE_MINIMIZE_BUTTON.isTrue()
// Maximize button.
maximizeButtonView.apply {
setAnimationTints(
@@ -329,11 +328,6 @@ class AppHeaderViewHolder(
}
fun runOnAppChipGlobalLayout(runnable: () -> Unit) {
- if (openMenuButton.isAttachedToWindow) {
- // App chip is already inflated.
- runnable()
- return
- }
// Wait for app chip to be inflated before notifying repository.
openMenuButton.viewTreeObserver.addOnGlobalLayoutListener(object :
OnGlobalLayoutListener {
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppFromAllAppsLandscape.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppFromAllAppsLandscape.kt
new file mode 100644
index 000000000000..6b159a4152ac
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppFromAllAppsLandscape.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker
+
+import android.tools.Rotation.ROTATION_90
+import android.tools.flicker.FlickerConfig
+import android.tools.flicker.annotation.ExpectedScenarios
+import android.tools.flicker.annotation.FlickerConfigProvider
+import android.tools.flicker.config.FlickerConfig
+import android.tools.flicker.config.FlickerServiceConfig
+import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.CASCADE_APP
+import com.android.wm.shell.scenarios.OpenAppFromAllApps
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class OpenAppFromAllAppsLandscape : OpenAppFromAllApps(rotation = ROTATION_90) {
+
+ @ExpectedScenarios(["CASCADE_APP"])
+ @Test
+ override fun openApp() = super.openApp()
+
+ companion object {
+ @JvmStatic
+ @FlickerConfigProvider
+ fun flickerConfigProvider(): FlickerConfig =
+ FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(CASCADE_APP)
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppFromAllAppsPortrait.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppFromAllAppsPortrait.kt
new file mode 100644
index 000000000000..07b439284680
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppFromAllAppsPortrait.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker
+
+import android.tools.Rotation.ROTATION_0
+import android.tools.flicker.FlickerConfig
+import android.tools.flicker.annotation.ExpectedScenarios
+import android.tools.flicker.annotation.FlickerConfigProvider
+import android.tools.flicker.config.FlickerConfig
+import android.tools.flicker.config.FlickerServiceConfig
+import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.CASCADE_APP
+import com.android.wm.shell.scenarios.OpenAppFromAllApps
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class OpenAppFromAllAppsPortrait : OpenAppFromAllApps(rotation = ROTATION_0) {
+
+ @ExpectedScenarios(["CASCADE_APP"])
+ @Test
+ override fun openApp() = super.openApp()
+
+ companion object {
+ @JvmStatic
+ @FlickerConfigProvider
+ fun flickerConfigProvider(): FlickerConfig =
+ FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(CASCADE_APP)
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppFromTaskbarLandscape.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppFromTaskbarLandscape.kt
new file mode 100644
index 000000000000..caadd3be0b9c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppFromTaskbarLandscape.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker
+
+import android.tools.Rotation.ROTATION_90
+import android.tools.flicker.FlickerConfig
+import android.tools.flicker.annotation.ExpectedScenarios
+import android.tools.flicker.annotation.FlickerConfigProvider
+import android.tools.flicker.config.FlickerConfig
+import android.tools.flicker.config.FlickerServiceConfig
+import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.CASCADE_APP
+import com.android.wm.shell.scenarios.OpenAppFromTaskbar
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class OpenAppFromTaskbarLandscape : OpenAppFromTaskbar(rotation = ROTATION_90) {
+
+ @ExpectedScenarios(["CASCADE_APP"])
+ @Test
+ override fun openApp() = super.openApp()
+
+ companion object {
+ @JvmStatic
+ @FlickerConfigProvider
+ fun flickerConfigProvider(): FlickerConfig =
+ FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(CASCADE_APP)
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppFromTaskbarPortrait.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppFromTaskbarPortrait.kt
new file mode 100644
index 000000000000..77f5ab290e20
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppFromTaskbarPortrait.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker
+
+import android.tools.Rotation.ROTATION_0
+import android.tools.flicker.FlickerConfig
+import android.tools.flicker.annotation.ExpectedScenarios
+import android.tools.flicker.annotation.FlickerConfigProvider
+import android.tools.flicker.config.FlickerConfig
+import android.tools.flicker.config.FlickerServiceConfig
+import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.CASCADE_APP
+import com.android.wm.shell.scenarios.OpenAppFromTaskbar
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class OpenAppFromTaskbarPortrait : OpenAppFromTaskbar(rotation = ROTATION_0) {
+
+ @ExpectedScenarios(["CASCADE_APP"])
+ @Test
+ override fun openApp() = super.openApp()
+
+ companion object {
+ @JvmStatic
+ @FlickerConfigProvider
+ fun flickerConfigProvider(): FlickerConfig =
+ FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(CASCADE_APP)
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt
index 966aea3088c4..7855698d0151 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt
@@ -21,6 +21,7 @@ import android.tools.NavBar
import android.tools.Rotation
import android.tools.flicker.rules.ChangeDisplayOrientationRule
import android.tools.traces.parsers.WindowManagerStateHelper
+import android.window.DesktopModeFlags
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import com.android.launcher3.tapl.LauncherInstrumentation
@@ -59,7 +60,7 @@ constructor(
fun setup() {
Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
if (usingKeyboard) {
- Assume.assumeTrue(Flags.enableTaskResizingKeyboardShortcuts())
+ Assume.assumeTrue(DesktopModeFlags.ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS.isTrue)
}
tapl.setEnableRotation(true)
tapl.setExpectedRotation(rotation.value)
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeAppWindows.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeAppWindows.kt
index 46c97b0a1397..2f99fbaba078 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeAppWindows.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeAppWindows.kt
@@ -21,6 +21,7 @@ import android.tools.NavBar
import android.tools.Rotation
import android.tools.flicker.rules.ChangeDisplayOrientationRule
import android.tools.traces.parsers.WindowManagerStateHelper
+import android.window.DesktopModeFlags
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import com.android.launcher3.tapl.LauncherInstrumentation
@@ -62,7 +63,7 @@ constructor(
Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
Assume.assumeTrue(Flags.enableMinimizeButton())
if (usingKeyboard) {
- Assume.assumeTrue(Flags.enableTaskResizingKeyboardShortcuts())
+ Assume.assumeTrue(DesktopModeFlags.ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS.isTrue)
}
tapl.setEnableRotation(true)
tapl.setExpectedRotation(rotation.value)
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppFromAllApps.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppFromAllApps.kt
index 36cdd5b26992..348219631245 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppFromAllApps.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppFromAllApps.kt
@@ -20,12 +20,12 @@ import android.app.Instrumentation
import android.tools.NavBar
import android.tools.flicker.rules.ChangeDisplayOrientationRule
import android.tools.Rotation
+import android.tools.device.apphelpers.CalculatorAppHelper
import android.tools.traces.parsers.WindowManagerStateHelper
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import com.android.launcher3.tapl.LauncherInstrumentation
import com.android.server.wm.flicker.helpers.DesktopModeAppHelper
-import com.android.server.wm.flicker.helpers.MailAppHelper
import com.android.server.wm.flicker.helpers.SimpleAppHelper
import com.android.window.flags.Flags
import com.android.wm.shell.Utils
@@ -44,7 +44,7 @@ abstract class OpenAppFromAllApps(val rotation: Rotation = Rotation.ROTATION_0)
private val wmHelper = WindowManagerStateHelper(instrumentation)
private val device = UiDevice.getInstance(instrumentation)
private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation))
- private val mailApp = MailAppHelper(instrumentation)
+ private val calculatorApp = CalculatorAppHelper(instrumentation)
@Rule
@JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation)
@@ -64,13 +64,13 @@ abstract class OpenAppFromAllApps(val rotation: Rotation = Rotation.ROTATION_0)
open fun openApp() {
tapl.launchedAppState.taskbar
.openAllApps()
- .getAppIcon(mailApp.appName)
- .launch(mailApp.packageName)
+ .getAppIcon(calculatorApp.appName)
+ .launch(calculatorApp.packageName)
}
@After
fun teardown() {
- mailApp.exit(wmHelper)
+ calculatorApp.exit(wmHelper)
testApp.exit(wmHelper)
}
} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithKeyboardShortcuts.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithKeyboardShortcuts.kt
index 068064402ee5..59d15ca4fa6b 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithKeyboardShortcuts.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithKeyboardShortcuts.kt
@@ -20,6 +20,7 @@ import android.app.Instrumentation
import android.tools.NavBar
import android.tools.Rotation
import android.tools.traces.parsers.WindowManagerStateHelper
+import android.window.DesktopModeFlags
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import com.android.launcher3.tapl.LauncherInstrumentation
@@ -59,7 +60,7 @@ abstract class SnapResizeAppWindowWithKeyboardShortcuts(
@Before
fun setup() {
Assume.assumeTrue(Flags.enableDesktopWindowingMode() &&
- Flags.enableTaskResizingKeyboardShortcuts() && tapl.isTablet)
+ DesktopModeFlags.ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS.isTrue && tapl.isTablet)
testApp.enterDesktopMode(wmHelper, device)
}
diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/CopyContentInSplit.kt
index 31d89f92f744..9d501d32fbc7 100644
--- a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/CopyContentInSplit.kt
+++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/CopyContentInSplit.kt
@@ -23,8 +23,8 @@ import android.tools.traces.parsers.WindowManagerStateHelper
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.RecentTasksUtils
import com.android.wm.shell.Utils
-import com.android.wm.shell.flicker.utils.RecentTasksUtils
import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.After
import org.junit.Before
diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByDivider.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByDivider.kt
index 1af6cac39085..f574f02ac3b3 100644
--- a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByDivider.kt
+++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByDivider.kt
@@ -23,8 +23,8 @@ import android.tools.traces.parsers.WindowManagerStateHelper
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.RecentTasksUtils
import com.android.wm.shell.Utils
-import com.android.wm.shell.flicker.utils.RecentTasksUtils
import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.After
import org.junit.Before
diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByGoHome.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByGoHome.kt
index 8ad8c7bd7a7f..60fcce2fbf18 100644
--- a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByGoHome.kt
+++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByGoHome.kt
@@ -23,8 +23,8 @@ import android.tools.traces.parsers.WindowManagerStateHelper
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.RecentTasksUtils
import com.android.wm.shell.Utils
-import com.android.wm.shell.flicker.utils.RecentTasksUtils
import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.After
import org.junit.Before
diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DragDividerToResize.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DragDividerToResize.kt
index da0ace472153..e6a080b2d258 100644
--- a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DragDividerToResize.kt
+++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DragDividerToResize.kt
@@ -23,8 +23,8 @@ import android.tools.traces.parsers.WindowManagerStateHelper
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.RecentTasksUtils
import com.android.wm.shell.Utils
-import com.android.wm.shell.flicker.utils.RecentTasksUtils
import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.After
import org.junit.Before
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenAutoEnterPipOnGoToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenAutoEnterPipOnGoToHomeTest.kt
index f9c60ad14fae..aa893ed65e7c 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenAutoEnterPipOnGoToHomeTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenAutoEnterPipOnGoToHomeTest.kt
@@ -19,7 +19,6 @@ package com.android.wm.shell.flicker.pip
import android.platform.test.annotations.FlakyTest
import android.platform.test.annotations.Presubmit
import android.platform.test.annotations.RequiresDevice
-import android.platform.test.annotations.RequiresFlagsDisabled
import android.tools.Rotation
import android.tools.flicker.junit.FlickerParametersRunnerFactory
import android.tools.flicker.legacy.FlickerBuilder
@@ -29,7 +28,6 @@ import android.tools.helpers.WindowUtils
import android.tools.traces.parsers.toFlickerComponent
import com.android.server.wm.flicker.helpers.SimpleAppHelper
import com.android.server.wm.flicker.testapp.ActivityOptions
-import com.android.wm.shell.Flags
import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.Assume
import org.junit.FixMethodOrder
@@ -64,7 +62,6 @@ import org.junit.runners.Parameterized
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@RequiresFlagsDisabled(Flags.FLAG_ENABLE_PIP2)
open class FromSplitScreenAutoEnterPipOnGoToHomeTest(flicker: LegacyFlickerTest) :
AutoEnterPipOnGoToHomeTest(flicker) {
private val portraitDisplayBounds = WindowUtils.getDisplayBounds(Rotation.ROTATION_0)
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
index 805f4c2fe7f8..8e7cb56c0f07 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
@@ -19,7 +19,6 @@ package com.android.wm.shell.flicker.pip
import android.platform.test.annotations.FlakyTest
import android.platform.test.annotations.Presubmit
import android.platform.test.annotations.RequiresDevice
-import android.platform.test.annotations.RequiresFlagsDisabled
import android.tools.Rotation
import android.tools.flicker.junit.FlickerParametersRunnerFactory
import android.tools.flicker.legacy.FlickerBuilder
@@ -30,7 +29,6 @@ import android.tools.traces.parsers.toFlickerComponent
import com.android.server.wm.flicker.helpers.PipAppHelper
import com.android.server.wm.flicker.helpers.SimpleAppHelper
import com.android.server.wm.flicker.testapp.ActivityOptions
-import com.android.wm.shell.Flags
import com.android.wm.shell.flicker.pip.common.EnterPipTransition
import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.Assume
@@ -66,7 +64,6 @@ import org.junit.runners.Parameterized
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@RequiresFlagsDisabled(Flags.FLAG_ENABLE_PIP2)
@FlakyTest(bugId = 386333280)
open class FromSplitScreenEnterPipOnUserLeaveHintTest(flicker: LegacyFlickerTest) :
EnterPipTransition(flicker) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java
new file mode 100644
index 000000000000..9d0ddbc6de12
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.bubbles;
+
+import static android.view.WindowManager.TRANSIT_CHANGE;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager;
+import android.os.IBinder;
+import android.view.SurfaceControl;
+import android.view.ViewRootImpl;
+import android.window.IWindowContainerToken;
+import android.window.TransitionInfo;
+import android.window.WindowContainerToken;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.icons.BubbleIconFactory;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.TestSyncExecutor;
+import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView;
+import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.taskview.TaskView;
+import com.android.wm.shell.taskview.TaskViewRepository;
+import com.android.wm.shell.taskview.TaskViewTaskController;
+import com.android.wm.shell.taskview.TaskViewTransitions;
+import com.android.wm.shell.transition.Transitions;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests of {@link BubbleTransitions}.
+ */
+@SmallTest
+public class BubbleTransitionsTest extends ShellTestCase {
+ @Mock
+ private BubbleData mBubbleData;
+ @Mock
+ private Bubble mBubble;
+ @Mock
+ private Transitions mTransitions;
+ @Mock
+ private SyncTransactionQueue mSyncQueue;
+ @Mock
+ private BubbleExpandedViewManager mExpandedViewManager;
+ @Mock
+ private BubblePositioner mBubblePositioner;
+ @Mock
+ private BubbleLogger mBubbleLogger;
+ @Mock
+ private BubbleStackView mStackView;
+ @Mock
+ private BubbleBarLayerView mLayerView;
+ @Mock
+ private BubbleIconFactory mIconFactory;
+
+ @Mock private ShellTaskOrganizer mTaskOrganizer;
+ private TaskViewTransitions mTaskViewTransitions;
+ private TaskViewRepository mRepository;
+ private BubbleTransitions mBubbleTransitions;
+ private BubbleTaskViewFactory mTaskViewFactory;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mRepository = new TaskViewRepository();
+ ShellExecutor syncExecutor = new TestSyncExecutor();
+
+ when(mTransitions.getMainExecutor()).thenReturn(syncExecutor);
+ when(mTransitions.isRegistered()).thenReturn(true);
+ mTaskViewTransitions = new TaskViewTransitions(mTransitions, mRepository, mTaskOrganizer,
+ mSyncQueue);
+ mBubbleTransitions = new BubbleTransitions(mTransitions, mTaskOrganizer, mRepository,
+ mBubbleData, mTaskViewTransitions, mContext);
+ mTaskViewFactory = () -> {
+ TaskViewTaskController taskViewTaskController = new TaskViewTaskController(
+ mContext, mTaskOrganizer, mTaskViewTransitions, mSyncQueue);
+ TaskView taskView = new TaskView(mContext, mTaskViewTransitions,
+ taskViewTaskController);
+ return new BubbleTaskView(taskView, syncExecutor);
+ };
+ final BubbleBarExpandedView bbev = mock(BubbleBarExpandedView.class);
+ final ViewRootImpl vri = mock(ViewRootImpl.class);
+ when(bbev.getViewRootImpl()).thenReturn(vri);
+ when(mBubble.getBubbleBarExpandedView()).thenReturn(bbev);
+ }
+
+ private ActivityManager.RunningTaskInfo setupBubble() {
+ ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
+ final IWindowContainerToken itoken = mock(IWindowContainerToken.class);
+ final IBinder asBinder = mock(IBinder.class);
+ when(itoken.asBinder()).thenReturn(asBinder);
+ WindowContainerToken token = new WindowContainerToken(itoken);
+ taskInfo.token = token;
+ final TaskView tv = mock(TaskView.class);
+ final TaskViewTaskController tvtc = mock(TaskViewTaskController.class);
+ when(tvtc.getTaskInfo()).thenReturn(taskInfo);
+ when(tv.getController()).thenReturn(tvtc);
+ when(mBubble.getTaskView()).thenReturn(tv);
+ mRepository.add(tvtc);
+ return taskInfo;
+ }
+
+ @Test
+ public void testConvertToBubble() {
+ // Basic walk-through of convert-to-bubble transition stages
+ ActivityManager.RunningTaskInfo taskInfo = setupBubble();
+ final BubbleTransitions.BubbleTransition bt = mBubbleTransitions.startConvertToBubble(
+ mBubble, taskInfo, mExpandedViewManager, mTaskViewFactory, mBubblePositioner,
+ mBubbleLogger, mStackView, mLayerView, mIconFactory, false);
+ final BubbleTransitions.ConvertToBubble ctb = (BubbleTransitions.ConvertToBubble) bt;
+ ctb.onInflated(mBubble);
+ when(mLayerView.canExpandView(any())).thenReturn(true);
+ verify(mTransitions).startTransition(anyInt(), any(), eq(ctb));
+ verify(mBubble).setPreparingTransition(eq(bt));
+ // Ensure we are communicating with the taskviewtransitions queue
+ assertTrue(mTaskViewTransitions.hasPending());
+
+ final TransitionInfo info = new TransitionInfo(TRANSIT_CHANGE, 0);
+ final TransitionInfo.Change chg = new TransitionInfo.Change(taskInfo.token,
+ mock(SurfaceControl.class));
+ chg.setTaskInfo(taskInfo);
+ chg.setMode(TRANSIT_CHANGE);
+ info.addChange(chg);
+ info.addRoot(new TransitionInfo.Root(0, mock(SurfaceControl.class), 0, 0));
+ SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
+ SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
+ final boolean[] finishCalled = new boolean[]{false};
+ Transitions.TransitionFinishCallback finishCb = wct -> {
+ assertFalse(finishCalled[0]);
+ finishCalled[0] = true;
+ };
+ ctb.startAnimation(ctb.mTransition, info, startT, finishT, finishCb);
+ assertFalse(mTaskViewTransitions.hasPending());
+
+ verify(mBubbleData).notificationEntryUpdated(eq(mBubble), anyBoolean(), anyBoolean());
+ ctb.continueExpand();
+
+ clearInvocations(mBubble);
+ verify(mBubble, never()).setPreparingTransition(any());
+
+ ctb.surfaceCreated();
+ verify(mBubble).setPreparingTransition(isNull());
+ ArgumentCaptor<Runnable> animCb = ArgumentCaptor.forClass(Runnable.class);
+ verify(mLayerView).animateConvert(any(), any(), any(), any(), animCb.capture());
+ assertFalse(finishCalled[0]);
+ animCb.getValue().run();
+ assertTrue(finishCalled[0]);
+ }
+
+ @Test
+ public void testConvertFromBubble() {
+ ActivityManager.RunningTaskInfo taskInfo = setupBubble();
+ final BubbleTransitions.BubbleTransition bt = mBubbleTransitions.startConvertFromBubble(
+ mBubble, taskInfo);
+ final BubbleTransitions.ConvertFromBubble cfb = (BubbleTransitions.ConvertFromBubble) bt;
+ verify(mTransitions).startTransition(anyInt(), any(), eq(cfb));
+ verify(mBubble).setPreparingTransition(eq(bt));
+ assertTrue(mTaskViewTransitions.hasPending());
+
+ final TransitionInfo info = new TransitionInfo(TRANSIT_CHANGE, 0);
+ final TransitionInfo.Change chg = new TransitionInfo.Change(taskInfo.token,
+ mock(SurfaceControl.class));
+ chg.setMode(TRANSIT_CHANGE);
+ chg.setTaskInfo(taskInfo);
+ info.addChange(chg);
+ info.addRoot(new TransitionInfo.Root(0, mock(SurfaceControl.class), 0, 0));
+ SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
+ SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
+ Transitions.TransitionFinishCallback finishCb = wct -> {};
+ cfb.startAnimation(cfb.mTransition, info, startT, finishT, finishCb);
+
+ // Can really only verify that it interfaces with the taskViewTransitions queue.
+ // The actual functioning of this is tightly-coupled with SurfaceFlinger and renderthread
+ // in order to properly synchronize surface manipulation with drawing and thus can't be
+ // directly tested.
+ assertFalse(mTaskViewTransitions.hasPending());
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt
index bec91e910cf7..6b0c390ac239 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt
@@ -33,8 +33,6 @@ import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
import org.mockito.ArgumentMatchers.eq
import org.mockito.kotlin.any
-import org.mockito.kotlin.doReturn
-import org.mockito.kotlin.doThrow
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
@@ -80,17 +78,19 @@ class MultiInstanceHelperTest : ShellTestCase() {
@Test
fun supportsMultiInstanceSplit_inStaticAllowList() {
val allowList = arrayOf(TEST_PACKAGE)
- val helper = MultiInstanceHelper(mContext, context.packageManager, allowList, true)
+ val helper = MultiInstanceHelper(mContext, context.packageManager, allowList,
+ mock(), mock(), true)
val component = ComponentName(TEST_PACKAGE, TEST_ACTIVITY)
- assertEquals(true, helper.supportsMultiInstanceSplit(component))
+ assertEquals(true, helper.supportsMultiInstanceSplit(component, TEST_OTHER_USER_ID))
}
@Test
fun supportsMultiInstanceSplit_notInStaticAllowList() {
val allowList = arrayOf(TEST_PACKAGE)
- val helper = MultiInstanceHelper(mContext, context.packageManager, allowList, true)
+ val helper = MultiInstanceHelper(mContext, context.packageManager, allowList,
+ mock(), mock(), true)
val component = ComponentName(TEST_NOT_ALLOWED_PACKAGE, TEST_ACTIVITY)
- assertEquals(false, helper.supportsMultiInstanceSplit(component))
+ assertEquals(false, helper.supportsMultiInstanceSplit(component, TEST_OTHER_USER_ID))
}
@Test
@@ -99,17 +99,17 @@ class MultiInstanceHelperTest : ShellTestCase() {
val component = ComponentName(TEST_PACKAGE, TEST_ACTIVITY)
val pm = mock<PackageManager>()
val activityProp = PackageManager.Property("", true, "", "")
- whenever(pm.getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
- eq(component)))
+ whenever(pm.getPropertyAsUser(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
+ eq(component.packageName), eq(component.className), eq(TEST_OTHER_USER_ID)))
.thenReturn(activityProp)
val appProp = PackageManager.Property("", false, "", "")
- whenever(pm.getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
- eq(component.packageName)))
+ whenever(pm.getPropertyAsUser(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
+ eq(component.packageName), eq(null), eq(TEST_OTHER_USER_ID)))
.thenReturn(appProp)
- val helper = MultiInstanceHelper(mContext, pm, emptyArray(), true)
+ val helper = MultiInstanceHelper(mContext, pm, emptyArray(), mock(), mock(), true)
// Expect activity property to override application property
- assertEquals(true, helper.supportsMultiInstanceSplit(component))
+ assertEquals(true, helper.supportsMultiInstanceSplit(component, TEST_OTHER_USER_ID))
}
@Test
@@ -118,17 +118,17 @@ class MultiInstanceHelperTest : ShellTestCase() {
val component = ComponentName(TEST_PACKAGE, TEST_ACTIVITY)
val pm = mock<PackageManager>()
val activityProp = PackageManager.Property("", false, "", "")
- whenever(pm.getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
- eq(component)))
+ whenever(pm.getPropertyAsUser(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
+ eq(component.packageName), eq(component.className), eq(TEST_OTHER_USER_ID)))
.thenReturn(activityProp)
val appProp = PackageManager.Property("", true, "", "")
- whenever(pm.getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
- eq(component.packageName)))
+ whenever(pm.getPropertyAsUser(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
+ eq(component.packageName), eq(null), eq(TEST_OTHER_USER_ID)))
.thenReturn(appProp)
- val helper = MultiInstanceHelper(mContext, pm, emptyArray(), true)
+ val helper = MultiInstanceHelper(mContext, pm, emptyArray(), mock(), mock(), true)
// Expect activity property to override application property
- assertEquals(false, helper.supportsMultiInstanceSplit(component))
+ assertEquals(false, helper.supportsMultiInstanceSplit(component, TEST_OTHER_USER_ID))
}
@Test
@@ -136,17 +136,17 @@ class MultiInstanceHelperTest : ShellTestCase() {
fun supportsMultiInstanceSplit_noActivityPropertyApplicationPropertyTrue() {
val component = ComponentName(TEST_PACKAGE, TEST_ACTIVITY)
val pm = mock<PackageManager>()
- whenever(pm.getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
- eq(component)))
+ whenever(pm.getPropertyAsUser(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
+ eq(component.packageName), eq(component.className), eq(TEST_OTHER_USER_ID)))
.thenThrow(PackageManager.NameNotFoundException())
val appProp = PackageManager.Property("", true, "", "")
- whenever(pm.getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
- eq(component.packageName)))
+ whenever(pm.getPropertyAsUser(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
+ eq(component.packageName), eq(null), eq(TEST_OTHER_USER_ID)))
.thenReturn(appProp)
- val helper = MultiInstanceHelper(mContext, pm, emptyArray(), true)
+ val helper = MultiInstanceHelper(mContext, pm, emptyArray(), mock(), mock(), true)
// Expect fall through to app property
- assertEquals(true, helper.supportsMultiInstanceSplit(component))
+ assertEquals(true, helper.supportsMultiInstanceSplit(component, TEST_OTHER_USER_ID))
}
@Test
@@ -154,15 +154,15 @@ class MultiInstanceHelperTest : ShellTestCase() {
fun supportsMultiInstanceSplit_noActivityOrAppProperty() {
val component = ComponentName(TEST_PACKAGE, TEST_ACTIVITY)
val pm = mock<PackageManager>()
- whenever(pm.getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
- eq(component)))
+ whenever(pm.getPropertyAsUser(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
+ eq(component.packageName), eq(component.className), eq(TEST_OTHER_USER_ID)))
.thenThrow(PackageManager.NameNotFoundException())
- whenever(pm.getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
- eq(component.packageName)))
+ whenever(pm.getPropertyAsUser(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
+ eq(component.packageName), eq(null), eq(TEST_OTHER_USER_ID)))
.thenThrow(PackageManager.NameNotFoundException())
- val helper = MultiInstanceHelper(mContext, pm, emptyArray(), true)
- assertEquals(false, helper.supportsMultiInstanceSplit(component))
+ val helper = MultiInstanceHelper(mContext, pm, emptyArray(), mock(), mock(), true)
+ assertEquals(false, helper.supportsMultiInstanceSplit(component, TEST_OTHER_USER_ID))
}
@Test
@@ -171,24 +171,25 @@ class MultiInstanceHelperTest : ShellTestCase() {
val component = ComponentName(TEST_PACKAGE, TEST_ACTIVITY)
val pm = mock<PackageManager>()
val activityProp = PackageManager.Property("", true, "", "")
- whenever(pm.getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
- eq(component)))
+ whenever(pm.getPropertyAsUser(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
+ eq(component.packageName), eq(component.className), eq(TEST_OTHER_USER_ID)))
.thenReturn(activityProp)
val appProp = PackageManager.Property("", true, "", "")
- whenever(pm.getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
- eq(component.packageName)))
+ whenever(pm.getPropertyAsUser(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
+ eq(component.packageName), eq(null), eq(TEST_OTHER_USER_ID)))
.thenReturn(appProp)
- val helper = MultiInstanceHelper(mContext, pm, emptyArray(), false)
+ val helper = MultiInstanceHelper(mContext, pm, emptyArray(), mock(), mock(), false)
// Expect we only check the static list and not the property
- assertEquals(false, helper.supportsMultiInstanceSplit(component))
+ assertEquals(false, helper.supportsMultiInstanceSplit(component, TEST_OTHER_USER_ID))
verify(pm, never()).getProperty(any(), any<ComponentName>())
}
companion object {
val TEST_PACKAGE = "com.android.wm.shell.common"
- val TEST_NOT_ALLOWED_PACKAGE = "com.android.wm.shell.common.fake";
- val TEST_ACTIVITY = "TestActivity";
+ val TEST_NOT_ALLOWED_PACKAGE = "com.android.wm.shell.common.fake"
+ val TEST_ACTIVITY = "TestActivity"
val TEST_SHORTCUT_ID = "test_shortcut_1"
+ val TEST_OTHER_USER_ID = 1234
}
} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/UserProfileContextsTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/UserProfileContextsTest.kt
new file mode 100644
index 000000000000..ef0b8ab14c81
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/UserProfileContextsTest.kt
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common
+
+import android.app.ActivityManager
+import android.content.Context
+import android.content.pm.UserInfo
+import android.os.UserHandle
+import android.os.UserManager
+import android.testing.AndroidTestingRunner
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestShellExecutor
+import com.android.wm.shell.sysui.ShellController
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.sysui.UserChangeListener
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyInt
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+/**
+ * Tests for [UserProfileContexts].
+ */
+@RunWith(AndroidTestingRunner::class)
+class UserProfileContextsTest : ShellTestCase() {
+
+ private val testExecutor = TestShellExecutor()
+ private val shellInit = ShellInit(testExecutor)
+ private val activityManager = mock<ActivityManager>()
+ private val userManager = mock<UserManager>()
+ private val shellController = mock<ShellController>()
+ private val baseContext = mock<Context>()
+
+ private lateinit var userProfilesContexts: UserProfileContexts
+
+ @Before
+ fun setUp() {
+ doReturn(activityManager)
+ .whenever(baseContext)
+ .getSystemService(eq(ActivityManager::class.java))
+ doReturn(userManager).whenever(baseContext).getSystemService(eq(UserManager::class.java))
+ doAnswer { invocation ->
+ val userHandle = invocation.getArgument<UserHandle>(0)
+ createContextForUser(userHandle.identifier)
+ }
+ .whenever(baseContext)
+ .createContextAsUser(any<UserHandle>(), anyInt())
+ // Define users and profiles
+ val currentUser = ActivityManager.getCurrentUser()
+ whenever(userManager.getProfiles(eq(currentUser)))
+ .thenReturn(
+ listOf(UserInfo(currentUser, "Current", 0), UserInfo(MAIN_PROFILE, "Work", 0))
+ )
+ whenever(userManager.getProfiles(eq(SECOND_USER))).thenReturn(SECOND_PROFILES)
+ userProfilesContexts = UserProfileContexts(baseContext, shellController, shellInit)
+ shellInit.init()
+ }
+
+ @Test
+ fun onInit_registerUserChangeAndInit() {
+ val currentUser = ActivityManager.getCurrentUser()
+
+ verify(shellController, times(1)).addUserChangeListener(any())
+ assertThat(userProfilesContexts.userContext.userId).isEqualTo(currentUser)
+ assertThat(userProfilesContexts[currentUser]?.userId).isEqualTo(currentUser)
+ assertThat(userProfilesContexts[MAIN_PROFILE]?.userId).isEqualTo(MAIN_PROFILE)
+ assertThat(userProfilesContexts[SECOND_USER]).isNull()
+ }
+
+ @Test
+ fun onUserChanged_updateUserContext() {
+ val userChangeListener = retrieveUserChangeListener()
+ val newUserContext = createContextForUser(SECOND_USER)
+
+ userChangeListener.onUserChanged(SECOND_USER, newUserContext)
+
+ assertThat(userProfilesContexts.userContext).isEqualTo(newUserContext)
+ assertThat(userProfilesContexts[SECOND_USER]).isEqualTo(newUserContext)
+ }
+
+ @Test
+ fun onUserProfilesChanged_updateAllContexts() {
+ val userChangeListener = retrieveUserChangeListener()
+ val newUserContext = createContextForUser(SECOND_USER)
+ userChangeListener.onUserChanged(SECOND_USER, newUserContext)
+
+ userChangeListener.onUserProfilesChanged(SECOND_PROFILES)
+
+ assertThat(userProfilesContexts.userContext).isEqualTo(newUserContext)
+ assertThat(userProfilesContexts[SECOND_USER]).isEqualTo(newUserContext)
+ assertThat(userProfilesContexts[SECOND_PROFILE]?.userId).isEqualTo(SECOND_PROFILE)
+ assertThat(userProfilesContexts[SECOND_PROFILE_2]?.userId).isEqualTo(SECOND_PROFILE_2)
+ }
+
+ @Test
+ fun onUserProfilesChanged_keepOnlyNewProfiles() {
+ val userChangeListener = retrieveUserChangeListener()
+ val newUserContext = createContextForUser(SECOND_USER)
+ userChangeListener.onUserChanged(SECOND_USER, newUserContext)
+ userChangeListener.onUserProfilesChanged(SECOND_PROFILES)
+ val newProfiles = listOf(
+ UserInfo(SECOND_USER, "Second", 0),
+ UserInfo(SECOND_PROFILE, "Second Profile", 0),
+ UserInfo(MAIN_PROFILE, "Main profile", 0),
+ )
+
+ userChangeListener.onUserProfilesChanged(newProfiles)
+
+ assertThat(userProfilesContexts[SECOND_PROFILE_2]).isNull()
+ assertThat(userProfilesContexts[MAIN_PROFILE]?.userId).isEqualTo(MAIN_PROFILE)
+ assertThat(userProfilesContexts[SECOND_USER]?.userId).isEqualTo(SECOND_USER)
+ assertThat(userProfilesContexts[SECOND_PROFILE]?.userId).isEqualTo(SECOND_PROFILE)
+ }
+
+ private fun retrieveUserChangeListener(): UserChangeListener {
+ val captor = argumentCaptor<UserChangeListener>()
+
+ verify(shellController, times(1)).addUserChangeListener(captor.capture())
+
+ return captor.firstValue
+ }
+
+ private fun createContextForUser(userId: Int): Context {
+ val newContext = mock<Context>()
+ whenever(newContext.userId).thenReturn(userId)
+ return newContext
+ }
+
+ private companion object {
+ const val SECOND_USER = 3
+ const val MAIN_PROFILE = 11
+ const val SECOND_PROFILE = 15
+ const val SECOND_PROFILE_2 = 17
+
+ val SECOND_PROFILES =
+ listOf(
+ UserInfo(SECOND_USER, "Second", 0),
+ UserInfo(SECOND_PROFILE, "Second Profile", 0),
+ UserInfo(SECOND_PROFILE_2, "Second Profile 2", 0),
+ )
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt
index ecad5217b87f..957fdf995776 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt
@@ -183,6 +183,8 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() {
@Test
fun handleActivityOrientationChange_resizeable_doNothing() {
+ userRepositories.current.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ userRepositories.current.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
val task = setUpFreeformTask()
taskStackListener.onActivityRequestedOrientationChanged(
@@ -195,6 +197,8 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() {
@Test
fun handleActivityOrientationChange_nonResizeableFullscreen_doNothing() {
+ userRepositories.current.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ userRepositories.current.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
val task = createFullscreenTask()
task.isResizeable = false
val activityInfo = ActivityInfo()
@@ -214,6 +218,8 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() {
@Test
fun handleActivityOrientationChange_nonResizeablePortrait_requestSameOrientation_doNothing() {
+ userRepositories.current.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ userRepositories.current.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
val task = setUpFreeformTask(isResizeable = false)
val newTask =
setUpFreeformTask(
@@ -228,6 +234,8 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() {
@Test
fun handleActivityOrientationChange_notInDesktopMode_doNothing() {
+ userRepositories.current.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ userRepositories.current.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
val task = setUpFreeformTask(isResizeable = false)
userRepositories.current.updateTask(task.displayId, task.taskId, isVisible = false)
@@ -241,6 +249,8 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() {
@Test
fun handleActivityOrientationChange_nonResizeablePortrait_respectLandscapeRequest() {
+ userRepositories.current.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ userRepositories.current.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
val task = setUpFreeformTask(isResizeable = false)
val oldBounds = task.configuration.windowConfiguration.bounds
val newTask =
@@ -263,6 +273,8 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() {
@Test
fun handleActivityOrientationChange_nonResizeableLandscape_respectPortraitRequest() {
+ userRepositories.current.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ userRepositories.current.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
val oldBounds = Rect(0, 0, 500, 200)
val task =
setUpFreeformTask(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt
index 6a3717427e93..fae7363e0676 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt
@@ -16,10 +16,14 @@
package com.android.wm.shell.desktopmode
+import android.app.ActivityManager.RunningTaskInfo
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
import android.content.ContentResolver
import android.os.Binder
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
import android.provider.Settings
import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS
import android.testing.AndroidTestingRunner
@@ -29,20 +33,27 @@ import android.view.WindowManager.TRANSIT_CHANGE
import android.window.DisplayAreaInfo
import android.window.WindowContainerTransaction
import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
import com.android.dx.mockito.inline.extended.ExtendedMockito.never
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
+import com.android.window.flags.Flags
import com.android.wm.shell.MockToken
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestRunningTaskInfoBuilder
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener
import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.transition.Transitions
import com.google.common.truth.Truth.assertThat
+import org.junit.After
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.isNull
import org.mockito.Mock
import org.mockito.Mockito.anyInt
@@ -53,6 +64,7 @@ import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.eq
import org.mockito.kotlin.whenever
+import org.mockito.quality.Strictness
/**
* Test class for [DesktopDisplayEventHandler]
@@ -63,21 +75,44 @@ import org.mockito.kotlin.whenever
@RunWith(AndroidTestingRunner::class)
class DesktopDisplayEventHandlerTest : ShellTestCase() {
+ @JvmField @Rule val setFlagsRule = SetFlagsRule()
+
@Mock lateinit var testExecutor: ShellExecutor
@Mock lateinit var transitions: Transitions
@Mock lateinit var displayController: DisplayController
@Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
@Mock private lateinit var mockWindowManager: IWindowManager
+ @Mock private lateinit var mockDesktopUserRepositories: DesktopUserRepositories
+ @Mock private lateinit var mockDesktopRepository: DesktopRepository
+ @Mock private lateinit var mockDesktopTasksController: DesktopTasksController
+ @Mock private lateinit var shellTaskOrganizer: ShellTaskOrganizer
+ private lateinit var mockitoSession: StaticMockitoSession
private lateinit var shellInit: ShellInit
private lateinit var handler: DesktopDisplayEventHandler
+ private val onDisplaysChangedListenerCaptor = argumentCaptor<OnDisplaysChangedListener>()
+ private val runningTasks = mutableListOf<RunningTaskInfo>()
+ private val externalDisplayId = 100
+ private val freeformTask =
+ TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build()
+ private val fullscreenTask =
+ TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FULLSCREEN).build()
+ private val defaultTDA = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0)
+
@Before
fun setUp() {
+ mockitoSession =
+ mockitoSession()
+ .strictness(Strictness.LENIENT)
+ .spyStatic(DesktopModeStatus::class.java)
+ .startMocking()
+
shellInit = spy(ShellInit(testExecutor))
+ whenever(mockDesktopUserRepositories.current).thenReturn(mockDesktopRepository)
whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() }
- val tda = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0)
- whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda)
+ whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
+ .thenReturn(defaultTDA)
handler =
DesktopDisplayEventHandler(
context,
@@ -86,8 +121,21 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() {
displayController,
rootTaskDisplayAreaOrganizer,
mockWindowManager,
+ mockDesktopUserRepositories,
+ mockDesktopTasksController,
+ shellTaskOrganizer,
)
+ whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks }
+ runningTasks.add(freeformTask)
+ runningTasks.add(fullscreenTask)
shellInit.init()
+ verify(displayController)
+ .addDisplayWindowListener(onDisplaysChangedListenerCaptor.capture())
+ }
+
+ @After
+ fun tearDown() {
+ mockitoSession.finishMocking()
}
private fun testDisplayWindowingModeSwitch(
@@ -95,11 +143,7 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() {
extendedDisplayEnabled: Boolean,
expectTransition: Boolean,
) {
- val externalDisplayId = 100
- val captor = ArgumentCaptor.forClass(OnDisplaysChangedListener::class.java)
- verify(displayController).addDisplayWindowListener(captor.capture())
- val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
- tda.configuration.windowConfiguration.windowingMode = defaultWindowingMode
+ defaultTDA.configuration.windowConfiguration.windowingMode = defaultWindowingMode
whenever(mockWindowManager.getWindowingMode(anyInt())).thenAnswer { defaultWindowingMode }
val settingsSession =
ExtendedDisplaySettingsSession(
@@ -108,23 +152,17 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() {
)
settingsSession.use {
- // The external display connected.
- whenever(rootTaskDisplayAreaOrganizer.getDisplayIds())
- .thenReturn(intArrayOf(DEFAULT_DISPLAY, externalDisplayId))
- captor.value.onDisplayAdded(externalDisplayId)
- tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
- // The external display disconnected.
- whenever(rootTaskDisplayAreaOrganizer.getDisplayIds())
- .thenReturn(intArrayOf(DEFAULT_DISPLAY))
- captor.value.onDisplayRemoved(externalDisplayId)
+ connectExternalDisplay()
+ defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+ disconnectExternalDisplay()
if (expectTransition) {
val arg = argumentCaptor<WindowContainerTransaction>()
verify(transitions, times(2))
.startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull())
- assertThat(arg.firstValue.changes[tda.token.asBinder()]?.windowingMode)
+ assertThat(arg.firstValue.changes[defaultTDA.token.asBinder()]?.windowingMode)
.isEqualTo(WINDOWING_MODE_FREEFORM)
- assertThat(arg.secondValue.changes[tda.token.asBinder()]?.windowingMode)
+ assertThat(arg.secondValue.changes[defaultTDA.token.asBinder()]?.windowingMode)
.isEqualTo(defaultWindowingMode)
} else {
verify(transitions, never()).startTransition(eq(TRANSIT_CHANGE), any(), isNull())
@@ -159,6 +197,96 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() {
)
}
+ @Test
+ fun testDisplayAdded_supportsDesks_createsDesk() {
+ whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
+
+ onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY)
+
+ verify(mockDesktopTasksController).createDesk(DEFAULT_DISPLAY)
+ }
+
+ @Test
+ fun testDisplayAdded_cannotEnterDesktopMode_doesNotCreateDesk() {
+ whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(false)
+
+ onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY)
+
+ verify(mockDesktopTasksController, never()).createDesk(DEFAULT_DISPLAY)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun testDeskRemoved_noDesksRemain_createsDesk() {
+ whenever(mockDesktopRepository.getNumberOfDesks(DEFAULT_DISPLAY)).thenReturn(0)
+
+ handler.onDeskRemoved(DEFAULT_DISPLAY, deskId = 1)
+
+ verify(mockDesktopTasksController).createDesk(DEFAULT_DISPLAY)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun testDeskRemoved_desksRemain_doesNotCreateDesk() {
+ whenever(mockDesktopRepository.getNumberOfDesks(DEFAULT_DISPLAY)).thenReturn(1)
+
+ handler.onDeskRemoved(DEFAULT_DISPLAY, deskId = 1)
+
+ verify(mockDesktopTasksController, never()).createDesk(DEFAULT_DISPLAY)
+ }
+
+ @Test
+ fun displayWindowingModeSwitch_existingTasksOnConnected() {
+ defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+ whenever(mockWindowManager.getWindowingMode(anyInt())).thenAnswer {
+ WINDOWING_MODE_FULLSCREEN
+ }
+
+ ExtendedDisplaySettingsSession(context.contentResolver, 1).use {
+ connectExternalDisplay()
+
+ val arg = argumentCaptor<WindowContainerTransaction>()
+ verify(transitions, times(1))
+ .startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull())
+ assertThat(arg.firstValue.changes[freeformTask.token.asBinder()]?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_UNDEFINED)
+ assertThat(arg.firstValue.changes[fullscreenTask.token.asBinder()]?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_FULLSCREEN)
+ }
+ }
+
+ @Test
+ fun displayWindowingModeSwitch_existingTasksOnDisconnected() {
+ defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+ whenever(mockWindowManager.getWindowingMode(anyInt())).thenAnswer {
+ WINDOWING_MODE_FULLSCREEN
+ }
+
+ ExtendedDisplaySettingsSession(context.contentResolver, 1).use {
+ disconnectExternalDisplay()
+
+ val arg = argumentCaptor<WindowContainerTransaction>()
+ verify(transitions, times(1))
+ .startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull())
+ assertThat(arg.firstValue.changes[freeformTask.token.asBinder()]?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_FREEFORM)
+ assertThat(arg.firstValue.changes[fullscreenTask.token.asBinder()]?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_UNDEFINED)
+ }
+ }
+
+ private fun connectExternalDisplay() {
+ whenever(rootTaskDisplayAreaOrganizer.getDisplayIds())
+ .thenReturn(intArrayOf(DEFAULT_DISPLAY, externalDisplayId))
+ onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(externalDisplayId)
+ }
+
+ private fun disconnectExternalDisplay() {
+ whenever(rootTaskDisplayAreaOrganizer.getDisplayIds())
+ .thenReturn(intArrayOf(DEFAULT_DISPLAY))
+ onDisplaysChangedListenerCaptor.lastValue.onDisplayRemoved(externalDisplayId)
+ }
+
private class ExtendedDisplaySettingsSession(
private val contentResolver: ContentResolver,
private val overrideValue: Int,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
index 8d73f3f59afd..f5c93ee8ffe4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
@@ -17,13 +17,15 @@
package com.android.wm.shell.desktopmode
import android.graphics.Rect
+import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.FlagsParameterization
import android.platform.test.flag.junit.SetFlagsRule
-import android.testing.AndroidTestingRunner
import android.util.ArraySet
import android.view.Display.DEFAULT_DISPLAY
import android.view.Display.INVALID_DISPLAY
import androidx.test.filters.SmallTest
+import com.android.window.flags.Flags
import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE
import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP
import com.android.wm.shell.ShellTestCase
@@ -56,6 +58,8 @@ import org.mockito.kotlin.never
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
/**
* Tests for [@link DesktopRepository].
@@ -63,11 +67,11 @@ import org.mockito.kotlin.whenever
* Build/Install/Run: atest WMShellUnitTests:DesktopRepositoryTest
*/
@SmallTest
-@RunWith(AndroidTestingRunner::class)
+@RunWith(ParameterizedAndroidJunit4::class)
@ExperimentalCoroutinesApi
-class DesktopRepositoryTest : ShellTestCase() {
+class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() {
- @JvmField @Rule val setFlagsRule = SetFlagsRule()
+ @JvmField @Rule val setFlagsRule = SetFlagsRule(flags)
private lateinit var repo: DesktopRepository
private lateinit var shellInit: ShellInit
@@ -86,6 +90,8 @@ class DesktopRepositoryTest : ShellTestCase() {
whenever(runBlocking { persistentRepository.readDesktop(any(), any()) })
.thenReturn(Desktop.getDefaultInstance())
shellInit.init()
+ repo.addDesk(displayId = DEFAULT_DISPLAY, deskId = DEFAULT_DESKTOP_ID)
+ repo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = DEFAULT_DESKTOP_ID)
}
@After
@@ -137,6 +143,7 @@ class DesktopRepositoryTest : ShellTestCase() {
@Test
fun addTask_multipleDisplays_notifiesCorrectListener() {
+ repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
val listener = TestListener()
repo.addActiveTaskListener(listener)
@@ -150,6 +157,7 @@ class DesktopRepositoryTest : ShellTestCase() {
@Test
fun addTask_multipleDisplays_moveToAnotherDisplay() {
+ repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true)
repo.addTask(SECOND_DISPLAY, taskId = 1, isVisible = true)
assertThat(repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY)).isEmpty()
@@ -310,19 +318,21 @@ class DesktopRepositoryTest : ShellTestCase() {
@Test
fun isOnlyVisibleNonClosingTask_multipleDisplays() {
+ repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
+ repo.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true)
repo.updateTask(DEFAULT_DISPLAY, taskId = 2, isVisible = true)
repo.updateTask(SECOND_DISPLAY, taskId = 3, isVisible = true)
// Not the only task on DEFAULT_DISPLAY
assertThat(repo.isVisibleTask(1)).isTrue()
- assertThat(repo.isOnlyVisibleNonClosingTask(1)).isFalse()
+ assertThat(repo.isOnlyVisibleNonClosingTask(1, DEFAULT_DISPLAY)).isFalse()
// Not the only task on DEFAULT_DISPLAY
assertThat(repo.isVisibleTask(2)).isTrue()
- assertThat(repo.isOnlyVisibleNonClosingTask(2)).isFalse()
+ assertThat(repo.isOnlyVisibleNonClosingTask(2, DEFAULT_DISPLAY)).isFalse()
// The only visible task on SECOND_DISPLAY
assertThat(repo.isVisibleTask(3)).isTrue()
- assertThat(repo.isOnlyVisibleNonClosingTask(3)).isTrue()
+ assertThat(repo.isOnlyVisibleNonClosingTask(3, SECOND_DISPLAY)).isTrue()
// Not a visible task
assertThat(repo.isVisibleTask(99)).isFalse()
assertThat(repo.isOnlyVisibleNonClosingTask(99)).isFalse()
@@ -343,6 +353,7 @@ class DesktopRepositoryTest : ShellTestCase() {
@Test
fun addListener_tasksOnDifferentDisplay_doesNotNotify() {
+ repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
repo.updateTask(SECOND_DISPLAY, taskId = 1, isVisible = true)
val listener = TestVisibilityListener()
val executor = TestShellExecutor()
@@ -351,7 +362,7 @@ class DesktopRepositoryTest : ShellTestCase() {
assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(0)
// One call as adding listener notifies it
- assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(0)
+ assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1)
}
@Test
@@ -365,11 +376,14 @@ class DesktopRepositoryTest : ShellTestCase() {
executor.flushAll()
assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(2)
- assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(2)
+ // 1 from registration, 2 for the updates.
+ assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(3)
}
@Test
fun updateTask_visibleTask_addVisibleTaskNotifiesListenerForThatDisplay() {
+ repo.addDesk(displayId = 1, deskId = 1)
+ repo.setActiveDesk(displayId = 1, deskId = 1)
val listener = TestVisibilityListener()
val executor = TestShellExecutor()
repo.addVisibleTasksListener(listener, executor)
@@ -378,22 +392,27 @@ class DesktopRepositoryTest : ShellTestCase() {
executor.flushAll()
assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(1)
- assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1)
+ // 1 for the registration, 1 for the update.
+ assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(2)
assertThat(listener.visibleTasksCountOnSecondaryDisplay).isEqualTo(0)
- assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(0)
+ // 1 for the registration.
+ assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(1)
repo.updateTask(displayId = 1, taskId = 2, isVisible = true)
executor.flushAll()
// Listener for secondary display is notified
assertThat(listener.visibleTasksCountOnSecondaryDisplay).isEqualTo(1)
- assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(1)
+ // 1 for the registration, 1 for the update.
+ assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(2)
// No changes to listener for default display
- assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1)
+ assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(2)
}
@Test
fun updateTask_taskOnDefaultBecomesVisibleOnSecondDisplay_listenersNotified() {
+ repo.addDesk(displayId = 1, deskId = 1)
+ repo.setActiveDesk(displayId = 1, deskId = 1)
val listener = TestVisibilityListener()
val executor = TestShellExecutor()
repo.addVisibleTasksListener(listener, executor)
@@ -406,14 +425,15 @@ class DesktopRepositoryTest : ShellTestCase() {
repo.updateTask(displayId = 1, taskId = 1, isVisible = true)
executor.flushAll()
- // Default display should have 2 calls
- // 1 - visible task added
- // 2 - visible task removed
- assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(2)
+ // Default display should have 3 calls
+ // 1 - listener registered
+ // 2 - visible task added
+ // 3 - visible task removed
+ assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(3)
assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(0)
- // Secondary display should have 1 call for visible task added
- assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(1)
+ // Secondary display should have 2 calls for registration + visible task added
+ assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(2)
assertThat(listener.visibleTasksCountOnSecondaryDisplay).isEqualTo(1)
}
@@ -431,13 +451,13 @@ class DesktopRepositoryTest : ShellTestCase() {
repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = false)
executor.flushAll()
- assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(3)
+ assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(4)
repo.updateTask(DEFAULT_DISPLAY, taskId = 2, isVisible = false)
executor.flushAll()
assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(0)
- assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(4)
+ assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(5)
}
/**
@@ -458,7 +478,8 @@ class DesktopRepositoryTest : ShellTestCase() {
repo.updateTask(INVALID_DISPLAY, taskId = 1, isVisible = false)
executor.flushAll()
- assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(3)
+ // 1 from registering, 1x3 for each update including the one to the invalid display.
+ assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(4)
assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(1)
}
@@ -497,6 +518,8 @@ class DesktopRepositoryTest : ShellTestCase() {
@Test
fun getVisibleTaskCount_multipleDisplays_returnsCorrectCount() {
+ repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
+ repo.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(0)
@@ -674,8 +697,6 @@ class DesktopRepositoryTest : ShellTestCase() {
repo.removeTask(INVALID_DISPLAY, taskId = 1)
- val invalidDisplayTasks = repo.getFreeformTasksInZOrder(INVALID_DISPLAY)
- assertThat(invalidDisplayTasks).isEmpty()
val validDisplayTasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY)
assertThat(validDisplayTasks).isEmpty()
}
@@ -746,6 +767,7 @@ class DesktopRepositoryTest : ShellTestCase() {
@Test
fun removeTask_validDisplay_differentDisplay_doesNotRemovesTask() {
+ repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true)
repo.removeTask(SECOND_DISPLAY, taskId = 1)
@@ -758,6 +780,7 @@ class DesktopRepositoryTest : ShellTestCase() {
@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE)
fun removeTask_validDisplayButDifferentDisplay_persistenceEnabled_doesNotRemoveTask() {
runTest(StandardTestDispatcher()) {
+ repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true)
repo.removeTask(SECOND_DISPLAY, taskId = 1)
@@ -784,10 +807,10 @@ class DesktopRepositoryTest : ShellTestCase() {
@Test
fun removeTask_removesTaskBoundsBeforeMaximize() {
val taskId = 1
- repo.addTask(THIRD_DISPLAY, taskId, isVisible = true)
+ repo.addTask(DEFAULT_DISPLAY, taskId, isVisible = true)
repo.saveBoundsBeforeMaximize(taskId, Rect(0, 0, 200, 200))
- repo.removeTask(THIRD_DISPLAY, taskId)
+ repo.removeTask(DEFAULT_DISPLAY, taskId)
assertThat(repo.removeBoundsBeforeMaximize(taskId)).isNull()
}
@@ -795,16 +818,17 @@ class DesktopRepositoryTest : ShellTestCase() {
@Test
fun removeTask_removesTaskBoundsBeforeImmersive() {
val taskId = 1
- repo.addTask(THIRD_DISPLAY, taskId, isVisible = true)
+ repo.addTask(DEFAULT_DISPLAY, taskId, isVisible = true)
repo.saveBoundsBeforeFullImmersive(taskId, Rect(0, 0, 200, 200))
- repo.removeTask(THIRD_DISPLAY, taskId)
+ repo.removeTask(DEFAULT_DISPLAY, taskId)
assertThat(repo.removeBoundsBeforeFullImmersive(taskId)).isNull()
}
@Test
fun removeTask_removesActiveTask() {
+ repo.addDesk(THIRD_DISPLAY, THIRD_DISPLAY)
val taskId = 1
val listener = TestListener()
repo.addActiveTaskListener(listener)
@@ -829,6 +853,7 @@ class DesktopRepositoryTest : ShellTestCase() {
@Test
fun removeTask_updatesTaskVisibility() {
+ repo.addDesk(displayId = THIRD_DISPLAY, deskId = THIRD_DISPLAY)
val taskId = 1
repo.addTask(DEFAULT_DISPLAY, taskId, isVisible = true)
@@ -930,8 +955,8 @@ class DesktopRepositoryTest : ShellTestCase() {
@Test
fun updateTask_minimizedTaskBecomesVisible_unminimizesTask() {
- repo.minimizeTask(displayId = 10, taskId = 2)
- repo.updateTask(displayId = 10, taskId = 2, isVisible = true)
+ repo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = 2)
+ repo.updateTask(displayId = DEFAULT_DISPLAY, taskId = 2, isVisible = true)
val isMinimizedTask = repo.isMinimizedTask(taskId = 2)
@@ -1003,34 +1028,34 @@ class DesktopRepositoryTest : ShellTestCase() {
fun setTaskInFullImmersiveState_savedAsInImmersiveState() {
assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isFalse()
- repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true)
+ repo.setTaskInFullImmersiveState(DEFAULT_DISPLAY, taskId = 1, immersive = true)
assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isTrue()
}
@Test
fun removeTaskInFullImmersiveState_removedAsInImmersiveState() {
- repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true)
+ repo.setTaskInFullImmersiveState(DEFAULT_DISPLAY, taskId = 1, immersive = true)
assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isTrue()
- repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = false)
+ repo.setTaskInFullImmersiveState(DEFAULT_DISPLAY, taskId = 1, immersive = false)
assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isFalse()
}
@Test
fun removeTaskInFullImmersiveState_otherWasImmersive_otherRemainsImmersive() {
- repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true)
+ repo.setTaskInFullImmersiveState(DEFAULT_DISPLAY, taskId = 1, immersive = true)
- repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 2, immersive = false)
+ repo.setTaskInFullImmersiveState(DEFAULT_DISPLAY, taskId = 2, immersive = false)
assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isTrue()
}
@Test
fun setTaskInFullImmersiveState_sameDisplay_overridesExistingFullImmersiveTask() {
- repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true)
- repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 2, immersive = true)
+ repo.setTaskInFullImmersiveState(DEFAULT_DISPLAY, taskId = 1, immersive = true)
+ repo.setTaskInFullImmersiveState(DEFAULT_DISPLAY, taskId = 2, immersive = true)
assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isFalse()
assertThat(repo.isTaskInFullImmersiveState(taskId = 2)).isTrue()
@@ -1038,8 +1063,10 @@ class DesktopRepositoryTest : ShellTestCase() {
@Test
fun setTaskInFullImmersiveState_differentDisplay_bothAreImmersive() {
- repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true)
- repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID + 1, taskId = 2, immersive = true)
+ repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
+ repo.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
+ repo.setTaskInFullImmersiveState(DEFAULT_DISPLAY, taskId = 1, immersive = true)
+ repo.setTaskInFullImmersiveState(SECOND_DISPLAY, taskId = 2, immersive = true)
assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isTrue()
assertThat(repo.isTaskInFullImmersiveState(taskId = 2)).isTrue()
@@ -1061,11 +1088,13 @@ class DesktopRepositoryTest : ShellTestCase() {
@Test
fun getTaskInFullImmersiveState_byDisplay() {
+ repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
+ repo.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true)
- repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID + 1, taskId = 2, immersive = true)
+ repo.setTaskInFullImmersiveState(SECOND_DISPLAY, taskId = 2, immersive = true)
assertThat(repo.getTaskInFullImmersiveState(DEFAULT_DESKTOP_ID)).isEqualTo(1)
- assertThat(repo.getTaskInFullImmersiveState(DEFAULT_DESKTOP_ID + 1)).isEqualTo(2)
+ assertThat(repo.getTaskInFullImmersiveState(SECOND_DISPLAY)).isEqualTo(2)
}
@Test
@@ -1089,11 +1118,13 @@ class DesktopRepositoryTest : ShellTestCase() {
@Test
fun setTaskInPip_multipleDisplays_bothAreInPip() {
+ repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
+ repo.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true)
- repo.setTaskInPip(DEFAULT_DESKTOP_ID + 1, taskId = 2, enterPip = true)
+ repo.setTaskInPip(SECOND_DISPLAY, taskId = 2, enterPip = true)
assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isTrue()
- assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID + 1, taskId = 2)).isTrue()
+ assertThat(repo.isTaskMinimizedPipInDisplay(SECOND_DISPLAY, taskId = 2)).isTrue()
}
@Test
@@ -1129,6 +1160,14 @@ class DesktopRepositoryTest : ShellTestCase() {
assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isFalse()
}
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun addTask_deskDoesNotExists_createsDesk() {
+ repo.addTask(displayId = 999, taskId = 6, isVisible = true)
+
+ assertThat(repo.getActiveTaskIdsInDesk(999)).contains(6)
+ }
+
class TestListener : DesktopRepository.ActiveTasksListener {
var activeChangesOnDefaultDisplay = 0
var activeChangesOnSecondaryDisplay = 0
@@ -1169,5 +1208,10 @@ class DesktopRepositoryTest : ShellTestCase() {
const val THIRD_DISPLAY = 345
private const val DEFAULT_USER_ID = 1000
private const val DEFAULT_DESKTOP_ID = 0
+
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> =
+ FlagsParameterization.allCombinationsOf(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index fffaab36c9ad..40c0e3610da2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -48,8 +48,8 @@ import android.os.IBinder
import android.os.UserManager
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.FlagsParameterization
import android.platform.test.flag.junit.SetFlagsRule
-import android.testing.AndroidTestingRunner
import android.view.Display.DEFAULT_DISPLAY
import android.view.DragEvent
import android.view.Gravity
@@ -100,6 +100,7 @@ import com.android.wm.shell.common.DisplayLayout
import com.android.wm.shell.common.MultiInstanceHelper
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.common.SyncTransactionQueue
+import com.android.wm.shell.common.UserProfileContexts
import com.android.wm.shell.desktopmode.DesktopImmersiveController.ExitResult
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.InputMethod
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.MinimizeReason
@@ -117,6 +118,7 @@ import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler.FULLSCR
import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction
import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider
import com.android.wm.shell.desktopmode.minimize.DesktopWindowLimitRemoteHandler
+import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer
import com.android.wm.shell.desktopmode.persistence.Desktop
import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository
import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer
@@ -127,6 +129,7 @@ import com.android.wm.shell.recents.RecentsTransitionHandler
import com.android.wm.shell.recents.RecentsTransitionStateListener
import com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_ANIMATING
import com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED
+import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.UNKNOWN
import com.android.wm.shell.shared.split.SplitScreenConstants
@@ -184,6 +187,8 @@ import org.mockito.kotlin.capture
import org.mockito.kotlin.eq
import org.mockito.kotlin.whenever
import org.mockito.quality.Strictness
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
/**
* Test class for {@link DesktopTasksController}
@@ -191,12 +196,12 @@ import org.mockito.quality.Strictness
* Usage: atest WMShellUnitTests:DesktopTasksControllerTest
*/
@SmallTest
-@RunWith(AndroidTestingRunner::class)
+@RunWith(ParameterizedAndroidJunit4::class)
@ExperimentalCoroutinesApi
@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
-class DesktopTasksControllerTest : ShellTestCase() {
+class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() {
- @JvmField @Rule val setFlagsRule = SetFlagsRule()
+ @JvmField @Rule val setFlagsRule = SetFlagsRule(flags)
@Mock lateinit var testExecutor: ShellExecutor
@Mock lateinit var shellCommandHandler: ShellCommandHandler
@@ -247,6 +252,8 @@ class DesktopTasksControllerTest : ShellTestCase() {
DesktopWallpaperActivityTokenProvider
@Mock
private lateinit var overviewToDesktopTransitionObserver: OverviewToDesktopTransitionObserver
+ @Mock private lateinit var desksOrganizer: DesksOrganizer
+ @Mock private lateinit var userProfileContexts: UserProfileContexts
private lateinit var controller: DesktopTasksController
private lateinit var shellInit: ShellInit
@@ -255,6 +262,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
private lateinit var desktopTasksLimiter: DesktopTasksLimiter
private lateinit var recentsTransitionStateListener: RecentsTransitionStateListener
private lateinit var testScope: CoroutineScope
+ private lateinit var desktopModeCompatPolicy: DesktopModeCompatPolicy
private val shellExecutor = TestShellExecutor()
@@ -305,6 +313,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
mContext,
mockHandler,
)
+ desktopModeCompatPolicy = DesktopModeCompatPolicy(context)
whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks }
whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() }
@@ -341,6 +350,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
)
.thenReturn(ExitResult.NoExit)
whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(wallpaperToken)
+ whenever(userProfileContexts[anyInt()]).thenReturn(context)
controller = createController()
controller.setSplitScreenController(splitScreenController)
@@ -358,6 +368,8 @@ class DesktopTasksControllerTest : ShellTestCase() {
assumeTrue(ENABLE_SHELL_TRANSITIONS)
taskRepository = userRepositories.current
+ taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = DEFAULT_DISPLAY)
+ taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = DEFAULT_DISPLAY)
}
private fun createController() =
@@ -395,6 +407,9 @@ class DesktopTasksControllerTest : ShellTestCase() {
desktopWallpaperActivityTokenProvider,
Optional.of(bubbleController),
overviewToDesktopTransitionObserver,
+ desksOrganizer,
+ userProfileContexts,
+ desktopModeCompatPolicy,
)
@After
@@ -613,7 +628,12 @@ class DesktopTasksControllerTest : ShellTestCase() {
Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY,
)
+ @DisableFlags(
+ /** TODO: b/362720497 - re-enable when activation is implemented. */
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND
+ )
fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_perDisplayWallpaperEnabled_shouldShowWallpaper() {
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
val homeTask = setUpHomeTask(SECOND_DISPLAY)
val task1 = setUpFreeformTask(SECOND_DISPLAY)
val task2 = setUpFreeformTask(SECOND_DISPLAY)
@@ -634,8 +654,13 @@ class DesktopTasksControllerTest : ShellTestCase() {
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- @DisableFlags(Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY)
+ @DisableFlags(
+ Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY,
+ /** TODO: b/362720497 - re-enable when activation is implemented. */
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_shouldNotShowWallpaper() {
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
val homeTask = setUpHomeTask(SECOND_DISPLAY)
val task1 = setUpFreeformTask(SECOND_DISPLAY)
val task2 = setUpFreeformTask(SECOND_DISPLAY)
@@ -674,8 +699,13 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ @DisableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+ /** TODO: b/362720497 - re-enable when activation is implemented. */
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
fun showDesktopApps_onSecondaryDisplay_desktopWallpaperDisabled_shouldNotMoveLauncher() {
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
val homeTask = setUpHomeTask(SECOND_DISPLAY)
val task1 = setUpFreeformTask(SECOND_DISPLAY)
val task2 = setUpFreeformTask(SECOND_DISPLAY)
@@ -777,6 +807,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
@Test
@DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperDisabled() {
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY)
val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY)
setUpHomeTask(SECOND_DISPLAY)
@@ -797,6 +828,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperEnabled() {
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY)
val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY)
setUpHomeTask(SECOND_DISPLAY)
@@ -883,6 +915,8 @@ class DesktopTasksControllerTest : ShellTestCase() {
@Test
fun visibleTaskCount_twoTasksVisibleOnDifferentDisplays_returnsOne() {
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
+ taskRepository.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
setUpHomeTask()
setUpFreeformTask(DEFAULT_DISPLAY).also(::markTaskVisible)
setUpFreeformTask(SECOND_DISPLAY).also(::markTaskVisible)
@@ -1476,6 +1510,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
val fullscreenTaskDefault = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
markTaskHidden(freeformTaskDefault)
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
val homeTaskSecond = setUpHomeTask(displayId = SECOND_DISPLAY)
val freeformTaskSecond = setUpFreeformTask(displayId = SECOND_DISPLAY)
markTaskHidden(freeformTaskSecond)
@@ -1673,6 +1708,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
@Test
fun moveToFullscreen_secondDisplayTaskHasFreeform_secondDisplayNotAffected() {
val taskDefaultDisplay = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
val taskSecondDisplay = setUpFreeformTask(displayId = SECOND_DISPLAY)
controller.moveToFullscreen(taskDefaultDisplay.taskId, transitionSource = UNKNOWN)
@@ -1853,6 +1889,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
@Test
fun moveToNextDisplay_moveFromFirstToSecondDisplay() {
// Set up two display ids
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
whenever(rootTaskDisplayAreaOrganizer.displayIds)
.thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
// Create a mock for the target display area: second display
@@ -1882,6 +1919,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
.thenReturn(defaultDisplayArea)
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
val task = setUpFreeformTask(displayId = SECOND_DISPLAY)
controller.moveToNextDisplay(task.taskId)
@@ -1901,6 +1939,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
)
fun moveToNextDisplay_wallpaperOnSystemUser_reorderWallpaperToBack() {
// Set up two display ids
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
whenever(rootTaskDisplayAreaOrganizer.displayIds)
.thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
// Create a mock for the target display area: second display
@@ -1925,6 +1964,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
@DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER)
fun moveToNextDisplay_wallpaperNotOnSystemUser_removeWallpaper() {
// Set up two display ids
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
whenever(rootTaskDisplayAreaOrganizer.displayIds)
.thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
// Create a mock for the target display area: second display
@@ -2049,6 +2089,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
@Test
@EnableFlags(FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT)
fun moveToNextDisplay_defaultBoundsWhenDestinationTooSmall() {
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
// Set up two display ids
whenever(rootTaskDisplayAreaOrganizer.displayIds)
.thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
@@ -2090,6 +2131,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT,
)
fun moveToNextDisplay_destinationGainGlobalFocus() {
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
// Set up two display ids
whenever(rootTaskDisplayAreaOrganizer.displayIds)
.thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
@@ -2977,6 +3019,27 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
+ fun handleRequest_systemUIActivityWithDisplay_returnSwitchToFullscreenWCT_enforcedDesktop() {
+ whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
+ val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+ tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+ // Set task as systemUI package
+ val systemUIPackageName =
+ context.resources.getString(com.android.internal.R.string.config_systemUi)
+ val baseComponent = ComponentName(systemUIPackageName, /* cls= */ "")
+ val task =
+ createFreeformTask().apply {
+ baseActivity = baseComponent
+ isTopActivityNoDisplay = false
+ }
+
+ assertThat(controller.isDesktopModeShowing(DEFAULT_DISPLAY)).isFalse()
+ val result = controller.handleRequest(Binder(), createTransition(task))
+ assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_FULLSCREEN)
+ }
+
+ @Test
@DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
fun handleRequest_backTransition_singleTaskNoToken_noWallpaper_doesNotHandle() {
val task = setUpFreeformTask()
@@ -3158,6 +3221,8 @@ class DesktopTasksControllerTest : ShellTestCase() {
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
fun handleRequest_closeTransition_singleTaskNoToken_secondaryDisplay_launchesHome() {
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
+ taskRepository.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
val task = setUpFreeformTask(displayId = SECOND_DISPLAY)
whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
@@ -4933,7 +4998,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
@Test
@EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
fun shouldPlayDesktopAnimation_notShowingDesktop_doesNotPlay() {
- val triggerTask = setUpFullscreenTask(displayId = 5)
+ val triggerTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
taskRepository.setTaskInFullImmersiveState(
displayId = triggerTask.displayId,
taskId = triggerTask.taskId,
@@ -4951,7 +5016,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
@Test
@EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
fun shouldPlayDesktopAnimation_notOpening_doesNotPlay() {
- val triggerTask = setUpFreeformTask(displayId = 5)
+ val triggerTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
taskRepository.setTaskInFullImmersiveState(
displayId = triggerTask.displayId,
taskId = triggerTask.taskId,
@@ -4969,7 +5034,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
@Test
@EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
fun shouldPlayDesktopAnimation_notImmersive_doesNotPlay() {
- val triggerTask = setUpFreeformTask(displayId = 5)
+ val triggerTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
taskRepository.setTaskInFullImmersiveState(
displayId = triggerTask.displayId,
taskId = triggerTask.taskId,
@@ -4988,8 +5053,8 @@ class DesktopTasksControllerTest : ShellTestCase() {
@EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
fun shouldPlayDesktopAnimation_fullscreenEntersDesktop_plays() {
// At least one freeform task to be in a desktop.
- val existingTask = setUpFreeformTask(displayId = 5)
- val triggerTask = setUpFullscreenTask(displayId = 5)
+ val existingTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ val triggerTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isTrue()
taskRepository.setTaskInFullImmersiveState(
displayId = existingTask.displayId,
@@ -5008,7 +5073,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
@Test
@EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
fun shouldPlayDesktopAnimation_fullscreenStaysFullscreen_doesNotPlay() {
- val triggerTask = setUpFullscreenTask(displayId = 5)
+ val triggerTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isFalse()
assertThat(
@@ -5023,8 +5088,8 @@ class DesktopTasksControllerTest : ShellTestCase() {
@EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
fun shouldPlayDesktopAnimation_freeformStaysInDesktop_plays() {
// At least one freeform task to be in a desktop.
- val existingTask = setUpFreeformTask(displayId = 5)
- val triggerTask = setUpFreeformTask(displayId = 5, active = false)
+ val existingTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ val triggerTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, active = false)
assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isTrue()
taskRepository.setTaskInFullImmersiveState(
displayId = existingTask.displayId,
@@ -5043,7 +5108,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
@Test
@EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
fun shouldPlayDesktopAnimation_freeformExitsDesktop_doesNotPlay() {
- val triggerTask = setUpFreeformTask(displayId = 5, active = false)
+ val triggerTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, active = false)
assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isFalse()
assertThat(
@@ -5054,6 +5119,19 @@ class DesktopTasksControllerTest : ShellTestCase() {
.isFalse()
}
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun testCreateDesk() {
+ val currentDeskCount = taskRepository.getNumberOfDesks(DEFAULT_DISPLAY)
+ whenever(desksOrganizer.createDesk(eq(DEFAULT_DISPLAY), any())).thenAnswer { invocation ->
+ (invocation.arguments[1] as DesksOrganizer.OnCreateCallback).onCreated(deskId = 5)
+ }
+
+ controller.createDesk(DEFAULT_DISPLAY)
+
+ assertThat(taskRepository.getNumberOfDesks(DEFAULT_DISPLAY)).isEqualTo(currentDeskCount + 1)
+ }
+
private class RunOnStartTransitionCallback : ((IBinder) -> Unit) {
var invocations = 0
private set
@@ -5098,7 +5176,8 @@ class DesktopTasksControllerTest : ShellTestCase() {
whenever(mockDragEvent.dragSurface).thenReturn(dragSurface)
whenever(mockDragEvent.x).thenReturn(inputCoordinate.x)
whenever(mockDragEvent.y).thenReturn(inputCoordinate.y)
- whenever(multiInstanceHelper.supportsMultiInstanceSplit(anyOrNull())).thenReturn(true)
+ whenever(multiInstanceHelper.supportsMultiInstanceSplit(anyOrNull(), anyInt()))
+ .thenReturn(true)
whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
doReturn(indicatorType)
.whenever(spyController)
@@ -5112,6 +5191,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
spyController.onUnhandledDrag(
mockPendingIntent,
+ context.userId,
mockDragEvent,
mockCallback as Consumer<Boolean>,
)
@@ -5387,6 +5467,11 @@ class DesktopTasksControllerTest : ShellTestCase() {
val STABLE_BOUNDS = Rect(0, 0, 1000, 1000)
const val MAX_TASK_LIMIT = 6
private const val TASKBAR_FRAME_HEIGHT = 200
+
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> =
+ FlagsParameterization.allCombinationsOf(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
index e85901bbd9d4..554b09f130bd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
@@ -180,6 +180,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
@Test
fun addPendingMinimizeTransition_taskIsNotMinimized() {
+ desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
val task = setUpFreeformTask()
markTaskHidden(task)
@@ -190,6 +192,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
@Test
fun onTransitionReady_noPendingTransition_taskIsNotMinimized() {
+ desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
val task = setUpFreeformTask()
markTaskHidden(task)
@@ -203,6 +207,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
@Test
fun onTransitionReady_differentPendingTransition_taskIsNotMinimized() {
+ desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
val pendingTransition = Binder()
val taskTransition = Binder()
val task = setUpFreeformTask()
@@ -219,6 +225,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
@Test
fun onTransitionReady_pendingTransition_noTaskChange_taskVisible_taskIsNotMinimized() {
+ desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
val transition = Binder()
val task = setUpFreeformTask()
markTaskVisible(task)
@@ -232,6 +240,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
@Test
fun onTransitionReady_pendingTransition_noTaskChange_taskInvisible_taskIsMinimized() {
val transition = Binder()
+ desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
val task = setUpFreeformTask()
markTaskHidden(task)
addPendingMinimizeChange(transition, taskId = task.taskId)
@@ -243,6 +253,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
@Test
fun onTransitionReady_pendingTransition_changeTaskToBack_taskIsMinimized() {
+ desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
val transition = Binder()
val task = setUpFreeformTask()
addPendingMinimizeChange(transition, taskId = task.taskId)
@@ -257,6 +269,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
@Test
fun onTransitionReady_pendingTransition_changeTaskToBack_boundsSaved() {
+ desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
val bounds = Rect(0, 0, 200, 200)
val transition = Binder()
val task = setUpFreeformTask()
@@ -280,6 +294,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
@Test
fun onTransitionReady_transitionMergedFromPending_taskIsMinimized() {
+ desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
val mergedTransition = Binder()
val newTransition = Binder()
val task = setUpFreeformTask()
@@ -302,6 +318,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
@Test
@DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
fun removeLeftoverMinimizedTasks_activeNonMinimizedTasksStillAround_doesNothing() {
+ desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
desktopTaskRepo.addTask(displayId = DEFAULT_DISPLAY, taskId = 1, isVisible = true)
desktopTaskRepo.addTask(displayId = DEFAULT_DISPLAY, taskId = 2, isVisible = true)
desktopTaskRepo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = 2)
@@ -318,6 +336,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
@Test
@DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
fun removeLeftoverMinimizedTasks_noMinimizedTasks_doesNothing() {
+ desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
val wct = WindowContainerTransaction()
desktopTasksLimiter.leftoverMinimizedTasksRemover.removeLeftoverMinimizedTasks(
DEFAULT_DISPLAY,
@@ -330,6 +350,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
@Test
@DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
fun removeLeftoverMinimizedTasks_onlyMinimizedTasksLeft_removesAllMinimizedTasks() {
+ desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
desktopTaskRepo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task1.taskId)
@@ -351,6 +373,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
@Test
@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
fun removeLeftoverMinimizedTasks_onlyMinimizedTasksLeft_backNavEnabled_doesNothing() {
+ desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
desktopTaskRepo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task1.taskId)
@@ -364,6 +388,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
@Test
fun addAndGetMinimizeTaskChanges_tasksWithinLimit_noTaskMinimized() {
+ desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
(1..<MAX_TASK_LIMIT).forEach { _ -> setUpFreeformTask() }
val wct = WindowContainerTransaction()
@@ -380,6 +406,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
@Test
fun addAndGetMinimizeTaskChanges_tasksAboveLimit_backTaskMinimized() {
+ desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
// The following list will be ordered bottom -> top, as the last task is moved to top last.
val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() }
@@ -399,6 +427,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
@Test
fun addAndGetMinimizeTaskChanges_nonMinimizedTasksWithinLimit_noTaskMinimized() {
+ desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() }
desktopTaskRepo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = tasks[0].taskId)
@@ -416,6 +446,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
@Test
fun getTaskToMinimize_tasksWithinLimit_returnsNull() {
+ desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() }
val minimizedTask =
@@ -426,6 +458,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
@Test
fun getTaskToMinimize_tasksAboveLimit_returnsBackTask() {
+ desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
val tasks = (1..MAX_TASK_LIMIT + 1).map { setUpFreeformTask() }
val minimizedTask =
@@ -437,6 +471,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
@Test
fun getTaskToMinimize_tasksAboveLimit_otherLimit_returnsBackTask() {
+ desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
desktopTasksLimiter =
DesktopTasksLimiter(
transitions,
@@ -458,6 +494,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
@Test
fun getTaskToMinimize_withNewTask_tasksAboveLimit_returnsBackTask() {
+ desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() }
val minimizedTask =
@@ -472,6 +510,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
@Test
fun getTaskToMinimize_tasksAtLimit_newIntentReturnsBackTask() {
+ desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() }
val minimizedTask =
desktopTasksLimiter.getTaskIdToMinimize(
@@ -486,6 +526,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
@Test
fun minimizeTransitionReadyAndFinished_logsJankInstrumentationBeginAndEnd() {
+ desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
(1..<MAX_TASK_LIMIT).forEach { _ -> setUpFreeformTask() }
val transition = Binder()
val task = setUpFreeformTask()
@@ -510,6 +552,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
@Test
fun minimizeTransitionReadyAndAborted_logsJankInstrumentationBeginAndCancel() {
+ desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
(1..<MAX_TASK_LIMIT).forEach { _ -> setUpFreeformTask() }
val transition = Binder()
val task = setUpFreeformTask()
@@ -534,6 +578,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
@Test
fun minimizeTransitionReadyAndMerged_logsJankInstrumentationBeginAndEnd() {
+ desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
(1..<MAX_TASK_LIMIT).forEach { _ -> setUpFreeformTask() }
val mergedTransition = Binder()
val newTransition = Binder()
@@ -566,6 +612,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
@Test
fun getMinimizingTask_pendingTaskTransition_returnsTask() {
+ desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
val transition = Binder()
val task = setUpFreeformTask()
addPendingMinimizeChange(
@@ -582,6 +630,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
@Test
fun getMinimizingTask_activeTaskTransition_returnsTask() {
+ desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
val transition = Binder()
val task = setUpFreeformTask()
addPendingMinimizeChange(
@@ -613,6 +663,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
@Test
fun getUnminimizingTask_pendingTaskTransition_returnsTask() {
+ desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
val transition = Binder()
val task = setUpFreeformTask()
addPendingUnminimizeChange(
@@ -632,6 +684,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
@Test
fun getUnminimizingTask_activeTaskTransition_returnsTask() {
+ desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+ desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
val transition = Binder()
val task = setUpFreeformTask()
addPendingMinimizeChange(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt
index aee8821a63f6..8b6cafb10df4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt
@@ -35,6 +35,7 @@ object DesktopTestHelpers {
): RunningTaskInfo =
TestRunningTaskInfoBuilder()
.setDisplayId(displayId)
+ .setParentTaskId(displayId)
.setToken(MockToken().token())
.setActivityType(ACTIVITY_TYPE_STANDARD)
.setWindowingMode(WINDOWING_MODE_FREEFORM)
@@ -73,10 +74,14 @@ object DesktopTestHelpers {
.setLastActiveTime(100)
.build()
+ /**
+ * Create a new System Modal task builder, i.e. a builder for a task with only transparent
+ * activities.
+ */
+ fun createSystemModalTaskBuilder(displayId: Int = DEFAULT_DISPLAY): TestRunningTaskInfoBuilder =
+ createFullscreenTaskBuilder(displayId).setActivityStackTransparent(true).setNumActivities(1)
+
/** Create a new System Modal task, i.e. a task with only transparent activities. */
fun createSystemModalTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo =
- createFullscreenTaskBuilder(displayId)
- .setActivityStackTransparent(true)
- .setNumActivities(1)
- .build()
+ createSystemModalTaskBuilder(displayId).build()
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/compatui/SystemModalsTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/compatui/SystemModalsTransitionHandlerTest.kt
index 1569f9dc9b10..dfb1b0c8c642 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/compatui/SystemModalsTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/compatui/SystemModalsTransitionHandlerTest.kt
@@ -16,6 +16,7 @@
package com.android.wm.shell.desktopmode.compatui
+import android.content.Intent
import android.os.Binder
import android.testing.AndroidTestingRunner
import android.view.SurfaceControl
@@ -29,7 +30,10 @@ import com.android.wm.shell.desktopmode.DesktopRepository
import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFullscreenTask
import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFullscreenTaskBuilder
import com.android.wm.shell.desktopmode.DesktopTestHelpers.createSystemModalTask
+import com.android.wm.shell.desktopmode.DesktopTestHelpers.createSystemModalTaskBuilder
import com.android.wm.shell.desktopmode.DesktopUserRepositories
+import com.android.wm.shell.desktopmode.DesktopWallpaperActivity
+import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.transition.TransitionInfoBuilder
import com.android.wm.shell.transition.Transitions
@@ -60,12 +64,14 @@ class SystemModalsTransitionHandlerTest : ShellTestCase() {
private val finishT = mock<SurfaceControl.Transaction>()
private lateinit var transitionHandler: SystemModalsTransitionHandler
+ private lateinit var desktopModeCompatPolicy: DesktopModeCompatPolicy
@Before
fun setUp() {
// Simulate having one Desktop task so that we see Desktop Mode as active
whenever(desktopUserRepositories.current).thenReturn(desktopRepository)
whenever(desktopRepository.getVisibleTaskCount(anyInt())).thenReturn(1)
+ desktopModeCompatPolicy = DesktopModeCompatPolicy(context)
transitionHandler = createTransitionHandler()
}
@@ -77,6 +83,7 @@ class SystemModalsTransitionHandlerTest : ShellTestCase() {
shellInit,
transitions,
desktopUserRepositories,
+ desktopModeCompatPolicy,
)
@Test
@@ -116,6 +123,19 @@ class SystemModalsTransitionHandlerTest : ShellTestCase() {
}
@Test
+ fun startAnimation_launchingWallpaperTask_doesNotAnimate() {
+ val wallpaperTask =
+ createSystemModalTaskBuilder().setBaseIntent(createWallpaperIntent()).build()
+ val info =
+ TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_OPEN, wallpaperTask).build()
+
+ assertThat(transitionHandler.startAnimation(Binder(), info, startT, finishT) {}).isFalse()
+ }
+
+ private fun createWallpaperIntent() =
+ Intent().apply { setComponent(DesktopWallpaperActivity.wallpaperActivityComponent) }
+
+ @Test
fun startAnimation_launchingFullscreenTask_doesNotAnimate() {
val info =
TransitionInfoBuilder(TRANSIT_OPEN)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt
index 5475032f35a9..493a8c83c48e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt
@@ -29,7 +29,6 @@ import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.desktopmode.CaptionState
import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository
import com.android.wm.shell.desktopmode.education.AppHandleEducationController.Companion.APP_HANDLE_EDUCATION_DELAY_MILLIS
-import com.android.wm.shell.desktopmode.education.AppHandleEducationController.Companion.APP_HANDLE_EDUCATION_TIMEOUT_MILLIS
import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource
@@ -47,7 +46,6 @@ import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.Before
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -113,10 +111,10 @@ class AppHandleEducationControllerTest : ShellTestCase() {
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
- fun init_appHandleVisible_shouldCallShowEducationTooltip() =
+ fun init_appHandleVisible_shouldCallShowEducationTooltipAndMarkAsViewed() =
testScope.runTest {
// App handle is visible. Should show education tooltip.
- setShouldShowAppHandleEducation(true)
+ setShouldShowDesktopModeEducation(true)
// Simulate app handle visible.
testCaptionStateFlow.value = createAppHandleState()
@@ -124,6 +122,38 @@ class AppHandleEducationControllerTest : ShellTestCase() {
waitForBufferDelay()
verify(mockTooltipController, times(1)).showEducationTooltip(any(), any())
+ verify(mockDataStoreRepository, times(1))
+ .updateAppHandleHintViewedTimestampMillis(eq(true))
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+ fun init_appHandleVisibleAndMenuExpanded_shouldCallShowEducationTooltipAndMarkAsViewed() =
+ testScope.runTest {
+ setShouldShowDesktopModeEducation(true)
+
+ // Simulate app handle visible and handle menu is expanded.
+ testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
+ waitForBufferDelay()
+
+ verify(mockTooltipController, times(1)).showEducationTooltip(any(), any())
+ verify(mockDataStoreRepository, times(1))
+ .updateEnterDesktopModeHintViewedTimestampMillis(eq(true))
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+ fun init_appHeaderVisible_shouldCallShowEducationTooltipAndMarkAsViewed() =
+ testScope.runTest {
+ setShouldShowDesktopModeEducation(true)
+
+ // Simulate app header visible.
+ testCaptionStateFlow.value = createAppHeaderState()
+ waitForBufferDelay()
+
+ verify(mockTooltipController, times(1)).showEducationTooltip(any(), any())
+ verify(mockDataStoreRepository, times(1))
+ .updateExitDesktopModeHintViewedTimestampMillis(eq(true))
}
@Test
@@ -133,7 +163,7 @@ class AppHandleEducationControllerTest : ShellTestCase() {
// App handle visible but education aconfig flag disabled, should not show education
// tooltip.
whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(false)
- setShouldShowAppHandleEducation(true)
+ setShouldShowDesktopModeEducation(true)
// Simulate app handle visible.
testCaptionStateFlow.value = createAppHandleState()
@@ -145,12 +175,11 @@ class AppHandleEducationControllerTest : ShellTestCase() {
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
- fun init_shouldShowAppHandleEducationReturnsFalse_shouldNotCallShowEducationTooltip() =
+ fun init_shouldShowDesktopModeEducationReturnsFalse_shouldNotCallShowEducationTooltip() =
testScope.runTest {
- // App handle is visible but [shouldShowAppHandleEducation] api returns false, should
- // not
- // show education tooltip.
- setShouldShowAppHandleEducation(false)
+ // App handle is visible but [shouldShowDesktopModeEducation] api returns false, should
+ // not show education tooltip.
+ setShouldShowDesktopModeEducation(false)
// Simulate app handle visible.
testCaptionStateFlow.value = createAppHandleState()
@@ -165,7 +194,7 @@ class AppHandleEducationControllerTest : ShellTestCase() {
fun init_appHandleNotVisible_shouldNotCallShowEducationTooltip() =
testScope.runTest {
// App handle is not visible, should not show education tooltip.
- setShouldShowAppHandleEducation(true)
+ setShouldShowDesktopModeEducation(true)
// Simulate app handle is not visible.
testCaptionStateFlow.value = CaptionState.NoCaption
@@ -184,7 +213,7 @@ class AppHandleEducationControllerTest : ShellTestCase() {
// Mark app handle hint viewed.
testDataStoreFlow.value =
createWindowingEducationProto(appHandleHintViewedTimestampMillis = 123L)
- setShouldShowAppHandleEducation(true)
+ setShouldShowDesktopModeEducation(true)
// Simulate app handle visible.
testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = false)
@@ -196,231 +225,95 @@ class AppHandleEducationControllerTest : ShellTestCase() {
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
- fun overridePrerequisite_appHandleHintViewedAlready_shouldCallShowEducationTooltip() =
+ fun init_enterDesktopModeHintViewedAlready_shouldNotCallShowEducationTooltip() =
testScope.runTest {
- // App handle is visible but app handle hint has been viewed before.
- // But as we are overriding prerequisite conditions, we should show app
- // handle tooltip.
+ // App handle is visible but app handle hint has been viewed before,
+ // should not show education tooltip.
// Mark app handle hint viewed.
testDataStoreFlow.value =
- createWindowingEducationProto(appHandleHintViewedTimestampMillis = 123L)
- val systemPropertiesKey =
- "persist.desktop_windowing_app_handle_education_override_conditions"
- whenever(SystemProperties.getBoolean(eq(systemPropertiesKey), anyBoolean()))
- .thenReturn(true)
- setShouldShowAppHandleEducation(true)
+ createWindowingEducationProto(enterDesktopModeHintViewedTimestampMillis = 123L)
+ setShouldShowDesktopModeEducation(true)
// Simulate app handle visible.
- testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = false)
- // Wait for first tooltip to showup.
- waitForBufferDelay()
-
- verify(mockTooltipController, times(1)).showEducationTooltip(any(), any())
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
- fun init_appHandleExpanded_shouldMarkAppHandleHintUsed() =
- testScope.runTest {
- setShouldShowAppHandleEducation(false)
-
- // Simulate app handle visible and expanded.
testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
- // Wait for some time before verifying
+ // Wait for first tooltip to showup.
waitForBufferDelay()
- verify(mockDataStoreRepository, times(1))
- .updateAppHandleHintUsedTimestampMillis(eq(true))
+ verify(mockTooltipController, never()).showEducationTooltip(any(), any())
}
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
- fun init_showFirstTooltip_shouldMarkAppHandleHintViewed() =
+ fun init_exitDesktopModeHintViewedAlready_shouldNotCallShowEducationTooltip() =
testScope.runTest {
- // App handle is visible. Should show education tooltip.
- setShouldShowAppHandleEducation(true)
+ // App handle is visible but app handle hint has been viewed before,
+ // should not show education tooltip.
+ // Mark app handle hint viewed.
+ testDataStoreFlow.value =
+ createWindowingEducationProto(exitDesktopModeHintViewedTimestampMillis = 123L)
+ setShouldShowDesktopModeEducation(true)
// Simulate app handle visible.
- testCaptionStateFlow.value = createAppHandleState()
+ testCaptionStateFlow.value = createAppHeaderState()
// Wait for first tooltip to showup.
waitForBufferDelay()
- verify(mockDataStoreRepository, times(1))
- .updateAppHandleHintViewedTimestampMillis(eq(true))
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
- @Ignore("b/371527084: revisit testcase after refactoring original logic")
- fun showWindowingImageButtonTooltip_appHandleExpanded_shouldCallShowEducationTooltipTwice() =
- testScope.runTest {
- // After first tooltip is dismissed, app handle is expanded. Should show second
- // education
- // tooltip.
- showAndDismissFirstTooltip()
-
- // Simulate app handle expanded.
- testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
- // Wait for next tooltip to showup.
- waitForBufferDelay()
-
- // [showEducationTooltip] should be called twice, once for each tooltip.
- verify(mockTooltipController, times(2)).showEducationTooltip(any(), any())
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
- @Ignore("b/371527084: revisit testcase after refactoring original logic")
- fun showWindowingImageButtonTooltip_appHandleExpandedAfterTimeout_shouldCallShowEducationTooltipOnce() =
- testScope.runTest {
- // After first tooltip is dismissed, app handle is expanded after timeout. Should not
- // show
- // second education tooltip.
- showAndDismissFirstTooltip()
-
- // Wait for timeout to occur, after this timeout we should not listen for further
- // triggers
- // anymore.
- advanceTimeBy(APP_HANDLE_EDUCATION_TIMEOUT_BUFFER_MILLIS)
- runCurrent()
- // Simulate app handle expanded.
- testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
- // Wait for next tooltip to showup.
- waitForBufferDelay()
-
- // [showEducationTooltip] should be called once, just for the first tooltip.
- verify(mockTooltipController, times(1)).showEducationTooltip(any(), any())
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
- @Ignore("b/371527084: revisit testcase after refactoring original logic")
- fun showWindowingImageButtonTooltip_appHandleExpandedTwice_shouldCallShowEducationTooltipTwice() =
- testScope.runTest {
- // After first tooltip is dismissed, app handle is expanded twice. Should show second
- // education tooltip only once.
- showAndDismissFirstTooltip()
-
- // Simulate app handle expanded.
- testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
- // Wait for next tooltip to showup.
- waitForBufferDelay()
- // Simulate app handle being expanded twice.
- testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
- waitForBufferDelay()
-
- // [showEducationTooltip] should not be called thrice, even if app handle was expanded
- // twice. Should be called twice, once for each tooltip.
- verify(mockTooltipController, times(2)).showEducationTooltip(any(), any())
+ verify(mockTooltipController, never()).showEducationTooltip(any(), any())
}
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
- @Ignore("b/371527084: revisit testcase after refactoring original logic")
- fun showWindowingImageButtonTooltip_appHandleNotExpanded_shouldCallShowEducationTooltipOnce() =
+ fun overridePrerequisite_appHandleHintViewedAlready_shouldCallShowEducationTooltip() =
testScope.runTest {
- // After first tooltip is dismissed, app handle is not expanded. Should not show second
- // education tooltip.
- showAndDismissFirstTooltip()
+ // App handle is visible but app handle hint has been viewed before.
+ // But as we are overriding prerequisite conditions, we should show app
+ // handle tooltip.
+ // Mark app handle hint viewed.
+ testDataStoreFlow.value =
+ createWindowingEducationProto(appHandleHintViewedTimestampMillis = 123L)
+ val systemPropertiesKey = "persist.windowing_force_show_desktop_mode_education"
+ whenever(SystemProperties.getBoolean(eq(systemPropertiesKey), anyBoolean()))
+ .thenReturn(true)
+ setShouldShowDesktopModeEducation(true)
- // Simulate app handle visible but not expanded.
+ // Simulate app handle visible.
testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = false)
- // Wait for next tooltip to showup.
+ // Wait for first tooltip to showup.
waitForBufferDelay()
- // [showEducationTooltip] should be called once, just for the first tooltip.
verify(mockTooltipController, times(1)).showEducationTooltip(any(), any())
}
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
- @Ignore("b/371527084: revisit testcase after refactoring original logic")
- fun showExitWindowingButtonTooltip_appHeaderVisible_shouldCallShowEducationTooltipThrice() =
- testScope.runTest {
- // After first two tooltips are dismissed, app header is visible. Should show third
- // education tooltip.
- showAndDismissFirstTooltip()
- showAndDismissSecondTooltip()
-
- // Simulate app header visible.
- testCaptionStateFlow.value = createAppHeaderState()
- // Wait for next tooltip to showup.
- waitForBufferDelay()
-
- verify(mockTooltipController, times(3)).showEducationTooltip(any(), any())
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
- @Ignore("b/371527084: revisit testcase after refactoring original logic")
- fun showExitWindowingButtonTooltip_appHeaderVisibleAfterTimeout_shouldCallShowEducationTooltipTwice() =
- testScope.runTest {
- // After first two tooltips are dismissed, app header is visible after timeout. Should
- // not
- // show third education tooltip.
- showAndDismissFirstTooltip()
- showAndDismissSecondTooltip()
-
- // Wait for timeout to occur, after this timeout we should not listen for further
- // triggers
- // anymore.
- advanceTimeBy(APP_HANDLE_EDUCATION_TIMEOUT_BUFFER_MILLIS)
- runCurrent()
- // Simulate app header visible.
- testCaptionStateFlow.value = createAppHeaderState()
- // Wait for next tooltip to showup.
- waitForBufferDelay()
-
- verify(mockTooltipController, times(2)).showEducationTooltip(any(), any())
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
- @Ignore("b/371527084: revisit testcase after refactoring original logic")
- fun showExitWindowingButtonTooltip_appHeaderVisibleTwice_shouldCallShowEducationTooltipThrice() =
+ fun clickAppHandleHint_openHandleMenuCallbackInvoked() =
testScope.runTest {
- // After first two tooltips are dismissed, app header is visible twice. Should show
- // third
- // education tooltip only once.
- showAndDismissFirstTooltip()
- showAndDismissSecondTooltip()
-
- // Simulate app header visible.
- testCaptionStateFlow.value = createAppHeaderState()
- // Wait for next tooltip to showup.
- waitForBufferDelay()
- testCaptionStateFlow.value = createAppHeaderState()
- // Wait for next tooltip to showup.
+ // App handle is visible. Should show education tooltip.
+ setShouldShowDesktopModeEducation(true)
+ val mockOpenHandleMenuCallback: (Int) -> Unit = mock()
+ val mockToDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit = mock()
+ educationController.setAppHandleEducationTooltipCallbacks(
+ mockOpenHandleMenuCallback,
+ mockToDesktopModeCallback,
+ )
+ // Simulate app handle visible.
+ testCaptionStateFlow.value = createAppHandleState()
+ // Wait for first tooltip to showup.
waitForBufferDelay()
- verify(mockTooltipController, times(3)).showEducationTooltip(any(), any())
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
- @Ignore("b/371527084: revisit testcase after refactoring original logic")
- fun showExitWindowingButtonTooltip_appHeaderExpanded_shouldCallShowEducationTooltipTwice() =
- testScope.runTest {
- // After first two tooltips are dismissed, app header is visible but expanded. Should
- // not
- // show third education tooltip.
- showAndDismissFirstTooltip()
- showAndDismissSecondTooltip()
-
- // Simulate app header visible.
- testCaptionStateFlow.value = createAppHeaderState(isHeaderMenuExpanded = true)
- // Wait for next tooltip to showup.
- waitForBufferDelay()
+ verify(mockTooltipController, atLeastOnce())
+ .showEducationTooltip(educationConfigCaptor.capture(), any())
+ educationConfigCaptor.lastValue.onEducationClickAction.invoke()
- verify(mockTooltipController, times(2)).showEducationTooltip(any(), any())
+ verify(mockOpenHandleMenuCallback, times(1)).invoke(any())
}
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
- fun setAppHandleEducationTooltipCallbacks_onAppHandleTooltipClicked_callbackInvoked() =
+ fun clickEnterDesktopModeHint_toDesktopModeCallbackInvoked() =
testScope.runTest {
// App handle is visible. Should show education tooltip.
- setShouldShowAppHandleEducation(true)
+ setShouldShowDesktopModeEducation(true)
val mockOpenHandleMenuCallback: (Int) -> Unit = mock()
val mockToDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit = mock()
educationController.setAppHandleEducationTooltipCallbacks(
@@ -428,7 +321,7 @@ class AppHandleEducationControllerTest : ShellTestCase() {
mockToDesktopModeCallback,
)
// Simulate app handle visible.
- testCaptionStateFlow.value = createAppHandleState()
+ testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
// Wait for first tooltip to showup.
waitForBufferDelay()
@@ -436,68 +329,41 @@ class AppHandleEducationControllerTest : ShellTestCase() {
.showEducationTooltip(educationConfigCaptor.capture(), any())
educationConfigCaptor.lastValue.onEducationClickAction.invoke()
- verify(mockOpenHandleMenuCallback, times(1)).invoke(any())
+ verify(mockToDesktopModeCallback, times(1))
+ .invoke(any(), eq(DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON))
}
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
- @Ignore("b/371527084: revisit testcase after refactoring original logic")
- fun setAppHandleEducationTooltipCallbacks_onWindowingImageButtonTooltipClicked_callbackInvoked() =
+ fun clickExitDesktopModeHint_openHandleMenuCallbackInvoked() =
testScope.runTest {
- // After first tooltip is dismissed, app handle is expanded. Should show second
- // education
- // tooltip.
- showAndDismissFirstTooltip()
+ // App handle is visible. Should show education tooltip.
+ setShouldShowDesktopModeEducation(true)
val mockOpenHandleMenuCallback: (Int) -> Unit = mock()
val mockToDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit = mock()
educationController.setAppHandleEducationTooltipCallbacks(
mockOpenHandleMenuCallback,
mockToDesktopModeCallback,
)
- // Simulate app handle expanded.
- testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
- // Wait for next tooltip to showup.
+ // Simulate app handle visible.
+ testCaptionStateFlow.value = createAppHeaderState()
+ // Wait for first tooltip to showup.
waitForBufferDelay()
verify(mockTooltipController, atLeastOnce())
.showEducationTooltip(educationConfigCaptor.capture(), any())
educationConfigCaptor.lastValue.onEducationClickAction.invoke()
- verify(mockToDesktopModeCallback, times(1)).invoke(any(), any())
+ verify(mockOpenHandleMenuCallback, times(1)).invoke(any())
}
- private suspend fun TestScope.showAndDismissFirstTooltip() {
- setShouldShowAppHandleEducation(true)
- // Simulate app handle visible.
- testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = false)
- // Wait for first tooltip to showup.
- waitForBufferDelay()
- // [shouldShowAppHandleEducation] should return false as education has been viewed
- // before.
- setShouldShowAppHandleEducation(false)
- // Dismiss previous tooltip, after this we should listen for next tooltip's trigger.
- captureAndInvokeOnDismissAction()
+ private suspend fun setShouldShowDesktopModeEducation(shouldShowDesktopModeEducation: Boolean) {
+ whenever(mockEducationFilter.shouldShowDesktopModeEducation(any<CaptionState.AppHandle>()))
+ .thenReturn(shouldShowDesktopModeEducation)
+ whenever(mockEducationFilter.shouldShowDesktopModeEducation(any<CaptionState.AppHeader>()))
+ .thenReturn(shouldShowDesktopModeEducation)
}
- private fun TestScope.showAndDismissSecondTooltip() {
- // Simulate app handle expanded.
- testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
- // Wait for next tooltip to showup.
- waitForBufferDelay()
- // Dismiss previous tooltip, after this we should listen for next tooltip's trigger.
- captureAndInvokeOnDismissAction()
- }
-
- private fun captureAndInvokeOnDismissAction() {
- verify(mockTooltipController, atLeastOnce())
- .showEducationTooltip(educationConfigCaptor.capture(), any())
- educationConfigCaptor.lastValue.onDismissAction.invoke()
- }
-
- private suspend fun setShouldShowAppHandleEducation(shouldShowAppHandleEducation: Boolean) =
- whenever(mockEducationFilter.shouldShowAppHandleEducation(any()))
- .thenReturn(shouldShowAppHandleEducation)
-
/**
* Class under test waits for some time before showing education, simulate advance time before
* verifying or moving forward
@@ -510,7 +376,5 @@ class AppHandleEducationControllerTest : ShellTestCase() {
private companion object {
val APP_HANDLE_EDUCATION_DELAY_BUFFER_MILLIS: Long =
APP_HANDLE_EDUCATION_DELAY_MILLIS + 1000L
- val APP_HANDLE_EDUCATION_TIMEOUT_BUFFER_MILLIS: Long =
- APP_HANDLE_EDUCATION_TIMEOUT_MILLIS + 1000L
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt
index 4db883d13551..31dfc78902b2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt
@@ -123,6 +123,24 @@ class AppHandleEducationDatastoreRepositoryTest {
}
@Test
+ fun updateEnterDesktopModeHintViewedTimestampMillis_updatesDatastoreProto() =
+ runTest(StandardTestDispatcher()) {
+ datastoreRepository.updateEnterDesktopModeHintViewedTimestampMillis(true)
+
+ val result = testDatastore.data.first().hasEnterDesktopModeHintViewedTimestampMillis()
+ assertThat(result).isEqualTo(true)
+ }
+
+ @Test
+ fun updateExitDesktopModeHintViewedTimestampMillis_updatesDatastoreProto() =
+ runTest(StandardTestDispatcher()) {
+ datastoreRepository.updateExitDesktopModeHintViewedTimestampMillis(true)
+
+ val result = testDatastore.data.first().hasExitDesktopModeHintViewedTimestampMillis()
+ assertThat(result).isEqualTo(true)
+ }
+
+ @Test
fun updateAppHandleHintUsedTimestampMillis_updatesDatastoreProto() =
runTest(StandardTestDispatcher()) {
datastoreRepository.updateAppHandleHintUsedTimestampMillis(true)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilterTest.kt
index 2fc36efb1a41..218226240c0f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilterTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilterTest.kt
@@ -89,9 +89,9 @@ class AppHandleEducationFilterTest : ShellTestCase() {
}
@Test
- fun shouldShowAppHandleEducation_isTriggerValid_returnsTrue() = runTest {
- // setup() makes sure that all of the conditions satisfy and #shouldShowAppHandleEducation
- // should return true
+ fun shouldShowDesktopModeEducation_isTriggerValid_returnsTrue() = runTest {
+ // setup() makes sure that all of the conditions satisfy and
+ // [shouldShowDesktopModeEducation] should return true
val windowingEducationProto =
createWindowingEducationProto(
appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4),
@@ -99,16 +99,15 @@ class AppHandleEducationFilterTest : ShellTestCase() {
)
`when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
- val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState())
+ val result = educationFilter.shouldShowDesktopModeEducation(createAppHandleState())
assertThat(result).isTrue()
}
@Test
- fun shouldShowAppHandleEducation_focusAppNotInAllowlist_returnsFalse() = runTest {
+ fun shouldShowDesktopModeEducation_focusAppNotInAllowlist_returnsFalse() = runTest {
// Pass Youtube as current focus app, it is not in allowlist hence
- // #shouldShowAppHandleEducation
- // should return false
+ // [shouldShowDesktopModeEducation] should return false
testableResources.addOverride(
R.array.desktop_windowing_app_handle_education_allowlist_apps,
arrayOf(GMAIL_PACKAGE_NAME),
@@ -122,16 +121,15 @@ class AppHandleEducationFilterTest : ShellTestCase() {
createAppHandleState(createTaskInfo(runningTaskPackageName = YOUTUBE_PACKAGE_NAME))
`when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
- val result = educationFilter.shouldShowAppHandleEducation(captionState)
+ val result = educationFilter.shouldShowDesktopModeEducation(captionState)
assertThat(result).isFalse()
}
@Test
- fun shouldShowAppHandleEducation_timeSinceSetupIsNotSufficient_returnsFalse() = runTest {
- // Time required to have passed setup is > 100 years, hence #shouldShowAppHandleEducation
- // should
- // return false
+ fun shouldShowDesktopModeEducation_timeSinceSetupIsNotSufficient_returnsFalse() = runTest {
+ // Time required to have passed setup is > 100 years, hence [shouldShowDesktopModeEducation]
+ // should return false
testableResources.addOverride(
R.integer.desktop_windowing_education_required_time_since_setup_seconds,
MAX_VALUE,
@@ -143,50 +141,15 @@ class AppHandleEducationFilterTest : ShellTestCase() {
)
`when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
- val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState())
-
- assertThat(result).isFalse()
- }
-
- @Test
- fun shouldShowAppHandleEducation_appHandleHintViewedBefore_returnsFalse() = runTest {
- // App handle hint has been viewed before, hence #shouldShowAppHandleEducation should return
- // false
- val windowingEducationProto =
- createWindowingEducationProto(
- appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4),
- appHandleHintViewedTimestampMillis = 123L,
- appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE,
- )
- `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
-
- val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState())
-
- assertThat(result).isFalse()
- }
-
- @Test
- fun shouldShowAppHandleEducation_appHandleHintUsedBefore_returnsFalse() = runTest {
- // App handle hint has been used before, hence #shouldShowAppHandleEducation should return
- // false
- val windowingEducationProto =
- createWindowingEducationProto(
- appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4),
- appHandleHintUsedTimestampMillis = 123L,
- appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE,
- )
- `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
-
- val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState())
+ val result = educationFilter.shouldShowDesktopModeEducation(createAppHandleState())
assertThat(result).isFalse()
}
@Test
- fun shouldShowAppHandleEducation_doesNotHaveMinAppUsage_returnsFalse() = runTest {
+ fun shouldShowDesktopModeEducation_doesNotHaveMinAppUsage_returnsFalse() = runTest {
// Simulate that gmail app has been launched twice before, minimum app launch count is 3,
- // hence
- // #shouldShowAppHandleEducation should return false
+ // hence [shouldShowDesktopModeEducation] should return false
testableResources.addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3)
val windowingEducationProto =
createWindowingEducationProto(
@@ -195,13 +158,13 @@ class AppHandleEducationFilterTest : ShellTestCase() {
)
`when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
- val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState())
+ val result = educationFilter.shouldShowDesktopModeEducation(createAppHandleState())
assertThat(result).isFalse()
}
@Test
- fun shouldShowAppHandleEducation_appUsageStatsStale_queryAppUsageStats() = runTest {
+ fun shouldShowDesktopModeEducation_appUsageStatsStale_queryAppUsageStats() = runTest {
// UsageStats caching interval is set to 0ms, that means caching should happen very
// frequently
testableResources.addOverride(
@@ -209,8 +172,7 @@ class AppHandleEducationFilterTest : ShellTestCase() {
0,
)
// The DataStore currently holds a proto object where Gmail's app launch count is recorded
- // as 4.
- // This value exceeds the minimum required count of 3.
+ // as 4. This value exceeds the minimum required count of 3.
testableResources.addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3)
val windowingEducationProto =
createWindowingEducationProto(
@@ -223,40 +185,20 @@ class AppHandleEducationFilterTest : ShellTestCase() {
.thenReturn(mapOf(GMAIL_PACKAGE_NAME to UsageStats().apply { mAppLaunchCount = 2 }))
`when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
- val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState())
+ val result = educationFilter.shouldShowDesktopModeEducation(createAppHandleState())
// Result should be false as queried usage stats should be considered to determine the
- // result
- // instead of cached stats
- assertThat(result).isFalse()
- }
-
- @Test
- fun shouldShowAppHandleEducation_appHandleMenuExpanded_returnsFalse() = runTest {
- val windowingEducationProto =
- createWindowingEducationProto(
- appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4),
- appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE,
- )
- // Simulate app handle menu is expanded
- val captionState = createAppHandleState(isHandleMenuExpanded = true)
- `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
-
- val result = educationFilter.shouldShowAppHandleEducation(captionState)
-
- // We should not show app handle education if app menu is expanded
+ // result instead of cached stats
assertThat(result).isFalse()
}
@Test
- fun shouldShowAppHandleEducation_overridePrerequisite_returnsTrue() = runTest {
+ fun shouldShowDesktopModeEducation_overridePrerequisite_returnsTrue() = runTest {
// Simulate that gmail app has been launched twice before, minimum app launch count is 3,
- // hence
- // #shouldShowAppHandleEducation should return false. But as we are overriding prerequisite
- // conditions, #shouldShowAppHandleEducation should return true.
+ // hence [shouldShowDesktopModeEducation] should return false. But as we are overriding
+ // prerequisite conditions, [shouldShowDesktopModeEducation] should return true.
testableResources.addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3)
- val systemPropertiesKey =
- "persist.desktop_windowing_app_handle_education_override_conditions"
+ val systemPropertiesKey = "persist.windowing_force_show_desktop_mode_education"
whenever(SystemProperties.getBoolean(eq(systemPropertiesKey), anyBoolean()))
.thenReturn(true)
val windowingEducationProto =
@@ -266,7 +208,7 @@ class AppHandleEducationFilterTest : ShellTestCase() {
)
`when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto)
- val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState())
+ val result = educationFilter.shouldShowDesktopModeEducation(createAppHandleState())
assertThat(result).isTrue()
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt
new file mode 100644
index 000000000000..a07203d86b75
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.desktopmode.multidesks
+
+import android.testing.AndroidTestingRunner
+import android.view.Display
+import android.view.SurfaceControl
+import android.window.TransitionInfo
+import android.window.WindowContainerTransaction
+import android.window.WindowContainerTransaction.HierarchyOp
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestShellExecutor
+import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask
+import com.android.wm.shell.sysui.ShellCommandHandler
+import com.android.wm.shell.sysui.ShellInit
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertThrows
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+
+/**
+ * Tests for [RootTaskDesksOrganizer].
+ *
+ * Usage: atest WMShellUnitTests:RootTaskDesksOrganizerTest
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class RootTaskDesksOrganizerTest : ShellTestCase() {
+
+ private val testExecutor = TestShellExecutor()
+ private val testShellInit = ShellInit(testExecutor)
+ private val mockShellCommandHandler = mock<ShellCommandHandler>()
+ private val mockShellTaskOrganizer = mock<ShellTaskOrganizer>()
+
+ private lateinit var organizer: RootTaskDesksOrganizer
+
+ @Before
+ fun setUp() {
+ organizer =
+ RootTaskDesksOrganizer(testShellInit, mockShellCommandHandler, mockShellTaskOrganizer)
+ }
+
+ @Test
+ fun testCreateDesk_callsBack() {
+ val callback = FakeOnCreateCallback()
+ organizer.createDesk(Display.DEFAULT_DISPLAY, callback)
+
+ val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
+ organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+
+ assertThat(callback.created).isTrue()
+ assertEquals(freeformRoot.taskId, callback.deskId)
+ }
+
+ @Test
+ fun testOnTaskAppeared_withoutRequest_throws() {
+ val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
+
+ assertThrows(Exception::class.java) {
+ organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+ }
+ }
+
+ @Test
+ fun testOnTaskAppeared_withRequestOnlyInAnotherDisplay_throws() {
+ organizer.createDesk(displayId = 2, FakeOnCreateCallback())
+ val freeformRoot = createFreeformTask(Display.DEFAULT_DISPLAY).apply { parentTaskId = -1 }
+
+ assertThrows(Exception::class.java) {
+ organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+ }
+ }
+
+ @Test
+ fun testOnTaskAppeared_duplicateRoot_throws() {
+ organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
+ val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
+ organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+
+ assertThrows(Exception::class.java) {
+ organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+ }
+ }
+
+ @Test
+ fun testOnTaskVanished_removesRoot() {
+ val callback = FakeOnCreateCallback()
+ organizer.createDesk(Display.DEFAULT_DISPLAY, callback)
+ val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
+ organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+
+ organizer.onTaskVanished(freeformRoot)
+
+ assertThat(organizer.roots.contains(freeformRoot.taskId)).isFalse()
+ }
+
+ @Test
+ fun testDesktopWindowAppearsInDesk() {
+ organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
+ val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
+ organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+ val child = createFreeformTask().apply { parentTaskId = freeformRoot.taskId }
+
+ organizer.onTaskAppeared(child, SurfaceControl())
+
+ assertThat(organizer.roots[freeformRoot.taskId].children).contains(child.taskId)
+ }
+
+ @Test
+ fun testDesktopWindowDisappearsFromDesk() {
+ organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
+ val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
+ organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+ val child = createFreeformTask().apply { parentTaskId = freeformRoot.taskId }
+
+ organizer.onTaskAppeared(child, SurfaceControl())
+ organizer.onTaskVanished(child)
+
+ assertThat(organizer.roots[freeformRoot.taskId].children).doesNotContain(child.taskId)
+ }
+
+ @Test
+ fun testRemoveDesk() {
+ organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
+ val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
+ organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+
+ val wct = WindowContainerTransaction()
+ organizer.removeDesk(wct, freeformRoot.taskId)
+
+ assertThat(
+ wct.hierarchyOps.any { hop ->
+ hop.type == HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_ROOT_TASK &&
+ hop.container == freeformRoot.token.asBinder()
+ }
+ )
+ .isTrue()
+ }
+
+ @Test
+ fun testRemoveDesk_didNotExist_throws() {
+ val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
+
+ val wct = WindowContainerTransaction()
+ assertThrows(Exception::class.java) { organizer.removeDesk(wct, freeformRoot.taskId) }
+ }
+
+ @Test
+ fun testActivateDesk() {
+ organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
+ val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
+ organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+
+ val wct = WindowContainerTransaction()
+ organizer.activateDesk(wct, freeformRoot.taskId)
+
+ assertThat(
+ wct.hierarchyOps.any { hop ->
+ hop.type == HierarchyOp.HIERARCHY_OP_TYPE_REORDER &&
+ hop.toTop &&
+ hop.container == freeformRoot.token.asBinder()
+ }
+ )
+ .isTrue()
+ assertThat(
+ wct.hierarchyOps.any { hop ->
+ hop.type == HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT &&
+ hop.container == freeformRoot.token.asBinder()
+ }
+ )
+ .isTrue()
+ }
+
+ @Test
+ fun testActivateDesk_didNotExist_throws() {
+ val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
+
+ val wct = WindowContainerTransaction()
+ assertThrows(Exception::class.java) { organizer.activateDesk(wct, freeformRoot.taskId) }
+ }
+
+ @Test
+ fun testMoveTaskToDesk() {
+ organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
+ val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
+ organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+
+ val desktopTask = createFreeformTask().apply { parentTaskId = -1 }
+ val wct = WindowContainerTransaction()
+ organizer.moveTaskToDesk(wct, freeformRoot.taskId, desktopTask)
+
+ assertThat(
+ wct.hierarchyOps.any { hop ->
+ hop.isReparent &&
+ hop.toTop &&
+ hop.container == desktopTask.token.asBinder() &&
+ hop.newParent == freeformRoot.token.asBinder()
+ }
+ )
+ .isTrue()
+ }
+
+ @Test
+ fun testMoveTaskToDesk_didNotExist_throws() {
+ val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
+
+ val desktopTask = createFreeformTask().apply { parentTaskId = -1 }
+ val wct = WindowContainerTransaction()
+ assertThrows(Exception::class.java) {
+ organizer.moveTaskToDesk(wct, freeformRoot.taskId, desktopTask)
+ }
+ }
+
+ @Test
+ fun testGetDeskAtEnd() {
+ organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
+ val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
+ organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+
+ val task = createFreeformTask().apply { parentTaskId = freeformRoot.taskId }
+ val endDesk =
+ organizer.getDeskAtEnd(
+ TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task }
+ )
+
+ assertThat(endDesk).isEqualTo(freeformRoot.taskId)
+ }
+
+ private class FakeOnCreateCallback : DesksOrganizer.OnCreateCallback {
+ var deskId: Int? = null
+ val created: Boolean
+ get() = deskId != null
+
+ override fun onCreated(deskId: Int) {
+ this.deskId = deskId
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt
index a3c441698905..9a8f264e98a4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt
@@ -17,6 +17,7 @@
package com.android.wm.shell.desktopmode.persistence
import android.os.UserManager
+import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
import android.testing.AndroidTestingRunner
@@ -24,6 +25,7 @@ import android.view.Display.DEFAULT_DISPLAY
import androidx.test.filters.SmallTest
import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_HSUM
import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE
+import com.android.window.flags.Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.desktopmode.DesktopUserRepositories
@@ -85,7 +87,9 @@ class DesktopRepositoryInitializerTest : ShellTestCase() {
@Test
@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE, FLAG_ENABLE_DESKTOP_WINDOWING_HSUM)
- fun initWithPersistence_multipleUsers_addedCorrectly() =
+ /** TODO: b/362720497 - add multi-desk version when implemented. */
+ @DisableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun initWithPersistence_multipleUsers_addedCorrectly_multiDesksDisabled() =
runTest(StandardTestDispatcher()) {
whenever(persistentRepository.getUserDesktopRepositoryMap())
.thenReturn(
@@ -145,7 +149,9 @@ class DesktopRepositoryInitializerTest : ShellTestCase() {
@Test
@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE)
- fun initWithPersistence_singleUser_addedCorrectly() =
+ /** TODO: b/362720497 - add multi-desk version when implemented. */
+ @DisableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun initWithPersistence_singleUser_addedCorrectly_multiDesksDisabled() =
runTest(StandardTestDispatcher()) {
whenever(persistentRepository.getUserDesktopRepositoryMap())
.thenReturn(mapOf(USER_ID_1 to desktopRepositoryState1))
@@ -156,24 +162,24 @@ class DesktopRepositoryInitializerTest : ShellTestCase() {
repositoryInitializer.initialize(desktopUserRepositories)
- // Desktop Repository currently returns all tasks across desktops for a specific user
- // since the repository currently doesn't handle desktops. This test logic should be
- // updated
- // once the repository handles multiple desktops.
assertThat(
- desktopUserRepositories.getProfile(USER_ID_1).getActiveTasks(DEFAULT_DISPLAY)
+ desktopUserRepositories
+ .getProfile(USER_ID_1)
+ .getActiveTaskIdsInDesk(deskId = DEFAULT_DISPLAY)
)
.containsExactly(1, 3, 4, 5)
.inOrder()
assertThat(
desktopUserRepositories
.getProfile(USER_ID_1)
- .getExpandedTasksOrdered(DEFAULT_DISPLAY)
+ .getExpandedTasksIdsInDeskOrdered(deskId = DEFAULT_DISPLAY)
)
.containsExactly(5, 1)
.inOrder()
assertThat(
- desktopUserRepositories.getProfile(USER_ID_1).getMinimizedTasks(DEFAULT_DISPLAY)
+ desktopUserRepositories
+ .getProfile(USER_ID_1)
+ .getMinimizedTaskIdsInDesk(deskId = DEFAULT_DISPLAY)
)
.containsExactly(3, 4)
.inOrder()
@@ -195,6 +201,7 @@ class DesktopRepositoryInitializerTest : ShellTestCase() {
val desktop1: Desktop =
Desktop.newBuilder()
.setDesktopId(DESKTOP_ID_1)
+ .setDisplayId(DEFAULT_DISPLAY)
.addAllZOrderedTasks(freeformTasksInZOrder1)
.putTasksByTaskId(
1,
@@ -216,6 +223,7 @@ class DesktopRepositoryInitializerTest : ShellTestCase() {
val desktop2: Desktop =
Desktop.newBuilder()
.setDesktopId(DESKTOP_ID_2)
+ .setDisplayId(DEFAULT_DISPLAY)
.addAllZOrderedTasks(freeformTasksInZOrder2)
.putTasksByTaskId(
4,
@@ -237,6 +245,7 @@ class DesktopRepositoryInitializerTest : ShellTestCase() {
val desktop3: Desktop =
Desktop.newBuilder()
.setDesktopId(DESKTOP_ID_3)
+ .setDisplayId(DEFAULT_DISPLAY)
.addAllZOrderedTasks(freeformTasksInZOrder3)
.putTasksByTaskId(
7,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
index 6c16b3220a07..4174bbd89b76 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
@@ -46,6 +46,7 @@ import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestRunningTaskInfoBuilder;
import com.android.wm.shell.common.LaunchAdjacentController;
+import com.android.wm.shell.desktopmode.DesktopModeLoggerTransitionObserver;
import com.android.wm.shell.desktopmode.DesktopRepository;
import com.android.wm.shell.desktopmode.DesktopTasksController;
import com.android.wm.shell.desktopmode.DesktopUserRepositories;
@@ -89,6 +90,8 @@ public final class FreeformTaskListenerTests extends ShellTestCase {
@Mock
private DesktopTasksController mDesktopTasksController;
@Mock
+ private DesktopModeLoggerTransitionObserver mDesktopModeLoggerTransitionObserver;
+ @Mock
private LaunchAdjacentController mLaunchAdjacentController;
@Mock
private TaskChangeListener mTaskChangeListener;
@@ -114,6 +117,7 @@ public final class FreeformTaskListenerTests extends ShellTestCase {
mTaskOrganizer,
Optional.of(mDesktopUserRepositories),
Optional.of(mDesktopTasksController),
+ mDesktopModeLoggerTransitionObserver,
mLaunchAdjacentController,
mWindowDecorViewModel,
Optional.of(mTaskChangeListener));
@@ -283,6 +287,19 @@ public final class FreeformTaskListenerTests extends ShellTestCase {
}
@Test
+ public void onTaskVanished_withDesktopModeLogger_forwards() {
+ ActivityManager.RunningTaskInfo task =
+ new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build();
+ task.isVisible = true;
+ mFreeformTaskListener.onTaskAppeared(task, mMockSurfaceControl);
+
+ mFreeformTaskListener.onTaskVanished(task);
+
+ verify(mDesktopModeLoggerTransitionObserver).onTaskVanished(task);
+ }
+
+
+ @Test
public void onTaskInfoChanged_withDesktopController_forwards() {
ActivityManager.RunningTaskInfo task =
new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
index 065fa219e8d0..542289db6cfc 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
@@ -211,6 +211,7 @@ public class RecentTasksControllerTest extends ShellTestCase {
@Test
public void testAddRemoveSplitNotifyChange() {
+ reset(mRecentTasksController);
RecentTaskInfo t1 = makeTaskInfo(1);
RecentTaskInfo t2 = makeTaskInfo(2);
setRawList(t1, t2);
@@ -225,6 +226,7 @@ public class RecentTasksControllerTest extends ShellTestCase {
@Test
public void testAddSameSplitBoundsInfoSkipNotifyChange() {
+ reset(mRecentTasksController);
RecentTaskInfo t1 = makeTaskInfo(1);
RecentTaskInfo t2 = makeTaskInfo(2);
setRawList(t1, t2);
@@ -535,6 +537,7 @@ public class RecentTasksControllerTest extends ShellTestCase {
@Test
public void testTaskWindowingModeChangedNotifiesChange() {
+ reset(mRecentTasksController);
RecentTaskInfo t1 = makeTaskInfo(1);
setRawList(t1);
@@ -551,7 +554,8 @@ public class RecentTasksControllerTest extends ShellTestCase {
WINDOWING_MODE_MULTI_WINDOW);
mShellTaskOrganizer.onTaskInfoChanged(rt2MultiWIndow);
- verify(mRecentTasksController).notifyRecentTasksChanged();
+ // One for onTaskAppeared and one for onTaskInfoChanged
+ verify(mRecentTasksController, times(2)).notifyRecentTasksChanged();
}
@Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/AppCompatUtilsTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt
index 7157a7f0b38f..8c78debdc19f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/AppCompatUtilsTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt
@@ -14,29 +14,38 @@
* limitations under the License.
*/
-package com.android.wm.shell.compatui
+package com.android.wm.shell.shared.desktopmode
import android.content.ComponentName
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.internal.R
+import com.android.wm.shell.compatui.CompatUIShellTestCase
import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
/**
- * Tests for [@link AppCompatUtils].
+ * Tests for [@link DesktopModeCompatPolicy].
*
- * Build/Install/Run: atest WMShellUnitTests:AppCompatUtilsTest
+ * Build/Install/Run: atest WMShellUnitTests:DesktopModeCompatPolicyTest
*/
@RunWith(AndroidTestingRunner::class)
@SmallTest
-class AppCompatUtilsTest : CompatUIShellTestCase() {
+class DesktopModeCompatPolicyTest : CompatUIShellTestCase() {
+ private lateinit var desktopModeCompatPolicy: DesktopModeCompatPolicy
+
+ @Before
+ fun setUp() {
+ desktopModeCompatPolicy = DesktopModeCompatPolicy(mContext)
+ }
+
@Test
fun testIsTopActivityExemptFromDesktopWindowing_onlyTransparentActivitiesInStack() {
- assertTrue(isTopActivityExemptFromDesktopWindowing(mContext,
+ assertTrue(desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(
createFreeformTask(/* displayId */ 0)
.apply {
isActivityStackTransparent = true
@@ -47,7 +56,7 @@ class AppCompatUtilsTest : CompatUIShellTestCase() {
@Test
fun testIsTopActivityExemptFromDesktopWindowing_noActivitiesInStack() {
- assertFalse(isTopActivityExemptFromDesktopWindowing(mContext,
+ assertFalse(desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(
createFreeformTask(/* displayId */ 0)
.apply {
isActivityStackTransparent = true
@@ -58,7 +67,7 @@ class AppCompatUtilsTest : CompatUIShellTestCase() {
@Test
fun testIsTopActivityExemptFromDesktopWindowing_nonTransparentActivitiesInStack() {
- assertFalse(isTopActivityExemptFromDesktopWindowing(mContext,
+ assertFalse(desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(
createFreeformTask(/* displayId */ 0)
.apply {
isActivityStackTransparent = false
@@ -69,7 +78,7 @@ class AppCompatUtilsTest : CompatUIShellTestCase() {
@Test
fun testIsTopActivityExemptFromDesktopWindowing_transparentActivityStack_notDisplayed() {
- assertFalse(isTopActivityExemptFromDesktopWindowing(mContext,
+ assertFalse(desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(
createFreeformTask(/* displayId */ 0)
.apply {
isActivityStackTransparent = true
@@ -82,7 +91,7 @@ class AppCompatUtilsTest : CompatUIShellTestCase() {
fun testIsTopActivityExemptFromDesktopWindowing_systemUiTask() {
val systemUIPackageName = context.resources.getString(R.string.config_systemUi)
val baseComponent = ComponentName(systemUIPackageName, /* class */ "")
- assertTrue(isTopActivityExemptFromDesktopWindowing(mContext,
+ assertTrue(desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(
createFreeformTask(/* displayId */ 0)
.apply {
baseActivity = baseComponent
@@ -94,7 +103,7 @@ class AppCompatUtilsTest : CompatUIShellTestCase() {
fun testIsTopActivityExemptFromDesktopWindowing_systemUiTask_notDisplayed() {
val systemUIPackageName = context.resources.getString(R.string.config_systemUi)
val baseComponent = ComponentName(systemUIPackageName, /* class */ "")
- assertFalse(isTopActivityExemptFromDesktopWindowing(mContext,
+ assertFalse(desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(
createFreeformTask(/* displayId */ 0)
.apply {
baseActivity = baseComponent
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
index bb9703fce2e3..7f6b06d46abf 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
@@ -213,7 +213,7 @@ public class SplitScreenControllerTests extends ShellTestCase {
@Test
public void startIntent_multiInstancesSupported_appendsMultipleTaskFag() {
- doReturn(true).when(mMultiInstanceHelper).supportsMultiInstanceSplit(any());
+ doReturn(true).when(mMultiInstanceHelper).supportsMultiInstanceSplit(any(), anyInt());
Intent startIntent = createStartIntent("startActivity");
PendingIntent pendingIntent =
PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
@@ -252,13 +252,13 @@ public class SplitScreenControllerTests extends ShellTestCase {
verify(mStageCoordinator).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT),
isNull(), isNull(), eq(SPLIT_INDEX_0));
- verify(mMultiInstanceHelper, never()).supportsMultiInstanceSplit(any());
+ verify(mMultiInstanceHelper, never()).supportsMultiInstanceSplit(any(), anyInt());
verify(mStageCoordinator, never()).switchSplitPosition(any());
}
@Test
public void startIntent_multiInstancesSupported_startTaskInBackgroundAfterSplitActivated() {
- doReturn(true).when(mMultiInstanceHelper).supportsMultiInstanceSplit(any());
+ doReturn(true).when(mMultiInstanceHelper).supportsMultiInstanceSplit(any(), anyInt());
doNothing().when(mSplitScreenController).startTask(anyInt(), anyInt(), any(), any());
Intent startIntent = createStartIntent("startActivity");
PendingIntent pendingIntent =
@@ -276,14 +276,14 @@ public class SplitScreenControllerTests extends ShellTestCase {
mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null,
SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */,
SPLIT_INDEX_0);
- verify(mMultiInstanceHelper, never()).supportsMultiInstanceSplit(any());
+ verify(mMultiInstanceHelper, never()).supportsMultiInstanceSplit(any(), anyInt());
verify(mStageCoordinator).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT),
isNull(), isNull(), eq(SPLIT_INDEX_0));
}
@Test
public void startIntent_multiInstancesNotSupported_switchesPositionAfterSplitActivated() {
- doReturn(false).when(mMultiInstanceHelper).supportsMultiInstanceSplit(any());
+ doReturn(false).when(mMultiInstanceHelper).supportsMultiInstanceSplit(any(), anyInt());
Intent startIntent = createStartIntent("startActivity");
PendingIntent pendingIntent =
PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
index 4211e4682810..b9d6a454694d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
@@ -67,6 +67,7 @@ import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.launcher3.icons.IconProvider;
+import com.android.wm.shell.Flags;
import com.android.wm.shell.MockToken;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTaskOrganizer;
@@ -355,8 +356,13 @@ public class SplitTransitionTests extends ShellTestCase {
// Make sure it cleans-up if recents doesn't restore
WindowContainerTransaction commitWCT = new WindowContainerTransaction();
- mStageCoordinator.onRecentsInSplitAnimationFinish(commitWCT,
- mock(SurfaceControl.Transaction.class));
+ if (Flags.enableRecentsBookendTransition()) {
+ mStageCoordinator.onRecentsInSplitAnimationFinishing(false /* returnToApp */, commitWCT,
+ mock(SurfaceControl.Transaction.class));
+ } else {
+ mStageCoordinator.onRecentsInSplitAnimationFinish(commitWCT,
+ mock(SurfaceControl.Transaction.class));
+ }
assertFalse(mStageCoordinator.isSplitScreenVisible());
}
@@ -420,8 +426,13 @@ public class SplitTransitionTests extends ShellTestCase {
// simulate the restoreWCT being applied:
mMainStage.onTaskAppeared(mMainChild, mock(SurfaceControl.class));
mSideStage.onTaskAppeared(mSideChild, mock(SurfaceControl.class));
- mStageCoordinator.onRecentsInSplitAnimationFinish(restoreWCT,
- mock(SurfaceControl.Transaction.class));
+ if (Flags.enableRecentsBookendTransition()) {
+ mStageCoordinator.onRecentsInSplitAnimationFinishing(true /* returnToApp */, restoreWCT,
+ mock(SurfaceControl.Transaction.class));
+ } else {
+ mStageCoordinator.onRecentsInSplitAnimationFinish(restoreWCT,
+ mock(SurfaceControl.Transaction.class));
+ }
assertTrue(mStageCoordinator.isSplitScreenVisible());
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/WindowingEducationTestUtils.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/WindowingEducationTestUtils.kt
index b9d91e7895db..546848421302 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/WindowingEducationTestUtils.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/WindowingEducationTestUtils.kt
@@ -81,7 +81,9 @@ fun createWindowingEducationProto(
appHandleHintViewedTimestampMillis: Long? = null,
appHandleHintUsedTimestampMillis: Long? = null,
appUsageStats: Map<String, Int>? = null,
- appUsageStatsLastUpdateTimestampMillis: Long? = null
+ appUsageStatsLastUpdateTimestampMillis: Long? = null,
+ enterDesktopModeHintViewedTimestampMillis: Long? = null,
+ exitDesktopModeHintViewedTimestampMillis: Long? = null,
): WindowingEducationProto =
WindowingEducationProto.newBuilder()
.apply {
@@ -91,6 +93,12 @@ fun createWindowingEducationProto(
if (appHandleHintUsedTimestampMillis != null) {
setAppHandleHintUsedTimestampMillis(appHandleHintUsedTimestampMillis)
}
+ if (enterDesktopModeHintViewedTimestampMillis != null) {
+ setEnterDesktopModeHintViewedTimestampMillis(enterDesktopModeHintViewedTimestampMillis)
+ }
+ if (exitDesktopModeHintViewedTimestampMillis != null) {
+ setExitDesktopModeHintViewedTimestampMillis(exitDesktopModeHintViewedTimestampMillis)
+ }
setAppHandleEducation(
createAppHandleEducationProto(appUsageStats, appUsageStatsLastUpdateTimestampMillis))
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt
index 908bc9952e99..c5c827467c75 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt
@@ -69,6 +69,7 @@ import com.android.wm.shell.desktopmode.education.AppToWebEducationController
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
import com.android.wm.shell.recents.RecentsTransitionHandler
import com.android.wm.shell.recents.RecentsTransitionStateListener
+import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy
import com.android.wm.shell.splitscreen.SplitScreenController
import com.android.wm.shell.sysui.ShellCommandHandler
import com.android.wm.shell.sysui.ShellController
@@ -161,6 +162,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() {
val display = mock<Display>()
protected lateinit var spyContext: TestableContext
private lateinit var desktopModeEventLogger: DesktopModeEventLogger
+ private lateinit var desktopModeCompatPolicy: DesktopModeCompatPolicy
private val transactionFactory = Supplier<SurfaceControl.Transaction> {
SurfaceControl.Transaction()
@@ -188,6 +190,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() {
whenever(mockDisplayController.getDisplay(any())).thenReturn(display)
whenever(mockDesktopUserRepositories.getProfile(anyInt()))
.thenReturn(mockDesktopRepository)
+ desktopModeCompatPolicy = DesktopModeCompatPolicy(context)
desktopModeWindowDecorViewModel = DesktopModeWindowDecorViewModel(
spyContext,
testShellExecutor,
@@ -230,6 +233,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() {
mock<DesktopModeUiEventLogger>(),
mock<WindowDecorTaskResourceLoader>(),
mockRecentsTransitionHandler,
+ desktopModeCompatPolicy,
)
desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController)
whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index 9ea5fd6e1abe..87198d14c839 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -280,7 +280,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
mTestableContext = new TestableContext(mContext);
mTestableContext.ensureTestableResources();
mContext.setMockPackageManager(mMockPackageManager);
- when(mMockMultiInstanceHelper.supportsMultiInstanceSplit(any()))
+ when(mMockMultiInstanceHelper.supportsMultiInstanceSplit(any(), anyInt()))
.thenReturn(false);
when(mMockPackageManager.getApplicationLabel(any())).thenReturn("applicationLabel");
final ActivityInfo activityInfo = createActivityInfo();
@@ -295,7 +295,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(),
any(), anyInt(), anyInt(), anyInt(), anyInt()))
.thenReturn(mMockHandleMenu);
- when(mMockMultiInstanceHelper.supportsMultiInstanceSplit(any())).thenReturn(false);
+ when(mMockMultiInstanceHelper.supportsMultiInstanceSplit(any(), anyInt()))
+ .thenReturn(false);
when(mMockAppHeaderViewHolderFactory.create(any(), any(), any(), any(), any(), any()))
.thenReturn(mMockAppHeaderViewHolder);
when(mMockDesktopUserRepositories.getCurrent()).thenReturn(mDesktopRepository);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt
index 2207c705d7dc..0615c1d677ba 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt
@@ -51,6 +51,7 @@ import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFI
import java.util.function.Supplier
import junit.framework.Assert
import org.junit.Before
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
@@ -206,6 +207,7 @@ class MultiDisplayVeiledResizeTaskPositionerTest : ShellTestCase() {
}
@Test
+ @Ignore("Causing presubmit failure b/391717499")
fun testDragResize_movesTask_doesNotShowResizeVeil() = runOnUiThread {
taskPositioner.onDragPositioningStart(
CTRL_TYPE_UNDEFINED,
@@ -245,6 +247,7 @@ class MultiDisplayVeiledResizeTaskPositionerTest : ShellTestCase() {
}
@Test
+ @Ignore("Causing presubmit failure b/391717499")
fun testDragResize_movesTaskToNewDisplay() = runOnUiThread {
taskPositioner.onDragPositioningStart(
CTRL_TYPE_UNDEFINED,
@@ -370,6 +373,7 @@ class MultiDisplayVeiledResizeTaskPositionerTest : ShellTestCase() {
}
@Test
+ @Ignore("Causing presubmit failure b/391717499")
fun testDragResize_drag_setBoundsNotRunIfDragEndsInDisallowedEndArea() = runOnUiThread {
taskPositioner.onDragPositioningStart(
CTRL_TYPE_UNDEFINED, // drag
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt
index 1ec0fe794d0a..431de896f433 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt
@@ -33,6 +33,7 @@ import com.android.launcher3.icons.IconProvider
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.TestRunningTaskInfoBuilder
import com.android.wm.shell.TestShellExecutor
+import com.android.wm.shell.common.UserProfileContexts
import com.android.wm.shell.sysui.ShellController
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.sysui.UserChangeListener
@@ -69,6 +70,7 @@ class WindowDecorTaskResourceLoaderTest : ShellTestCase() {
private val mockIconProvider = mock<IconProvider>()
private val mockHeaderIconFactory = mock<BaseIconFactory>()
private val mockVeilIconFactory = mock<BaseIconFactory>()
+ private val mMockUserProfileContexts = mock<UserProfileContexts>()
private lateinit var spyContext: TestableContext
private lateinit var loader: WindowDecorTaskResourceLoader
@@ -83,12 +85,13 @@ class WindowDecorTaskResourceLoaderTest : ShellTestCase() {
spyContext = spy(mContext)
spyContext.setMockPackageManager(mockPackageManager)
doReturn(spyContext).whenever(spyContext).createContextAsUser(any(), anyInt())
+ doReturn(spyContext).whenever(mMockUserProfileContexts)[anyInt()]
loader =
WindowDecorTaskResourceLoader(
- context = spyContext,
shellInit = shellInit,
shellController = mockShellController,
shellCommandHandler = mock(),
+ userProfilesContexts = mMockUserProfileContexts,
iconProvider = mockIconProvider,
headerIconFactory = mockHeaderIconFactory,
veilIconFactory = mockVeilIconFactory,
@@ -170,16 +173,6 @@ class WindowDecorTaskResourceLoaderTest : ShellTestCase() {
}
@Test
- fun testUserChange_updatesContext() {
- val newUser = 5000
- val newContext = mock<Context>()
-
- userChangeListener.onUserChanged(newUser, newContext)
-
- assertThat(loader.currentUserContext).isEqualTo(newContext)
- }
-
- @Test
fun testUserChange_clearsCache() {
val newUser = 5000
val newContext = mock<Context>()
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt
index 997ece6ecadc..2cabb9a33b86 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt
@@ -23,6 +23,7 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.common.SyncTransactionQueue
import com.android.wm.shell.desktopmode.DesktopModeEventLogger
import com.android.wm.shell.desktopmode.DesktopUserRepositories
@@ -30,6 +31,7 @@ import com.android.wm.shell.desktopmode.DesktopTasksController
import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask
import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator
import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler
+import com.android.wm.shell.transition.FocusTransitionObserver
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader
@@ -67,6 +69,8 @@ class DesktopTilingDecorViewModelTest : ShellTestCase() {
private val desktopModeWindowDecorationMock: DesktopModeWindowDecoration = mock()
private val desktopTilingDecoration: DesktopTilingWindowDecoration = mock()
private val taskResourceLoader: WindowDecorTaskResourceLoader = mock()
+ private val focusTransitionObserver: FocusTransitionObserver = mock()
+ private val mainExecutor: ShellExecutor = mock()
private lateinit var desktopTilingDecorViewModel: DesktopTilingDecorViewModel
@Before
@@ -86,6 +90,8 @@ class DesktopTilingDecorViewModelTest : ShellTestCase() {
userRepositories,
desktopModeEventLogger,
taskResourceLoader,
+ focusTransitionObserver,
+ mainExecutor
)
whenever(contextMock.createContextAsUser(any(), any())).thenReturn(contextMock)
}
@@ -140,7 +146,7 @@ class DesktopTilingDecorViewModelTest : ShellTestCase() {
desktopTilingDecorViewModel.moveTaskToFrontIfTiled(task1)
verify(desktopTilingDecoration, times(1))
- .moveTiledPairToFront(any(), isTaskFocused = eq(true))
+ .moveTiledPairToFront(any(), isFocusedOnDisplay = eq(true))
}
@Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt
index 2f15c2e38855..399a51e1ed08 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt
@@ -33,6 +33,7 @@ import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayLayout
+import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.common.SyncTransactionQueue
import com.android.wm.shell.desktopmode.DesktopModeEventLogger
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger
@@ -42,6 +43,7 @@ import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask
import com.android.wm.shell.desktopmode.DesktopUserRepositories
import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator
import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler
+import com.android.wm.shell.transition.FocusTransitionObserver
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
import com.android.wm.shell.windowdecor.DragResizeWindowGeometry
@@ -105,6 +107,8 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() {
private val mainDispatcher: MainCoroutineDispatcher = mock()
private val bgScope: CoroutineScope = mock()
private val taskResourceLoader: WindowDecorTaskResourceLoader = mock()
+ private val focusTransitionObserver: FocusTransitionObserver = mock()
+ private val mainExecutor: ShellExecutor = mock()
private lateinit var tilingDecoration: DesktopTilingWindowDecoration
private val split_divider_width = 10
@@ -129,6 +133,8 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() {
returnToDragStartAnimator,
userRepositories,
desktopModeEventLogger,
+ focusTransitionObserver,
+ mainExecutor
)
whenever(context.createContextAsUser(any(), any())).thenReturn(context)
whenever(userRepositories.current).thenReturn(desktopRepository)
@@ -242,7 +248,7 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() {
BOUNDS,
)
- assertThat(tilingDecoration.moveTiledPairToFront(task2)).isFalse()
+ assertThat(tilingDecoration.moveTiledPairToFront(task2.taskId, false)).isFalse()
verify(transitions, never()).startTransition(any(), any(), any())
}
@@ -272,7 +278,7 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() {
BOUNDS,
)
- assertThat(tilingDecoration.moveTiledPairToFront(task3)).isFalse()
+ assertThat(tilingDecoration.moveTiledPairToFront(task3.taskId, false)).isFalse()
verify(transitions, never()).startTransition(any(), any(), any())
}
@@ -304,7 +310,7 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() {
)
task1.isFocused = true
- assertThat(tilingDecoration.moveTiledPairToFront(task1, isTaskFocused = true)).isTrue()
+ assertThat(tilingDecoration.moveTiledPairToFront(task1.taskId, isFocusedOnDisplay = true)).isTrue()
verify(transitions, times(1)).startTransition(eq(TRANSIT_TO_FRONT), any(), eq(null))
}
@@ -336,8 +342,8 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() {
task1.isFocused = true
task3.isFocused = true
- assertThat(tilingDecoration.moveTiledPairToFront(task3)).isFalse()
- assertThat(tilingDecoration.moveTiledPairToFront(task1)).isTrue()
+ assertThat(tilingDecoration.moveTiledPairToFront(task3.taskId, true)).isFalse()
+ assertThat(tilingDecoration.moveTiledPairToFront(task1.taskId, true)).isTrue()
verify(transitions, times(1)).startTransition(eq(TRANSIT_TO_FRONT), any(), eq(null))
}
@@ -367,8 +373,8 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() {
BOUNDS,
)
- assertThat(tilingDecoration.moveTiledPairToFront(task3, isTaskFocused = true)).isFalse()
- assertThat(tilingDecoration.moveTiledPairToFront(task1, isTaskFocused = true)).isTrue()
+ assertThat(tilingDecoration.moveTiledPairToFront(task3.taskId, isFocusedOnDisplay = true)).isFalse()
+ assertThat(tilingDecoration.moveTiledPairToFront(task1.taskId, isFocusedOnDisplay = true)).isTrue()
verify(transitions, times(1)).startTransition(eq(TRANSIT_TO_FRONT), any(), eq(null))
}
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index 677fd86aca9c..53d3b77f1ba2 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -206,6 +206,9 @@ java_sdk_library {
visibility: [
"//frameworks/base", // Framework
],
+ impl_library_visibility: [
+ "//frameworks/base/ravenwood",
+ ],
srcs: [
":framework-graphics-srcs",
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 52eae43f7db9..71013f7f4e34 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -21,6 +21,8 @@ import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
import static android.content.Context.DEVICE_ID_DEFAULT;
import static android.media.audio.Flags.autoPublicVolumeApiHardening;
import static android.media.audio.Flags.automaticBtDeviceType;
+import static android.media.audio.Flags.cacheGetStreamMinMaxVolume;
+import static android.media.audio.Flags.cacheGetStreamVolume;
import static android.media.audio.Flags.FLAG_DEPRECATE_STREAM_BT_SCO;
import static android.media.audio.Flags.FLAG_FOCUS_EXCLUSIVE_WITH_RECORDING;
import static android.media.audio.Flags.FLAG_FOCUS_FREEZE_TEST_API;
@@ -58,7 +60,6 @@ import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.media.AudioAttributes.AttributeSystemUsage;
-import android.media.AudioDeviceInfo;
import android.media.CallbackUtil.ListenerInfo;
import android.media.audiopolicy.AudioPolicy;
import android.media.audiopolicy.AudioPolicy.AudioPolicyFocusListener;
@@ -75,6 +76,7 @@ import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
+import android.os.IpcDataCache;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
@@ -1231,6 +1233,102 @@ public class AudioManager {
}
/**
+ * API string for caching the min volume for each stream
+ * @hide
+ **/
+ public static final String VOLUME_MIN_CACHING_API = "getStreamMinVolume";
+ /**
+ * API string for caching the max volume for each stream
+ * @hide
+ **/
+ public static final String VOLUME_MAX_CACHING_API = "getStreamMaxVolume";
+ /**
+ * API string for caching the volume for each stream
+ * @hide
+ **/
+ public static final String VOLUME_CACHING_API = "getStreamVolume";
+ private static final int VOLUME_CACHING_SIZE = 16;
+
+ private final IpcDataCache.QueryHandler<VolumeCacheQuery, Integer> mVolQuery =
+ new IpcDataCache.QueryHandler<>() {
+ @Override
+ public Integer apply(VolumeCacheQuery query) {
+ final IAudioService service = getService();
+ try {
+ return switch (query.queryCommand) {
+ case QUERY_VOL_MIN -> service.getStreamMinVolume(query.stream);
+ case QUERY_VOL_MAX -> service.getStreamMaxVolume(query.stream);
+ case QUERY_VOL -> service.getStreamVolume(query.stream);
+ default -> {
+ Log.w(TAG, "Not a valid volume cache query: " + query);
+ yield null;
+ }
+ };
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ };
+
+ private final IpcDataCache<VolumeCacheQuery, Integer> mVolMinCache =
+ new IpcDataCache<>(VOLUME_CACHING_SIZE, IpcDataCache.MODULE_SYSTEM,
+ VOLUME_MIN_CACHING_API, VOLUME_MIN_CACHING_API, mVolQuery);
+
+ private final IpcDataCache<VolumeCacheQuery, Integer> mVolMaxCache =
+ new IpcDataCache<>(VOLUME_CACHING_SIZE, IpcDataCache.MODULE_SYSTEM,
+ VOLUME_MAX_CACHING_API, VOLUME_MAX_CACHING_API, mVolQuery);
+
+ private final IpcDataCache<VolumeCacheQuery, Integer> mVolCache =
+ new IpcDataCache<>(VOLUME_CACHING_SIZE, IpcDataCache.MODULE_SYSTEM,
+ VOLUME_CACHING_API, VOLUME_CACHING_API, mVolQuery);
+
+ /**
+ * Used to invalidate the cache for the given API
+ * @hide
+ **/
+ public static void clearVolumeCache(String api) {
+ if (cacheGetStreamMinMaxVolume() && (VOLUME_MAX_CACHING_API.equals(api)
+ || VOLUME_MIN_CACHING_API.equals(api))) {
+ IpcDataCache.invalidateCache(IpcDataCache.MODULE_SYSTEM, api);
+ } else if (cacheGetStreamVolume() && VOLUME_CACHING_API.equals(api)) {
+ IpcDataCache.invalidateCache(IpcDataCache.MODULE_SYSTEM, api);
+ } else {
+ Log.w(TAG, "invalid clearVolumeCache for api " + api);
+ }
+ }
+
+ private static final int QUERY_VOL_MIN = 1;
+ private static final int QUERY_VOL_MAX = 2;
+ private static final int QUERY_VOL = 3;
+ /** @hide */
+ @IntDef(prefix = "QUERY_VOL", value = {
+ QUERY_VOL_MIN,
+ QUERY_VOL_MAX,
+ QUERY_VOL}
+ )
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface QueryVolCommand {}
+
+ private record VolumeCacheQuery(int stream, @QueryVolCommand int queryCommand) {
+ private String queryVolCommandToString() {
+ return switch (queryCommand) {
+ case QUERY_VOL_MIN -> "getStreamMinVolume";
+ case QUERY_VOL_MAX -> "getStreamMaxVolume";
+ case QUERY_VOL -> "getStreamVolume";
+ default -> "invalid command";
+ };
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return TextUtils.formatSimple("VolumeCacheQuery(stream=%d, queryCommand=%s)", stream,
+ queryVolCommandToString());
+ }
+ }
+
+ /**
* Returns the maximum volume index for a particular stream.
*
* @param streamType The stream type whose maximum volume index is returned.
@@ -1238,6 +1336,9 @@ public class AudioManager {
* @see #getStreamVolume(int)
*/
public int getStreamMaxVolume(int streamType) {
+ if (cacheGetStreamMinMaxVolume()) {
+ return mVolMaxCache.query(new VolumeCacheQuery(streamType, QUERY_VOL_MAX));
+ }
final IAudioService service = getService();
try {
return service.getStreamMaxVolume(streamType);
@@ -1271,6 +1372,9 @@ public class AudioManager {
*/
@TestApi
public int getStreamMinVolumeInt(int streamType) {
+ if (cacheGetStreamMinMaxVolume()) {
+ return mVolMinCache.query(new VolumeCacheQuery(streamType, QUERY_VOL_MIN));
+ }
final IAudioService service = getService();
try {
return service.getStreamMinVolume(streamType);
@@ -1288,6 +1392,9 @@ public class AudioManager {
* @see #setStreamVolume(int, int, int)
*/
public int getStreamVolume(int streamType) {
+ if (cacheGetStreamVolume()) {
+ return mVolCache.query(new VolumeCacheQuery(streamType, QUERY_VOL));
+ }
final IAudioService service = getService();
try {
return service.getStreamVolume(streamType);
diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java
index c9625c405faa..c4886836f451 100644
--- a/media/java/android/media/MediaCodec.java
+++ b/media/java/android/media/MediaCodec.java
@@ -20,6 +20,8 @@ import static android.media.codec.Flags.FLAG_CODEC_AVAILABILITY;
import static android.media.codec.Flags.FLAG_NULL_OUTPUT_SURFACE;
import static android.media.codec.Flags.FLAG_REGION_OF_INTEREST;
import static android.media.codec.Flags.FLAG_SUBSESSION_METRICS;
+import static android.media.tv.flags.Flags.applyPictureProfiles;
+import static android.media.tv.flags.Flags.mediaQualityFw;
import static com.android.media.codec.flags.Flags.FLAG_LARGE_AUDIO_FRAME;
@@ -37,6 +39,8 @@ import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.hardware.HardwareBuffer;
import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.quality.PictureProfile;
+import android.media.quality.PictureProfileHandle;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
@@ -5370,6 +5374,9 @@ final public class MediaCodec {
* @param params The bundle of parameters to set.
* @throws IllegalStateException if in the Released state.
*/
+
+ private static final String PARAMETER_KEY_PICTURE_PROFILE_HANDLE = "picture-profile-handle";
+
public final void setParameters(@Nullable Bundle params) {
if (params == null) {
return;
@@ -5383,19 +5390,41 @@ final public class MediaCodec {
if (key.equals(MediaFormat.KEY_AUDIO_SESSION_ID)) {
int sessionId = 0;
try {
- sessionId = (Integer)params.get(key);
+ sessionId = (Integer) params.get(key);
} catch (Exception e) {
throw new IllegalArgumentException("Wrong Session ID Parameter!");
}
keys[i] = "audio-hw-sync";
values[i] = AudioSystem.getAudioHwSyncForSession(sessionId);
+ } else if (applyPictureProfiles() && mediaQualityFw()
+ && key.equals(MediaFormat.KEY_PICTURE_PROFILE_INSTANCE)) {
+ PictureProfile pictureProfile = null;
+ try {
+ pictureProfile = (PictureProfile) params.get(key);
+ } catch (ClassCastException e) {
+ throw new IllegalArgumentException(
+ "Cannot cast the instance parameter to PictureProfile!");
+ } catch (Exception e) {
+ android.util.Log.getStackTraceString(e);
+ throw new IllegalArgumentException("Unexpected exception when casting the "
+ + "instance parameter to PictureProfile!");
+ }
+ if (pictureProfile == null) {
+ throw new IllegalArgumentException(
+ "Picture profile instance parameter is null!");
+ }
+ PictureProfileHandle handle = pictureProfile.getHandle();
+ if (handle != PictureProfileHandle.NONE) {
+ keys[i] = PARAMETER_KEY_PICTURE_PROFILE_HANDLE;
+ values[i] = Long.valueOf(handle.getId());
+ }
} else {
keys[i] = key;
Object value = params.get(key);
// Bundle's byte array is a byte[], JNI layer only takes ByteBuffer
if (value instanceof byte[]) {
- values[i] = ByteBuffer.wrap((byte[])value);
+ values[i] = ByteBuffer.wrap((byte[]) value);
} else {
values[i] = value;
}
diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java
index e57148fe5a6a..3af36a404c30 100644
--- a/media/java/android/media/MediaRouter2.java
+++ b/media/java/android/media/MediaRouter2.java
@@ -281,7 +281,7 @@ public final class MediaRouter2 {
/* executor */ null,
/* onInstanceInvalidatedListener */ null);
} catch (IllegalArgumentException ex) {
- Log.e(TAG, "Package " + clientPackageName + " not found. Ignoring.");
+ Log.e(TAG, "Failed to create proxy router for package '" + clientPackageName + "'", ex);
return null;
}
}
diff --git a/media/java/android/media/flags/projection.aconfig b/media/java/android/media/flags/projection.aconfig
index fa1349c61c4c..6d4f0b4f47d5 100644
--- a/media/java/android/media/flags/projection.aconfig
+++ b/media/java/android/media/flags/projection.aconfig
@@ -29,3 +29,13 @@ flag {
is_exported: true
}
+flag {
+ namespace: "media_projection"
+ name: "show_stop_dialog_post_call_end"
+ description: "Shows a stop dialog for MediaProjection sessions that started during call and remain active after a call ends"
+ bug: "390343524"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+ is_exported: true
+}
diff --git a/media/java/android/media/projection/IMediaProjectionWatcherCallback.aidl b/media/java/android/media/projection/IMediaProjectionWatcherCallback.aidl
index e46d34e81483..3baf4d7efd65 100644
--- a/media/java/android/media/projection/IMediaProjectionWatcherCallback.aidl
+++ b/media/java/android/media/projection/IMediaProjectionWatcherCallback.aidl
@@ -18,6 +18,7 @@ package android.media.projection;
import android.media.projection.MediaProjectionInfo;
import android.view.ContentRecordingSession;
+import android.media.projection.MediaProjectionEvent;
/** {@hide} */
oneway interface IMediaProjectionWatcherCallback {
@@ -35,4 +36,19 @@ oneway interface IMediaProjectionWatcherCallback {
in MediaProjectionInfo info,
in @nullable ContentRecordingSession session
);
+
+ /**
+ * Called when a specific {@link MediaProjectionEvent} occurs during the media projection session.
+ *
+ * @param event contains the event type, which describes the nature/context of the event.
+ * @param info optional {@link MediaProjectionInfo} containing details about the media
+ projection host.
+ * @param session the recording session for the current media projection. Can be
+ * {@code null} when the recording will stop.
+ */
+ void onMediaProjectionEvent(
+ in MediaProjectionEvent event,
+ in @nullable MediaProjectionInfo info,
+ in @nullable ContentRecordingSession session
+ );
}
diff --git a/media/java/android/media/projection/MediaProjectionEvent.aidl b/media/java/android/media/projection/MediaProjectionEvent.aidl
new file mode 100644
index 000000000000..34359900ce81
--- /dev/null
+++ b/media/java/android/media/projection/MediaProjectionEvent.aidl
@@ -0,0 +1,3 @@
+package android.media.projection;
+
+parcelable MediaProjectionEvent; \ No newline at end of file
diff --git a/media/java/android/media/projection/MediaProjectionEvent.java b/media/java/android/media/projection/MediaProjectionEvent.java
new file mode 100644
index 000000000000..6922560c8abe
--- /dev/null
+++ b/media/java/android/media/projection/MediaProjectionEvent.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.projection;
+
+import android.annotation.IntDef;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/** @hide */
+public final class MediaProjectionEvent implements Parcelable {
+
+ /**
+ * Represents various media projection events.
+ */
+ @IntDef({PROJECTION_STARTED_DURING_CALL_AND_ACTIVE_POST_CALL})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface EventType {}
+
+ /** Event type for when a call ends but the session is still active. */
+ public static final int PROJECTION_STARTED_DURING_CALL_AND_ACTIVE_POST_CALL = 0;
+
+ private final @EventType int mEventType;
+ private final long mTimestampMillis;
+
+ public MediaProjectionEvent(@EventType int eventType, long timestampMillis) {
+ mEventType = eventType;
+ mTimestampMillis = timestampMillis;
+ }
+
+ private MediaProjectionEvent(Parcel in) {
+ mEventType = in.readInt();
+ mTimestampMillis = in.readLong();
+ }
+
+ public @EventType int getEventType() {
+ return mEventType;
+ }
+
+ public long getTimestampMillis() {
+ return mTimestampMillis;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof MediaProjectionEvent other) {
+ return mEventType == other.mEventType && mTimestampMillis == other.mTimestampMillis;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mEventType, mTimestampMillis);
+ }
+
+ @Override
+ public String toString() {
+ return "MediaProjectionEvent{mEventType=" + mEventType + ", mTimestampMillis="
+ + mTimestampMillis + "}";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mEventType);
+ out.writeLong(mTimestampMillis);
+ }
+
+ public static final Parcelable.Creator<MediaProjectionEvent> CREATOR =
+ new Parcelable.Creator<>() {
+ @Override
+ public MediaProjectionEvent createFromParcel(Parcel in) {
+ return new MediaProjectionEvent(in);
+ }
+
+ @Override
+ public MediaProjectionEvent[] newArray(int size) {
+ return new MediaProjectionEvent[size];
+ }
+ };
+}
diff --git a/media/java/android/media/projection/MediaProjectionManager.java b/media/java/android/media/projection/MediaProjectionManager.java
index 9cc2cca441a4..9036bf385d96 100644
--- a/media/java/android/media/projection/MediaProjectionManager.java
+++ b/media/java/android/media/projection/MediaProjectionManager.java
@@ -363,6 +363,19 @@ public final class MediaProjectionManager {
@Nullable ContentRecordingSession session
) {
}
+
+ /**
+ * Called when a specific {@link MediaProjectionEvent} occurs during the media projection
+ * session.
+ *
+ * @param event the media projection event details.
+ * @param info optional details about the media projection host.
+ * @param session optional associated recording session details.
+ */
+ public void onMediaProjectionEvent(
+ final MediaProjectionEvent event,
+ @Nullable MediaProjectionInfo info,
+ @Nullable final ContentRecordingSession session) {}
}
/** @hide */
@@ -405,5 +418,13 @@ public final class MediaProjectionManager {
) {
mHandler.post(() -> mCallback.onRecordingSessionSet(info, session));
}
+
+ @Override
+ public void onMediaProjectionEvent(
+ final MediaProjectionEvent event,
+ @Nullable MediaProjectionInfo info,
+ @Nullable final ContentRecordingSession session) {
+ mHandler.post(() -> mCallback.onMediaProjectionEvent(event, info, session));
+ }
}
}
diff --git a/media/java/android/media/quality/MediaQualityContract.java b/media/java/android/media/quality/MediaQualityContract.java
index d1f63404dbff..e558209420e0 100644
--- a/media/java/android/media/quality/MediaQualityContract.java
+++ b/media/java/android/media/quality/MediaQualityContract.java
@@ -385,6 +385,332 @@ public class MediaQualityContract {
public static final String PARAMETER_AUTO_SUPER_RESOLUTION_ENABLED =
"auto_super_resolution_enabled";
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_LEVEL_RANGE = "level_range";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_GAMUT_MAPPING = "gamut_mapping";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_PC_MODE = "pc_mode";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_LOW_LATENCY = "low_latency";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_VRR = "vrr";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_CVRR = "cvrr";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_HDMI_RGB_RANGE = "hdmi_rgb_range";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_COLOR_SPACE = "color_space";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_PANEL_INIT_MAX_LUMINCE_NITS =
+ "panel_init_max_lumince_nits";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_PANEL_INIT_MAX_LUMINCE_VALID =
+ "panel_init_max_lumince_valid";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_GAMMA = "gamma";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_COLOR_TEMPERATURE_RED_OFFSET =
+ "color_temperature_red_offset";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_COLOR_TEMPERATURE_GREEN_OFFSET =
+ "color_temperature_green_offset";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_COLOR_TEMPERATURE_BLUE_OFFSET =
+ "color_temperature_blue_offset";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_ELEVEN_POINT_RED = "eleven_point_red";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_ELEVEN_POINT_GREEN = "eleven_point_green";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_ELEVEN_POINT_BLUE = "eleven_point_blue";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_LOW_BLUE_LIGHT = "low_blue_light";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_LD_MODE = "ld_mode";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_OSD_RED_GAIN = "osd_red_gain";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_OSD_GREEN_GAIN = "osd_green_gain";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_OSD_BLUE_GAIN = "osd_blue_gain";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_OSD_RED_OFFSET = "osd_red_offset";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_OSD_GREEN_OFFSET = "osd_green_offset";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_OSD_BLUE_OFFSET = "osd_blue_offset";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_OSD_HUE = "osd_hue";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_OSD_SATURATION = "osd_saturation";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_OSD_CONTRAST = "osd_contrast";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_COLOR_TUNER_SWITCH = "color_tuner_switch";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_COLOR_TUNER_HUE_RED = "color_tuner_hue_red";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_COLOR_TUNER_HUE_GREEN = "color_tuner_hue_green";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_COLOR_TUNER_HUE_BLUE = "color_tuner_hue_blue";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_COLOR_TUNER_HUE_CYAN = "color_tuner_hue_cyan";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_COLOR_TUNER_HUE_MAGENTA = "color_tuner_hue_magenta";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_COLOR_TUNER_HUE_YELLOW = "color_tuner_hue_yellow";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_COLOR_TUNER_HUE_FLESH = "color_tuner_hue_flesh";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_COLOR_TUNER_SATURATION_RED =
+ "color_tuner_saturation_red";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_COLOR_TUNER_SATURATION_GREEN =
+ "color_tuner_saturation_green";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_COLOR_TUNER_SATURATION_BLUE =
+ "color_tuner_saturation_blue";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_COLOR_TUNER_SATURATION_CYAN =
+ "color_tuner_saturation_cyan";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_COLOR_TUNER_SATURATION_MAGENTA =
+ "color_tuner_saturation_magenta";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_COLOR_TUNER_SATURATION_YELLOW =
+ "color_tuner_saturation_yellow";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_COLOR_TUNER_SATURATION_FLESH =
+ "color_tuner_saturation_flesh";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_COLOR_TUNER_LUMINANCE_RED =
+ "color_tuner_luminance_red";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_COLOR_TUNER_LUMINANCE_GREEN =
+ "color_tuner_luminance_green";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_COLOR_TUNER_LUMINANCE_BLUE =
+ "color_tuner_luminance_blue";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_COLOR_TUNER_LUMINANCE_CYAN =
+ "color_tuner_luminance_cyan";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_COLOR_TUNER_LUMINANCE_MAGENTA =
+ "color_tuner_luminance_magenta";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_COLOR_TUNER_LUMINANCE_YELLOW =
+ "color_tuner_luminance_yellow";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_COLOR_TUNER_LUMINANCE_FLESH =
+ "color_tuner_luminance_flesh";
+
+ /**
+ * @hide
+ *
+ */
+ public static final String PARAMETER_PICTURE_QUALITY_EVENT_TYPE =
+ "picture_quality_event_type";
+
private PictureQuality() {
}
}
@@ -641,6 +967,12 @@ public class MediaQualityContract {
*/
public static final String PARAMETER_DIGITAL_OUTPUT_MODE = "digital_output_mode";
+ /**
+ * @hide
+ */
+ public static final String PARAMETER_SOUND_STYLE = "sound_style";
+
+
private SoundQuality() {
}
diff --git a/media/java/android/media/quality/MediaQualityManager.java b/media/java/android/media/quality/MediaQualityManager.java
index b7269256a449..0d6d32a22dae 100644
--- a/media/java/android/media/quality/MediaQualityManager.java
+++ b/media/java/android/media/quality/MediaQualityManager.java
@@ -51,7 +51,6 @@ import java.util.function.Consumer;
@FlaggedApi(Flags.FLAG_MEDIA_QUALITY_FW)
@SystemService(Context.MEDIA_QUALITY_SERVICE)
public final class MediaQualityManager {
- // TODO: unhide the APIs for api review
private static final String TAG = "MediaQualityManager";
private final IMediaQualityManager mService;
@@ -123,7 +122,6 @@ public final class MediaQualityManager {
public void onPictureProfileAdded(String profileId, PictureProfile profile) {
synchronized (mPpLock) {
for (PictureProfileCallbackRecord record : mPpCallbackRecords) {
- // TODO: filter callback record
record.postPictureProfileAdded(profileId, profile);
}
}
@@ -132,7 +130,6 @@ public final class MediaQualityManager {
public void onPictureProfileUpdated(String profileId, PictureProfile profile) {
synchronized (mPpLock) {
for (PictureProfileCallbackRecord record : mPpCallbackRecords) {
- // TODO: filter callback record
record.postPictureProfileUpdated(profileId, profile);
}
}
@@ -141,7 +138,6 @@ public final class MediaQualityManager {
public void onPictureProfileRemoved(String profileId, PictureProfile profile) {
synchronized (mPpLock) {
for (PictureProfileCallbackRecord record : mPpCallbackRecords) {
- // TODO: filter callback record
record.postPictureProfileRemoved(profileId, profile);
}
}
@@ -151,7 +147,6 @@ public final class MediaQualityManager {
String profileId, List<ParameterCapability> caps) {
synchronized (mPpLock) {
for (PictureProfileCallbackRecord record : mPpCallbackRecords) {
- // TODO: filter callback record
record.postParameterCapabilitiesChanged(profileId, caps);
}
}
@@ -160,7 +155,6 @@ public final class MediaQualityManager {
public void onError(String profileId, int err) {
synchronized (mPpLock) {
for (PictureProfileCallbackRecord record : mPpCallbackRecords) {
- // TODO: filter callback record
record.postError(profileId, err);
}
}
@@ -171,7 +165,6 @@ public final class MediaQualityManager {
public void onSoundProfileAdded(String profileId, SoundProfile profile) {
synchronized (mSpLock) {
for (SoundProfileCallbackRecord record : mSpCallbackRecords) {
- // TODO: filter callback record
record.postSoundProfileAdded(profileId, profile);
}
}
@@ -180,7 +173,6 @@ public final class MediaQualityManager {
public void onSoundProfileUpdated(String profileId, SoundProfile profile) {
synchronized (mSpLock) {
for (SoundProfileCallbackRecord record : mSpCallbackRecords) {
- // TODO: filter callback record
record.postSoundProfileUpdated(profileId, profile);
}
}
@@ -189,7 +181,6 @@ public final class MediaQualityManager {
public void onSoundProfileRemoved(String profileId, SoundProfile profile) {
synchronized (mSpLock) {
for (SoundProfileCallbackRecord record : mSpCallbackRecords) {
- // TODO: filter callback record
record.postSoundProfileRemoved(profileId, profile);
}
}
@@ -199,7 +190,6 @@ public final class MediaQualityManager {
String profileId, List<ParameterCapability> caps) {
synchronized (mSpLock) {
for (SoundProfileCallbackRecord record : mSpCallbackRecords) {
- // TODO: filter callback record
record.postParameterCapabilitiesChanged(profileId, caps);
}
}
@@ -208,7 +198,6 @@ public final class MediaQualityManager {
public void onError(String profileId, int err) {
synchronized (mSpLock) {
for (SoundProfileCallbackRecord record : mSpCallbackRecords) {
- // TODO: filter callback record
record.postError(profileId, err);
}
}
diff --git a/media/java/android/mtp/OWNERS b/media/java/android/mtp/OWNERS
index 77ed08b1f9a5..c57265a91eff 100644
--- a/media/java/android/mtp/OWNERS
+++ b/media/java/android/mtp/OWNERS
@@ -1,9 +1,9 @@
set noparent
-anothermark@google.com
+vmartensson@google.com
+nkapron@google.com
febinthattil@google.com
-aprasath@google.com
+shubhankarm@google.com
jsharkey@android.com
jameswei@google.com
rmojumder@google.com
-kumarashishg@google.com
diff --git a/media/jni/android_mtp_MtpDatabase.cpp b/media/jni/android_mtp_MtpDatabase.cpp
index a77bc9fe0570..a1ce495fe33d 100644
--- a/media/jni/android_mtp_MtpDatabase.cpp
+++ b/media/jni/android_mtp_MtpDatabase.cpp
@@ -910,7 +910,7 @@ MtpResponseCode MtpDatabase::getObjectInfo(MtpObjectHandle handle,
case MTP_FORMAT_TIFF:
case MTP_FORMAT_TIFF_EP:
case MTP_FORMAT_DEFINED: {
- String8 temp(path);
+ String8 temp {static_cast<std::string_view>(path)};
std::unique_ptr<FileStream> stream(new FileStream(temp));
piex::PreviewImageData image_data;
if (!GetExifFromRawImage(stream.get(), temp, image_data)) {
@@ -967,7 +967,7 @@ void* MtpDatabase::getThumbnail(MtpObjectHandle handle, size_t& outThumbSize) {
case MTP_FORMAT_TIFF:
case MTP_FORMAT_TIFF_EP:
case MTP_FORMAT_DEFINED: {
- String8 temp(path);
+ String8 temp {static_cast<std::string_view>(path)};
std::unique_ptr<FileStream> stream(new FileStream(temp));
piex::PreviewImageData image_data;
if (!GetExifFromRawImage(stream.get(), temp, image_data)) {
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioManagerTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioManagerTest.java
index e9a0d3eceba3..209734ca4a53 100644
--- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioManagerTest.java
+++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioManagerTest.java
@@ -16,6 +16,15 @@
package com.android.audiopolicytest;
+import static android.media.AudioManager.STREAM_ACCESSIBILITY;
+import static android.media.AudioManager.STREAM_ALARM;
+import static android.media.AudioManager.STREAM_DTMF;
+import static android.media.AudioManager.STREAM_MUSIC;
+import static android.media.AudioManager.STREAM_NOTIFICATION;
+import static android.media.AudioManager.STREAM_RING;
+import static android.media.AudioManager.STREAM_SYSTEM;
+import static android.media.AudioManager.STREAM_VOICE_CALL;
+
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static com.android.audiopolicytest.AudioVolumeTestUtil.DEFAULT_ATTRIBUTES;
@@ -28,11 +37,15 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
+import android.content.Context;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.AudioSystem;
+import android.media.IAudioService;
import android.media.audiopolicy.AudioProductStrategy;
import android.media.audiopolicy.AudioVolumeGroup;
+import android.os.IBinder;
+import android.os.ServiceManager;
import android.platform.test.annotations.Presubmit;
import android.util.Log;
@@ -54,6 +67,17 @@ public class AudioManagerTest {
private AudioManager mAudioManager;
+ private static final int[] PUBLIC_STREAM_TYPES = {
+ STREAM_VOICE_CALL,
+ STREAM_SYSTEM,
+ STREAM_RING,
+ STREAM_MUSIC,
+ STREAM_ALARM,
+ STREAM_NOTIFICATION,
+ STREAM_DTMF,
+ STREAM_ACCESSIBILITY,
+ };
+
@Rule
public final AudioVolumesTestRule rule = new AudioVolumesTestRule();
@@ -207,6 +231,33 @@ public class AudioManagerTest {
}
//-----------------------------------------------------------------
+ // Test getStreamVolume consistency with AudioService
+ //-----------------------------------------------------------------
+ @Test
+ public void getStreamMinMaxVolume_consistentWithAs() throws Exception {
+ IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
+ IAudioService service = IAudioService.Stub.asInterface(b);
+
+ for (int streamType : PUBLIC_STREAM_TYPES) {
+ assertEquals(service.getStreamMinVolume(streamType),
+ mAudioManager.getStreamMinVolume(streamType));
+ assertEquals(service.getStreamMaxVolume(streamType),
+ mAudioManager.getStreamMaxVolume(streamType));
+ }
+ }
+
+ @Test
+ public void getStreamVolume_consistentWithAs() throws Exception {
+ IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
+ IAudioService service = IAudioService.Stub.asInterface(b);
+
+ for (int streamType : PUBLIC_STREAM_TYPES) {
+ assertEquals(service.getStreamVolume(streamType),
+ mAudioManager.getStreamVolume(streamType));
+ }
+ }
+
+ //-----------------------------------------------------------------
// Test Volume per Attributes setter/getters
//-----------------------------------------------------------------
@Test
diff --git a/media/tests/MtpTests/OWNERS b/media/tests/MtpTests/OWNERS
index bdb6cdbea332..c57265a91eff 100644
--- a/media/tests/MtpTests/OWNERS
+++ b/media/tests/MtpTests/OWNERS
@@ -1,9 +1,9 @@
set noparent
-anothermark@google.com
+vmartensson@google.com
+nkapron@google.com
febinthattil@google.com
-aprasath@google.com
+shubhankarm@google.com
jsharkey@android.com
jameswei@google.com
rmojumder@google.com
-kumarashishg@google.com \ No newline at end of file
diff --git a/packages/PrintSpooler/src/com/android/printspooler/model/PageContentRepository.java b/packages/PrintSpooler/src/com/android/printspooler/model/PageContentRepository.java
index fe27cee7ba2d..acead8e2a0eb 100644
--- a/packages/PrintSpooler/src/com/android/printspooler/model/PageContentRepository.java
+++ b/packages/PrintSpooler/src/com/android/printspooler/model/PageContentRepository.java
@@ -510,7 +510,10 @@ public final class PageContentRepository {
protected Void doInBackground(Void... params) {
synchronized (mLock) {
try {
- if (mRenderer != null) {
+ // A page count < 0 indicates there was an error
+ // opening the document, in which case it doesn't
+ // need to be closed.
+ if (mRenderer != null && mPageCount >= 0) {
mRenderer.closeDocument();
}
} catch (RemoteException re) {
diff --git a/packages/PrintSpooler/src/com/android/printspooler/model/RemotePrintDocument.java b/packages/PrintSpooler/src/com/android/printspooler/model/RemotePrintDocument.java
index b48c55ddfef0..a9d00e9a77eb 100644
--- a/packages/PrintSpooler/src/com/android/printspooler/model/RemotePrintDocument.java
+++ b/packages/PrintSpooler/src/com/android/printspooler/model/RemotePrintDocument.java
@@ -70,6 +70,7 @@ public final class RemotePrintDocument {
private static final int STATE_CANCELING = 6;
private static final int STATE_CANCELED = 7;
private static final int STATE_DESTROYED = 8;
+ private static final int STATE_INVALID = 9;
private final Context mContext;
@@ -287,7 +288,8 @@ public final class RemotePrintDocument {
}
if (mState != STATE_STARTED && mState != STATE_UPDATED
&& mState != STATE_FAILED && mState != STATE_CANCELING
- && mState != STATE_CANCELED && mState != STATE_DESTROYED) {
+ && mState != STATE_CANCELED && mState != STATE_DESTROYED
+ && mState != STATE_INVALID) {
throw new IllegalStateException("Cannot finish in state:"
+ stateToString(mState));
}
@@ -300,6 +302,16 @@ public final class RemotePrintDocument {
}
}
+ /**
+ * Mark this document as invalid.
+ */
+ public void invalid() {
+ if (DEBUG) {
+ Log.i(LOG_TAG, "[CALLED] invalid()");
+ }
+ mState = STATE_INVALID;
+ }
+
public void cancel(boolean force) {
if (DEBUG) {
Log.i(LOG_TAG, "[CALLED] cancel(" + force + ")");
@@ -491,6 +503,9 @@ public final class RemotePrintDocument {
case STATE_DESTROYED: {
return "STATE_DESTROYED";
}
+ case STATE_INVALID: {
+ return "STATE_INVALID";
+ }
default: {
return "STATE_UNKNOWN";
}
diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java
index 4a3a6d248254..2e3234e6e622 100644
--- a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java
+++ b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java
@@ -167,6 +167,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat
private static final int STATE_PRINTER_UNAVAILABLE = 6;
private static final int STATE_UPDATE_SLOW = 7;
private static final int STATE_PRINT_COMPLETED = 8;
+ private static final int STATE_FILE_INVALID = 9;
private static final int UI_STATE_PREVIEW = 0;
private static final int UI_STATE_ERROR = 1;
@@ -404,6 +405,11 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat
public void onPause() {
PrintSpoolerService spooler = mSpoolerProvider.getSpooler();
+ if (isInvalid()) {
+ super.onPause();
+ return;
+ }
+
if (mState == STATE_INITIALIZING) {
if (isFinishing()) {
if (spooler != null) {
@@ -478,7 +484,8 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat
}
if (mState == STATE_PRINT_CANCELED || mState == STATE_PRINT_CONFIRMED
- || mState == STATE_PRINT_COMPLETED) {
+ || mState == STATE_PRINT_COMPLETED
+ || mState == STATE_FILE_INVALID) {
return true;
}
@@ -509,23 +516,32 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat
@Override
public void onMalformedPdfFile() {
onPrintDocumentError("Cannot print a malformed PDF file");
+ mPrintedDocument.invalid();
+ setState(STATE_FILE_INVALID);
}
@Override
public void onSecurePdfFile() {
onPrintDocumentError("Cannot print a password protected PDF file");
+ mPrintedDocument.invalid();
+ setState(STATE_FILE_INVALID);
}
private void onPrintDocumentError(String message) {
setState(mProgressMessageController.cancel());
- ensureErrorUiShown(null, PrintErrorFragment.ACTION_RETRY);
+ ensureErrorUiShown(
+ getString(R.string.print_cannot_load_page), PrintErrorFragment.ACTION_NONE);
setState(STATE_UPDATE_FAILED);
if (DEBUG) {
Log.i(LOG_TAG, "PrintJob state[" + PrintJobInfo.STATE_FAILED + "] reason: " + message);
}
PrintSpoolerService spooler = mSpoolerProvider.getSpooler();
- spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_FAILED, message);
+ // Use a cancel state for the spooler. This will prevent the notification from getting
+ // displayed and will remove the job. The notification (which displays the cancel and
+ // restart options) doesn't make sense for an invalid document since it will just fail
+ // again.
+ spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_CANCELED, message);
mPrintedDocument.finish();
}
@@ -995,6 +1011,9 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat
}
private void setState(int state) {
+ if (isInvalid()) {
+ return;
+ }
if (isFinalState(mState)) {
if (isFinalState(state)) {
if (DEBUG) {
@@ -1015,7 +1034,12 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat
private static boolean isFinalState(int state) {
return state == STATE_PRINT_CANCELED
|| state == STATE_PRINT_COMPLETED
- || state == STATE_CREATE_FILE_FAILED;
+ || state == STATE_CREATE_FILE_FAILED
+ || state == STATE_FILE_INVALID;
+ }
+
+ private boolean isInvalid() {
+ return mState == STATE_FILE_INVALID;
}
private void updateSelectedPagesFromPreview() {
@@ -1100,7 +1124,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat
}
private void ensurePreviewUiShown() {
- if (isFinishing() || isDestroyed()) {
+ if (isFinishing() || isDestroyed() || isInvalid()) {
return;
}
if (mUiState != UI_STATE_PREVIEW) {
@@ -1257,6 +1281,9 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat
}
private boolean updateDocument(boolean clearLastError) {
+ if (isInvalid()) {
+ return false;
+ }
if (!clearLastError && mPrintedDocument.hasUpdateError()) {
return false;
}
@@ -1676,7 +1703,8 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat
|| mState == STATE_UPDATE_FAILED
|| mState == STATE_CREATE_FILE_FAILED
|| mState == STATE_PRINTER_UNAVAILABLE
- || mState == STATE_UPDATE_SLOW) {
+ || mState == STATE_UPDATE_SLOW
+ || mState == STATE_FILE_INVALID) {
disableOptionsUi(isFinalState(mState));
return;
}
@@ -2100,6 +2128,9 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat
}
private boolean canUpdateDocument() {
+ if (isInvalid()) {
+ return false;
+ }
if (mPrintedDocument.isDestroyed()) {
return false;
}
diff --git a/packages/SettingsLib/BannerMessagePreference/Android.bp b/packages/SettingsLib/BannerMessagePreference/Android.bp
index 77e2cc735895..182daeb45d7c 100644
--- a/packages/SettingsLib/BannerMessagePreference/Android.bp
+++ b/packages/SettingsLib/BannerMessagePreference/Android.bp
@@ -28,4 +28,8 @@ android_library {
sdk_version: "system_current",
min_sdk_version: "28",
+ apex_available: [
+ "//apex_available:platform",
+ "com.android.healthfitness",
+ ],
}
diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled.xml
index f55b320269a8..ff22b2e7f86f 100644
--- a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled.xml
+++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled.xml
@@ -20,7 +20,8 @@
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
- android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:filterTouchesWhenObscured="true">
<com.google.android.material.button.MaterialButton
android:id="@+id/settingslib_button"
diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_extra.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_extra.xml
index b663b6ccc5bf..d878ba0d310b 100644
--- a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_extra.xml
+++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_extra.xml
@@ -20,7 +20,8 @@
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
- android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:filterTouchesWhenObscured="true">
<com.google.android.material.button.MaterialButton
android:id="@+id/settingslib_button"
diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_large.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_large.xml
index 784e6ad6a9f8..8f0a158c55eb 100644
--- a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_large.xml
+++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_large.xml
@@ -20,7 +20,8 @@
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
- android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:filterTouchesWhenObscured="true">
<com.google.android.material.button.MaterialButton
android:id="@+id/settingslib_button"
diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline.xml
index 8b44a6539801..0c8996063e9f 100644
--- a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline.xml
+++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline.xml
@@ -20,7 +20,8 @@
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
- android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:filterTouchesWhenObscured="true">
<com.google.android.material.button.MaterialButton
android:id="@+id/settingslib_button"
diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_extra.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_extra.xml
index f8a2d8fbd975..41d8490feeb3 100644
--- a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_extra.xml
+++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_extra.xml
@@ -20,7 +20,8 @@
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
- android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:filterTouchesWhenObscured="true">
<com.google.android.material.button.MaterialButton
android:id="@+id/settingslib_button"
diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_large.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_large.xml
index 781a5a136164..958552064c98 100644
--- a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_large.xml
+++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_large.xml
@@ -20,7 +20,8 @@
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
- android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:filterTouchesWhenObscured="true">
<com.google.android.material.button.MaterialButton
android:id="@+id/settingslib_button"
diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal.xml
index 5b568f870ea4..03ca1f0a1033 100644
--- a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal.xml
+++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal.xml
@@ -20,7 +20,8 @@
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
- android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:filterTouchesWhenObscured="true">
<com.google.android.material.button.MaterialButton
android:id="@+id/settingslib_button"
diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_extra.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_extra.xml
index 1e7a08b714f1..030ee66fef3f 100644
--- a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_extra.xml
+++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_extra.xml
@@ -20,7 +20,8 @@
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
- android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:filterTouchesWhenObscured="true">
<com.google.android.material.button.MaterialButton
android:id="@+id/settingslib_button"
diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_large.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_large.xml
index 42116be07041..5c16723f7a63 100644
--- a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_large.xml
+++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_large.xml
@@ -20,7 +20,8 @@
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
- android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:filterTouchesWhenObscured="true">
<com.google.android.material.button.MaterialButton
android:id="@+id/settingslib_button"
diff --git a/packages/SettingsLib/ButtonPreference/res/layout/settingslib_button_layout.xml b/packages/SettingsLib/ButtonPreference/res/layout/settingslib_button_layout.xml
index 1ff09901ffaf..5405045a013d 100644
--- a/packages/SettingsLib/ButtonPreference/res/layout/settingslib_button_layout.xml
+++ b/packages/SettingsLib/ButtonPreference/res/layout/settingslib_button_layout.xml
@@ -20,7 +20,8 @@
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
- android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:filterTouchesWhenObscured="true">
<Button
android:id="@+id/settingslib_button"
diff --git a/packages/SettingsLib/ButtonPreference/res/layout/settingslib_number_button.xml b/packages/SettingsLib/ButtonPreference/res/layout/settingslib_number_button.xml
index fa13b4125065..b23c5a510745 100644
--- a/packages/SettingsLib/ButtonPreference/res/layout/settingslib_number_button.xml
+++ b/packages/SettingsLib/ButtonPreference/res/layout/settingslib_number_button.xml
@@ -29,7 +29,8 @@
android:layout_height="wrap_content"
android:paddingVertical="@dimen/settingslib_expressive_space_small1"
android:paddingHorizontal="@dimen/settingslib_expressive_space_small4"
- android:background="@drawable/settingslib_number_button_background">
+ android:background="@drawable/settingslib_number_button_background"
+ android:filterTouchesWhenObscured="true">
<TextView
android:id="@+id/settingslib_number_count"
android:layout_width="wrap_content"
diff --git a/packages/SettingsLib/ButtonPreference/res/layout/settingslib_section_button.xml b/packages/SettingsLib/ButtonPreference/res/layout/settingslib_section_button.xml
index e7fb572d06dc..66a4c2e25c77 100644
--- a/packages/SettingsLib/ButtonPreference/res/layout/settingslib_section_button.xml
+++ b/packages/SettingsLib/ButtonPreference/res/layout/settingslib_section_button.xml
@@ -21,7 +21,8 @@
android:orientation="vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
- android:paddingVertical="@dimen/settingslib_expressive_space_extrasmall4">
+ android:paddingVertical="@dimen/settingslib_expressive_space_extrasmall4"
+ android:filterTouchesWhenObscured="true">
<com.google.android.material.button.MaterialButton
android:id="@+id/settingslib_section_button"
diff --git a/packages/SettingsLib/DisplayUtils/Android.bp b/packages/SettingsLib/DisplayUtils/Android.bp
index 279bb70d81bf..62630b5a9331 100644
--- a/packages/SettingsLib/DisplayUtils/Android.bp
+++ b/packages/SettingsLib/DisplayUtils/Android.bp
@@ -15,6 +15,4 @@ android_library {
],
srcs: ["src/**/*.java"],
-
- min_sdk_version: "21",
}
diff --git a/packages/SettingsLib/DisplayUtils/src/com/android/settingslib/display/DisplayDensityConfiguration.java b/packages/SettingsLib/DisplayUtils/src/com/android/settingslib/display/DisplayDensityConfiguration.java
index 284a9025de64..127e628fbd2f 100644
--- a/packages/SettingsLib/DisplayUtils/src/com/android/settingslib/display/DisplayDensityConfiguration.java
+++ b/packages/SettingsLib/DisplayUtils/src/com/android/settingslib/display/DisplayDensityConfiguration.java
@@ -16,13 +16,20 @@
package com.android.settingslib.display;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.hardware.display.DisplayManager;
import android.os.AsyncTask;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;
+import android.view.Display;
+import android.view.DisplayInfo;
import android.view.IWindowManager;
import android.view.WindowManagerGlobal;
+import java.util.function.Predicate;
+
/** Utility methods for controlling the display density. */
public class DisplayDensityConfiguration {
private static final String LOG_TAG = "DisplayDensityConfig";
@@ -85,4 +92,42 @@ public class DisplayDensityConfiguration {
}
});
}
+
+ /**
+ * Asynchronously applies display density changes to all displays that satisfy the predicate.
+ *
+ * <p>The change will be applied to the user specified by the value of
+ * {@link UserHandle#myUserId()} at the time the method is called.
+ *
+ * @param context The context
+ * @param predicate Determines which displays to set the density to
+ * @param density The density to force
+ */
+ public static void setForcedDisplayDensity(@NonNull Context context,
+ @NonNull Predicate<DisplayInfo> predicate, final int density) {
+ final int userId = UserHandle.myUserId();
+ DisplayManager dm = context.getSystemService(DisplayManager.class);
+ AsyncTask.execute(() -> {
+ try {
+ for (Display display : dm.getDisplays(
+ DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED)) {
+ int displayId = display.getDisplayId();
+ DisplayInfo info = new DisplayInfo();
+ if (!display.getDisplayInfo(info)) {
+ Log.w(LOG_TAG, "Unable to save forced display density setting "
+ + "for display " + displayId);
+ continue;
+ }
+ if (!predicate.test(info)) {
+ continue;
+ }
+
+ final IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
+ wm.setForcedDisplayDensityForUser(displayId, density, userId);
+ }
+ } catch (RemoteException exc) {
+ Log.w(LOG_TAG, "Unable to save forced display density setting");
+ }
+ });
+ }
}
diff --git a/packages/SettingsLib/Graph/graph.proto b/packages/SettingsLib/Graph/graph.proto
index 33a7df4c6ba8..a834947144a0 100644
--- a/packages/SettingsLib/Graph/graph.proto
+++ b/packages/SettingsLib/Graph/graph.proto
@@ -26,6 +26,14 @@ message PreferenceScreenProto {
optional PreferenceGroupProto root = 2;
// If the preference screen provides complete hierarchy by source code.
optional bool complete_hierarchy = 3;
+ // Parameterized screens (not recursive, provided on the top level only)
+ repeated ParameterizedPreferenceScreenProto parameterized_screens = 4;
+}
+
+// Proto of parameterized preference screen
+message ParameterizedPreferenceScreenProto {
+ optional BundleProto args = 1;
+ optional PreferenceScreenProto screen = 2;
}
// Proto of PreferenceGroup.
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt
index adffd206d552..27ce1c7246e6 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt
@@ -18,12 +18,14 @@ package com.android.settingslib.graph
import android.app.Application
import android.os.Bundle
+import android.os.Parcelable
import android.os.SystemClock
import com.android.settingslib.graph.proto.PreferenceGraphProto
import com.android.settingslib.ipc.ApiHandler
import com.android.settingslib.ipc.ApiPermissionChecker
import com.android.settingslib.ipc.MessageCodec
import com.android.settingslib.metadata.PreferenceRemoteOpMetricsLogger
+import com.android.settingslib.metadata.PreferenceScreenCoordinate
import com.android.settingslib.metadata.PreferenceScreenRegistry
import com.android.settingslib.preference.PreferenceScreenProvider
import java.util.Locale
@@ -59,10 +61,9 @@ class GetPreferenceGraphApiHandler(
var success = false
try {
val builder = PreferenceGraphBuilder.of(application, callingPid, callingUid, request)
- if (request.screenKeys.isEmpty()) {
- PreferenceScreenRegistry.preferenceScreenMetadataFactories.forEachKeyAsync {
- builder.addPreferenceScreenFromRegistry(it)
- }
+ if (request.screens.isEmpty()) {
+ val factories = PreferenceScreenRegistry.preferenceScreenMetadataFactories
+ factories.forEachAsync { _, factory -> builder.addPreferenceScreen(factory) }
for (provider in preferenceScreenProviders) {
builder.addPreferenceScreenProvider(provider)
}
@@ -84,15 +85,15 @@ class GetPreferenceGraphApiHandler(
/**
* Request of [GetPreferenceGraphApiHandler].
*
- * @param screenKeys screen keys of the preference graph
- * @param visitedScreens keys of the visited preference screen
+ * @param screens screens of the preference graph
+ * @param visitedScreens visited preference screens
* @param locale locale of the preference graph
*/
data class GetPreferenceGraphRequest
@JvmOverloads
constructor(
- val screenKeys: Set<String> = setOf(),
- val visitedScreens: Set<String> = setOf(),
+ val screens: Set<PreferenceScreenCoordinate> = setOf(),
+ val visitedScreens: Set<PreferenceScreenCoordinate> = setOf(),
val locale: Locale? = null,
val flags: Int = PreferenceGetterFlags.ALL,
val includeValueDescriptor: Boolean = true,
@@ -101,26 +102,32 @@ constructor(
object GetPreferenceGraphRequestCodec : MessageCodec<GetPreferenceGraphRequest> {
override fun encode(data: GetPreferenceGraphRequest): Bundle =
Bundle(4).apply {
- putStringArray(KEY_SCREEN_KEYS, data.screenKeys.toTypedArray())
- putStringArray(KEY_VISITED_KEYS, data.visitedScreens.toTypedArray())
+ putParcelableArray(KEY_SCREENS, data.screens.toTypedArray())
+ putParcelableArray(KEY_VISITED_SCREENS, data.visitedScreens.toTypedArray())
putString(KEY_LOCALE, data.locale?.toLanguageTag())
putInt(KEY_FLAGS, data.flags)
}
+ @Suppress("DEPRECATION")
override fun decode(data: Bundle): GetPreferenceGraphRequest {
- val screenKeys = data.getStringArray(KEY_SCREEN_KEYS) ?: arrayOf()
- val visitedScreens = data.getStringArray(KEY_VISITED_KEYS) ?: arrayOf()
+ data.classLoader = PreferenceScreenCoordinate::class.java.classLoader
+ val screens = data.getParcelableArray(KEY_SCREENS) ?: arrayOf()
+ val visitedScreens = data.getParcelableArray(KEY_VISITED_SCREENS) ?: arrayOf()
fun String?.toLocale() = if (this != null) Locale.forLanguageTag(this) else null
+ fun Array<Parcelable>.toScreenCoordinates() =
+ buildSet(size) {
+ for (element in this@toScreenCoordinates) add(element as PreferenceScreenCoordinate)
+ }
return GetPreferenceGraphRequest(
- screenKeys.toSet(),
- visitedScreens.toSet(),
+ screens.toScreenCoordinates(),
+ visitedScreens.toScreenCoordinates(),
data.getString(KEY_LOCALE).toLocale(),
data.getInt(KEY_FLAGS),
)
}
- private const val KEY_SCREEN_KEYS = "k"
- private const val KEY_VISITED_KEYS = "v"
+ private const val KEY_SCREENS = "s"
+ private const val KEY_VISITED_SCREENS = "v"
private const val KEY_LOCALE = "l"
private const val KEY_FLAGS = "f"
}
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt
index a9958b975fc6..1d4e2c9e1bef 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt
@@ -26,6 +26,7 @@ import com.android.settingslib.ipc.ApiPermissionChecker
import com.android.settingslib.metadata.PreferenceCoordinate
import com.android.settingslib.metadata.PreferenceHierarchyNode
import com.android.settingslib.metadata.PreferenceRemoteOpMetricsLogger
+import com.android.settingslib.metadata.PreferenceScreenCoordinate
import com.android.settingslib.metadata.PreferenceScreenRegistry
/**
@@ -105,8 +106,10 @@ class PreferenceGetterApiHandler(
val errors = mutableMapOf<PreferenceCoordinate, Int>()
val preferences = mutableMapOf<PreferenceCoordinate, PreferenceProto>()
val flags = request.flags
- for ((screenKey, coordinates) in request.preferences.groupBy { it.screenKey }) {
- val screenMetadata = PreferenceScreenRegistry.create(application, screenKey)
+ val groups =
+ request.preferences.groupBy { PreferenceScreenCoordinate(it.screenKey, it.args) }
+ for ((screen, coordinates) in groups) {
+ val screenMetadata = PreferenceScreenRegistry.create(application, screen)
if (screenMetadata == null) {
val latencyMs = SystemClock.elapsedRealtime() - elapsedRealtime
for (coordinate in coordinates) {
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt
index c0d244989044..4290437b0d02 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt
@@ -40,6 +40,7 @@ import com.android.settingslib.graph.proto.PreferenceProto
import com.android.settingslib.graph.proto.PreferenceProto.ActionTarget
import com.android.settingslib.graph.proto.PreferenceScreenProto
import com.android.settingslib.graph.proto.TextProto
+import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_ARGS
import com.android.settingslib.metadata.IntRangeValuePreference
import com.android.settingslib.metadata.PersistentPreference
import com.android.settingslib.metadata.PreferenceAvailabilityProvider
@@ -47,7 +48,10 @@ import com.android.settingslib.metadata.PreferenceHierarchy
import com.android.settingslib.metadata.PreferenceMetadata
import com.android.settingslib.metadata.PreferenceRestrictionProvider
import com.android.settingslib.metadata.PreferenceScreenBindingKeyProvider
+import com.android.settingslib.metadata.PreferenceScreenCoordinate
import com.android.settingslib.metadata.PreferenceScreenMetadata
+import com.android.settingslib.metadata.PreferenceScreenMetadataFactory
+import com.android.settingslib.metadata.PreferenceScreenMetadataParameterizedFactory
import com.android.settingslib.metadata.PreferenceScreenRegistry
import com.android.settingslib.metadata.PreferenceSummaryProvider
import com.android.settingslib.metadata.PreferenceTitleProvider
@@ -72,15 +76,19 @@ private constructor(
PreferenceScreenFactory(context.ofLocale(request.locale))
}
private val builder by lazy { PreferenceGraphProto.newBuilder() }
- private val visitedScreens = mutableSetOf<String>().apply { addAll(request.visitedScreens) }
+ private val visitedScreens = request.visitedScreens.toMutableSet()
+ private val screens = mutableMapOf<String, PreferenceScreenProto.Builder>()
private suspend fun init() {
- for (key in request.screenKeys) {
- addPreferenceScreenFromRegistry(key)
+ for (screen in request.screens) {
+ PreferenceScreenRegistry.create(context, screen)?.let { addPreferenceScreen(it) }
}
}
- fun build(): PreferenceGraphProto = builder.build()
+ fun build(): PreferenceGraphProto {
+ for ((key, screenBuilder) in screens) builder.putScreens(key, screenBuilder.build())
+ return builder.build()
+ }
/**
* Adds an activity to the graph.
@@ -138,19 +146,12 @@ private constructor(
null
}
- suspend fun addPreferenceScreenFromRegistry(key: String): Boolean {
- val metadata = PreferenceScreenRegistry.create(context, key) ?: return false
- return addPreferenceScreenMetadata(metadata)
+ private suspend fun addPreferenceScreenFromRegistry(key: String): Boolean {
+ val factory =
+ PreferenceScreenRegistry.preferenceScreenMetadataFactories[key] ?: return false
+ return addPreferenceScreen(factory)
}
- private suspend fun addPreferenceScreenMetadata(metadata: PreferenceScreenMetadata): Boolean =
- addPreferenceScreen(metadata.key) {
- preferenceScreenProto {
- completeHierarchy = metadata.hasCompleteHierarchy()
- root = metadata.getPreferenceHierarchy(context).toProto(metadata, true)
- }
- }
-
suspend fun addPreferenceScreenProvider(activityClass: Class<*>) {
Log.d(TAG, "add $activityClass")
createPreferenceScreen { activityClass.newInstance() }
@@ -188,26 +189,52 @@ private constructor(
Log.e(TAG, "\"$preferenceScreen\" has no key")
return
}
- @Suppress("CheckReturnValue") addPreferenceScreen(key) { preferenceScreen.toProto(intent) }
+ val args = preferenceScreen.peekExtras()?.getBundle(EXTRA_BINDING_SCREEN_ARGS)
+ @Suppress("CheckReturnValue")
+ addPreferenceScreen(key, args) {
+ this.intent = intent.toProto()
+ root = preferenceScreen.toProto()
+ }
+ }
+
+ suspend fun addPreferenceScreen(factory: PreferenceScreenMetadataFactory): Boolean {
+ if (factory is PreferenceScreenMetadataParameterizedFactory) {
+ factory.parameters(context).collect { addPreferenceScreen(factory.create(context, it)) }
+ return true
+ }
+ return addPreferenceScreen(factory.create(context))
}
+ private suspend fun addPreferenceScreen(metadata: PreferenceScreenMetadata): Boolean =
+ addPreferenceScreen(metadata.key, metadata.arguments) {
+ completeHierarchy = metadata.hasCompleteHierarchy()
+ root = metadata.getPreferenceHierarchy(context).toProto(metadata, true)
+ }
+
private suspend fun addPreferenceScreen(
key: String,
- preferenceScreenProvider: suspend () -> PreferenceScreenProto,
- ): Boolean =
- if (visitedScreens.add(key)) {
- builder.putScreens(key, preferenceScreenProvider())
- true
- } else {
- Log.w(TAG, "$key visited")
- false
+ args: Bundle?,
+ init: suspend PreferenceScreenProto.Builder.() -> Unit,
+ ): Boolean {
+ if (!visitedScreens.add(PreferenceScreenCoordinate(key, args))) {
+ Log.w(TAG, "$key $args visited")
+ return false
}
-
- private suspend fun PreferenceScreen.toProto(intent: Intent?): PreferenceScreenProto =
- preferenceScreenProto {
- intent?.let { this.intent = it.toProto() }
- root = (this@toProto as PreferenceGroup).toProto()
+ if (args == null) { // normal screen
+ screens[key] = PreferenceScreenProto.newBuilder().also { init(it) }
+ } else if (args.isEmpty) { // parameterized screen with backward compatibility
+ val builder = screens.getOrPut(key) { PreferenceScreenProto.newBuilder() }
+ init(builder)
+ } else { // parameterized screen with non-empty arguments
+ val builder = screens.getOrPut(key) { PreferenceScreenProto.newBuilder() }
+ val parameterizedScreen = parameterizedPreferenceScreenProto {
+ setArgs(args.toProto())
+ setScreen(PreferenceScreenProto.newBuilder().also { init(it) })
+ }
+ builder.addParameterizedScreens(parameterizedScreen)
}
+ return true
+ }
private suspend fun PreferenceGroup.toProto(): PreferenceGroupProto = preferenceGroupProto {
preference = (this@toProto as Preference).toProto()
@@ -271,7 +298,7 @@ private constructor(
.toProto(context, callingPid, callingUid, screenMetadata, isRoot, request.flags)
.also {
if (metadata is PreferenceScreenMetadata) {
- @Suppress("CheckReturnValue") addPreferenceScreenMetadata(metadata)
+ @Suppress("CheckReturnValue") addPreferenceScreen(metadata)
}
metadata.intent(context)?.resolveActivity(context.packageManager)?.let {
if (it.packageName == context.packageName) {
@@ -322,7 +349,7 @@ private constructor(
val screenKey = screen?.key
if (!screenKey.isNullOrEmpty()) {
@Suppress("CheckReturnValue")
- addPreferenceScreen(screenKey) { screen.toProto(null) }
+ addPreferenceScreen(screenKey, null) { root = screen.toProto() }
return actionTargetProto { key = screenKey }
}
} catch (e: Exception) {
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt
index a595f42a573d..60f9c6bb92a3 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt
@@ -40,11 +40,12 @@ import com.android.settingslib.metadata.SensitivityLevel.Companion.HIGH_SENSITIV
import com.android.settingslib.metadata.SensitivityLevel.Companion.UNKNOWN_SENSITIVITY
/** Request to set preference value. */
-data class PreferenceSetterRequest(
- val screenKey: String,
- val key: String,
+class PreferenceSetterRequest(
+ screenKey: String,
+ args: Bundle?,
+ key: String,
val value: PreferenceValueProto,
-)
+) : PreferenceCoordinate(screenKey, args, key)
/** Result of preference setter request. */
@IntDef(
@@ -121,7 +122,7 @@ class PreferenceSetterApiHandler(
metricsLogger?.logSetterApi(
application,
callingUid,
- PreferenceCoordinate(request.screenKey, request.key),
+ request,
null,
null,
PreferenceSetterResult.UNSUPPORTED,
@@ -130,7 +131,7 @@ class PreferenceSetterApiHandler(
return PreferenceSetterResult.UNSUPPORTED
}
val screenMetadata =
- PreferenceScreenRegistry.create(application, request.screenKey) ?: return notFound()
+ PreferenceScreenRegistry.create(application, request) ?: return notFound()
val key = request.key
val metadata =
screenMetadata.getPreferenceHierarchy(application).find(key) ?: return notFound()
@@ -199,7 +200,7 @@ class PreferenceSetterApiHandler(
metricsLogger?.logSetterApi(
application,
callingUid,
- PreferenceCoordinate(request.screenKey, request.key),
+ request,
screenMetadata,
metadata,
result,
@@ -235,6 +236,7 @@ object PreferenceSetterRequestCodec : MessageCodec<PreferenceSetterRequest> {
override fun encode(data: PreferenceSetterRequest) =
Bundle(3).apply {
putString(SCREEN_KEY, data.screenKey)
+ putBundle(ARGS, data.args)
putString(KEY, data.key)
putByteArray(null, data.value.toByteArray())
}
@@ -242,10 +244,12 @@ object PreferenceSetterRequestCodec : MessageCodec<PreferenceSetterRequest> {
override fun decode(data: Bundle) =
PreferenceSetterRequest(
data.getString(SCREEN_KEY)!!,
+ data.getBundle(ARGS),
data.getString(KEY)!!,
PreferenceValueProto.parseFrom(data.getByteArray(null)!!),
)
private const val SCREEN_KEY = "s"
private const val KEY = "k"
+ private const val ARGS = "a"
}
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoDsl.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoDsl.kt
index adbe77318353..5f2a0d826407 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoDsl.kt
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoDsl.kt
@@ -19,6 +19,7 @@ package com.android.settingslib.graph
import com.android.settingslib.graph.proto.BundleProto
import com.android.settingslib.graph.proto.BundleProto.BundleValue
import com.android.settingslib.graph.proto.IntentProto
+import com.android.settingslib.graph.proto.ParameterizedPreferenceScreenProto
import com.android.settingslib.graph.proto.PreferenceGroupProto
import com.android.settingslib.graph.proto.PreferenceOrGroupProto
import com.android.settingslib.graph.proto.PreferenceProto
@@ -39,6 +40,12 @@ inline fun preferenceScreenProto(
init: PreferenceScreenProto.Builder.() -> Unit
): PreferenceScreenProto = PreferenceScreenProto.newBuilder().also(init).build()
+/** Kotlin DSL-style builder for [PreferenceScreenProto]. */
+inline fun parameterizedPreferenceScreenProto(
+ init: ParameterizedPreferenceScreenProto.Builder.() -> Unit
+): ParameterizedPreferenceScreenProto =
+ ParameterizedPreferenceScreenProto.newBuilder().also(init).build()
+
/** Returns preference or null. */
val PreferenceOrGroupProto.preferenceOrNull
get() = if (hasPreference()) preference else null
diff --git a/packages/SettingsLib/IntroPreference/res/layout/settingslib_expressive_preference_intro.xml b/packages/SettingsLib/IntroPreference/res/layout/settingslib_expressive_preference_intro.xml
index 43cf6aa09109..7adcbf6c6601 100644
--- a/packages/SettingsLib/IntroPreference/res/layout/settingslib_expressive_preference_intro.xml
+++ b/packages/SettingsLib/IntroPreference/res/layout/settingslib_expressive_preference_intro.xml
@@ -18,6 +18,8 @@
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/entity_header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
style="@style/SettingsLibEntityHeader">
<LinearLayout
diff --git a/packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt b/packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt
index 38b641336547..69b75adea9d3 100644
--- a/packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt
+++ b/packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt
@@ -33,6 +33,9 @@ import javax.tools.Diagnostic
/** Processor to gather preference screens annotated with `@ProvidePreferenceScreen`. */
class PreferenceScreenAnnotationProcessor : AbstractProcessor() {
private val screens = mutableListOf<Screen>()
+ private val bundleType: TypeMirror by lazy {
+ processingEnv.elementUtils.getTypeElement("android.os.Bundle").asType()
+ }
private val contextType: TypeMirror by lazy {
processingEnv.elementUtils.getTypeElement("android.content.Context").asType()
}
@@ -83,19 +86,57 @@ class PreferenceScreenAnnotationProcessor : AbstractProcessor() {
error("@$ANNOTATION_NAME must be added to $PREFERENCE_SCREEN_METADATA subclass", this)
return
}
- val constructorType = getConstructorType()
- if (constructorType == null) {
+ fun reportConstructorError() =
error(
- "Class must be an object, or has single public constructor that " +
- "accepts no parameter or a Context parameter",
+ "Must have only one public constructor: constructor(), " +
+ "constructor(Context), constructor(Bundle) or constructor(Context, Bundle)",
this,
)
+ val constructor = findConstructor()
+ if (constructor == null || constructor.parameters.size > 2) {
+ reportConstructorError()
return
}
+ val constructorHasContextParameter = constructor.hasParameter(0, contextType)
+ var index = if (constructorHasContextParameter) 1 else 0
val annotation = annotationMirrors.single { it.isElement(annotationElement) }
val key = annotation.fieldValue<String>("value")!!
val overlay = annotation.fieldValue<Boolean>("overlay") == true
- screens.add(Screen(key, overlay, qualifiedName.toString(), constructorType))
+ val parameterized = annotation.fieldValue<Boolean>("parameterized") == true
+ var parametersHasContextParameter = false
+ if (parameterized) {
+ val parameters = findParameters()
+ if (parameters == null) {
+ error("require a static 'parameters()' or 'parameters(Context)' method", this)
+ return
+ }
+ parametersHasContextParameter = parameters
+ if (constructor.hasParameter(index, bundleType)) {
+ index++
+ } else {
+ error(
+ "Parameterized screen constructor must be" +
+ "constructor(Bundle) or constructor(Context, Bundle)",
+ this,
+ )
+ return
+ }
+ }
+ if (index == constructor.parameters.size) {
+ screens.add(
+ Screen(
+ key,
+ overlay,
+ parameterized,
+ annotation.fieldValue<Boolean>("parameterizedMigration") == true,
+ qualifiedName.toString(),
+ constructorHasContextParameter,
+ parametersHasContextParameter,
+ )
+ )
+ } else {
+ reportConstructorError()
+ }
}
private fun codegen() {
@@ -116,10 +157,15 @@ class PreferenceScreenAnnotationProcessor : AbstractProcessor() {
screens.sort()
processingEnv.filer.createSourceFile("$outputPkg.$outputClass").openWriter().use {
it.write("package $outputPkg;\n\n")
+ it.write("import android.content.Context;\n")
+ it.write("import android.os.Bundle;\n")
it.write("import $PACKAGE.FixedArrayMap;\n")
it.write("import $PACKAGE.FixedArrayMap.OrderedInitializer;\n")
- it.write("import $PACKAGE.$FACTORY;\n\n")
- it.write("// Generated by annotation processor for @$ANNOTATION_NAME\n")
+ it.write("import $PACKAGE.$PREFERENCE_SCREEN_METADATA;\n")
+ it.write("import $PACKAGE.$FACTORY;\n")
+ it.write("import $PACKAGE.$PARAMETERIZED_FACTORY;\n")
+ it.write("import kotlinx.coroutines.flow.Flow;\n")
+ it.write("\n// Generated by annotation processor for @$ANNOTATION_NAME\n")
it.write("public final class $outputClass {\n")
it.write(" private $outputClass() {}\n\n")
it.write(" public static FixedArrayMap<String, $FACTORY> $outputFun() {\n")
@@ -127,10 +173,29 @@ class PreferenceScreenAnnotationProcessor : AbstractProcessor() {
it.write(" return new FixedArrayMap<>($size, $outputClass::init);\n")
it.write(" }\n\n")
fun Screen.write() {
- it.write(" screens.put(\"$key\", context -> new $klass(")
- when (constructorType) {
- ConstructorType.DEFAULT -> it.write("));")
- ConstructorType.CONTEXT -> it.write("context));")
+ it.write(" screens.put(\"$key\", ")
+ if (parameterized) {
+ it.write("new $PARAMETERIZED_FACTORY() {\n")
+ it.write(" @Override public PreferenceScreenMetadata create")
+ it.write("(Context context, Bundle args) {\n")
+ it.write(" return new $klass(")
+ if (constructorHasContextParameter) it.write("context, ")
+ it.write("args);\n")
+ it.write(" }\n\n")
+ it.write(" @Override public Flow<Bundle> parameters(Context context) {\n")
+ it.write(" return $klass.parameters(")
+ if (parametersHasContextParameter) it.write("context")
+ it.write(");\n")
+ it.write(" }\n")
+ if (parameterizedMigration) {
+ it.write("\n @Override public boolean acceptEmptyArguments()")
+ it.write(" { return true; }\n")
+ }
+ it.write(" });")
+ } else {
+ it.write("context -> new $klass(")
+ if (constructorHasContextParameter) it.write("context")
+ it.write("));")
}
if (overlay) it.write(" // overlay")
it.write("\n")
@@ -159,7 +224,7 @@ class PreferenceScreenAnnotationProcessor : AbstractProcessor() {
}
private fun AnnotationMirror.isElement(element: TypeElement) =
- processingEnv.typeUtils.isSameType(annotationType.asElement().asType(), element.asType())
+ annotationType.asElement().asType().isSameType(element.asType())
@Suppress("UNCHECKED_CAST")
private fun <T> AnnotationMirror.fieldValue(name: String): T? = field(name)?.value as? T
@@ -171,7 +236,7 @@ class PreferenceScreenAnnotationProcessor : AbstractProcessor() {
return null
}
- private fun TypeElement.getConstructorType(): ConstructorType? {
+ private fun TypeElement.findConstructor(): ExecutableElement? {
var constructor: ExecutableElement? = null
for (element in enclosedElements) {
if (element.kind != ElementKind.CONSTRUCTOR) continue
@@ -179,16 +244,30 @@ class PreferenceScreenAnnotationProcessor : AbstractProcessor() {
if (constructor != null) return null
constructor = element as ExecutableElement
}
- return constructor?.parameters?.run {
- when {
- isEmpty() -> ConstructorType.DEFAULT
- size == 1 && processingEnv.typeUtils.isSameType(this[0].asType(), contextType) ->
- ConstructorType.CONTEXT
- else -> null
- }
+ return constructor
+ }
+
+ private fun TypeElement.findParameters(): Boolean? {
+ for (element in enclosedElements) {
+ if (element.kind != ElementKind.METHOD) continue
+ if (!element.modifiers.contains(Modifier.PUBLIC)) continue
+ if (!element.modifiers.contains(Modifier.STATIC)) continue
+ if (!element.simpleName.contentEquals("parameters")) return null
+ val parameters = (element as ExecutableElement).parameters
+ if (parameters.isEmpty()) return false
+ if (parameters.size == 1 && parameters[0].asType().isSameType(contextType)) return true
+ error("parameters method should have no parameter or a Context parameter", element)
+ return null
}
+ return null
}
+ private fun ExecutableElement.hasParameter(index: Int, typeMirror: TypeMirror) =
+ index < parameters.size && parameters[index].asType().isSameType(typeMirror)
+
+ private fun TypeMirror.isSameType(typeMirror: TypeMirror) =
+ processingEnv.typeUtils.isSameType(this, typeMirror)
+
private fun warn(msg: CharSequence) =
processingEnv.messager.printMessage(Diagnostic.Kind.WARNING, msg)
@@ -198,8 +277,11 @@ class PreferenceScreenAnnotationProcessor : AbstractProcessor() {
private data class Screen(
val key: String,
val overlay: Boolean,
+ val parameterized: Boolean,
+ val parameterizedMigration: Boolean,
val klass: String,
- val constructorType: ConstructorType,
+ val constructorHasContextParameter: Boolean,
+ val parametersHasContextParameter: Boolean,
) : Comparable<Screen> {
override fun compareTo(other: Screen): Int {
val diff = key.compareTo(other.key)
@@ -207,17 +289,13 @@ class PreferenceScreenAnnotationProcessor : AbstractProcessor() {
}
}
- private enum class ConstructorType {
- DEFAULT, // default constructor with no parameter
- CONTEXT, // constructor with a Context parameter
- }
-
companion object {
private const val PACKAGE = "com.android.settingslib.metadata"
private const val ANNOTATION_NAME = "ProvidePreferenceScreen"
private const val ANNOTATION = "$PACKAGE.$ANNOTATION_NAME"
private const val PREFERENCE_SCREEN_METADATA = "PreferenceScreenMetadata"
private const val FACTORY = "PreferenceScreenMetadataFactory"
+ private const val PARAMETERIZED_FACTORY = "PreferenceScreenMetadataParameterizedFactory"
private const val OPTIONS_NAME = "ProvidePreferenceScreenOptions"
private const val OPTIONS = "$PACKAGE.$OPTIONS_NAME"
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Annotations.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Annotations.kt
index 4bed795ea760..449c78ce8965 100644
--- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Annotations.kt
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Annotations.kt
@@ -22,14 +22,27 @@ package com.android.settingslib.metadata
* The annotated class must satisfy either condition:
* - the primary constructor has no parameter
* - the primary constructor has a single [android.content.Context] parameter
+ * - (parameterized) the primary constructor has a single [android.os.Bundle] parameter to override
+ * [PreferenceScreenMetadata.arguments]
+ * - (parameterized) the primary constructor has a [android.content.Context] and a
+ * [android.os.Bundle] parameter to override [PreferenceScreenMetadata.arguments]
*
* @param value unique preference screen key
* @param overlay if true, current annotated screen will overlay the screen that has identical key
+ * @param parameterized if true, the screen relies on additional arguments to build its content
+ * @param parameterizedMigration whether the parameterized screen was a normal screen, in which case
+ * `Bundle.EMPTY` will be passed as arguments to take care of backward compatibility
+ * @see PreferenceScreenMetadata
*/
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.CLASS)
@MustBeDocumented
-annotation class ProvidePreferenceScreen(val value: String, val overlay: Boolean = false)
+annotation class ProvidePreferenceScreen(
+ val value: String,
+ val overlay: Boolean = false,
+ val parameterized: Boolean = false,
+ val parameterizedMigration: Boolean = false, // effective only when parameterized is true
+)
/**
* Provides options for [ProvidePreferenceScreen] annotation processor.
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Bundles.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Bundles.kt
new file mode 100644
index 000000000000..a63576510aec
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Bundles.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.metadata
+
+import android.content.Intent
+import android.os.Bundle
+
+@Suppress("DEPRECATION")
+fun Bundle?.contentEquals(other: Bundle?): Boolean {
+ if (this == null) return other == null
+ if (other == null) return false
+ if (keySet() != other.keySet()) return false
+ fun Any?.valueEquals(other: Any?) =
+ when (this) {
+ is Bundle -> other is Bundle && this.contentEquals(other)
+ is Intent -> other is Intent && this.filterEquals(other)
+ is BooleanArray -> other is BooleanArray && this contentEquals other
+ is ByteArray -> other is ByteArray && this contentEquals other
+ is CharArray -> other is CharArray && this contentEquals other
+ is DoubleArray -> other is DoubleArray && this contentEquals other
+ is FloatArray -> other is FloatArray && this contentEquals other
+ is IntArray -> other is IntArray && this contentEquals other
+ is LongArray -> other is LongArray && this contentEquals other
+ is ShortArray -> other is ShortArray && this contentEquals other
+ is Array<*> -> other is Array<*> && this contentDeepEquals other
+ else -> this == other
+ }
+ for (key in keySet()) {
+ if (!get(key).valueEquals(other.get(key))) return false
+ }
+ return true
+}
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt
index 63f1050df94e..e456a7f1aa1c 100644
--- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt
@@ -78,13 +78,8 @@ annotation class SensitivityLevel {
/** Preference metadata that has a value persisted in datastore. */
interface PersistentPreference<T> : PreferenceMetadata {
- /**
- * The value type the preference is associated with.
- *
- * TODO(b/388167302): Remove the default implementation once all subclasses are migrated.
- */
- val valueType: Class<T>?
- get() = null
+ /** The value type the preference is associated with. */
+ val valueType: Class<T>
/**
* Returns the key-value storage of the preference.
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceCoordinate.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceCoordinate.kt
index 2dd736ae6083..ac08847b6002 100644
--- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceCoordinate.kt
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceCoordinate.kt
@@ -16,26 +16,41 @@
package com.android.settingslib.metadata
+import android.os.Bundle
import android.os.Parcel
import android.os.Parcelable
/**
* Coordinate to locate a preference.
*
- * Within an app, the preference screen key (unique among screens) plus preference key (unique on
- * the screen) is used to locate a preference.
+ * Within an app, the preference screen coordinate (unique among screens) plus preference key
+ * (unique on the screen) is used to locate a preference.
*/
-data class PreferenceCoordinate(val screenKey: String, val key: String) : Parcelable {
+open class PreferenceCoordinate : PreferenceScreenCoordinate {
+ val key: String
- constructor(parcel: Parcel) : this(parcel.readString()!!, parcel.readString()!!)
+ constructor(screenKey: String, key: String) : this(screenKey, null, key)
+
+ constructor(screenKey: String, args: Bundle?, key: String) : super(screenKey, args) {
+ this.key = key
+ }
+
+ constructor(parcel: Parcel) : super(parcel) {
+ this.key = parcel.readString()!!
+ }
override fun writeToParcel(parcel: Parcel, flags: Int) {
- parcel.writeString(screenKey)
+ super.writeToParcel(parcel, flags)
parcel.writeString(key)
}
override fun describeContents() = 0
+ override fun equals(other: Any?) =
+ super.equals(other) && key == (other as PreferenceCoordinate).key
+
+ override fun hashCode() = super.hashCode() xor key.hashCode()
+
companion object CREATOR : Parcelable.Creator<PreferenceCoordinate> {
override fun createFromParcel(parcel: Parcel) = PreferenceCoordinate(parcel)
@@ -43,3 +58,46 @@ data class PreferenceCoordinate(val screenKey: String, val key: String) : Parcel
override fun newArray(size: Int) = arrayOfNulls<PreferenceCoordinate>(size)
}
}
+
+/** Coordinate to locate a preference screen. */
+open class PreferenceScreenCoordinate : Parcelable {
+ /** Unique preference screen key. */
+ val screenKey: String
+
+ /** Arguments to create parameterized preference screen. */
+ val args: Bundle?
+
+ constructor(screenKey: String, args: Bundle?) {
+ this.screenKey = screenKey
+ this.args = args
+ }
+
+ constructor(parcel: Parcel) {
+ screenKey = parcel.readString()!!
+ args = parcel.readBundle(javaClass.classLoader)
+ }
+
+ override fun writeToParcel(parcel: Parcel, flags: Int) {
+ parcel.writeString(screenKey)
+ parcel.writeBundle(args)
+ }
+
+ override fun describeContents() = 0
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+ other as PreferenceScreenCoordinate
+ return screenKey == other.screenKey && args.contentEquals(other.args)
+ }
+
+ // "args" is not included intentionally, otherwise we need to take care of array, etc.
+ override fun hashCode() = screenKey.hashCode()
+
+ companion object CREATOR : Parcelable.Creator<PreferenceScreenCoordinate> {
+
+ override fun createFromParcel(parcel: Parcel) = PreferenceScreenCoordinate(parcel)
+
+ override fun newArray(size: Int) = arrayOfNulls<PreferenceScreenCoordinate>(size)
+ }
+}
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt
index 876f6152cccd..3bd051dee41d 100644
--- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt
@@ -17,6 +17,7 @@
package com.android.settingslib.metadata
import android.content.Context
+import android.os.Bundle
/** A node in preference hierarchy that is associated with [PreferenceMetadata]. */
open class PreferenceHierarchyNode internal constructor(val metadata: PreferenceMetadata) {
@@ -54,8 +55,14 @@ internal constructor(private val context: Context, metadata: PreferenceMetadata)
*
* @throws NullPointerException if screen is not registered to [PreferenceScreenRegistry]
*/
- operator fun String.unaryPlus() =
- +PreferenceHierarchyNode(PreferenceScreenRegistry.create(context, this)!!)
+ operator fun String.unaryPlus() = addPreferenceScreen(this, null)
+
+ /**
+ * Adds parameterized preference screen with given key (as a placeholder) to the hierarchy.
+ *
+ * @see String.unaryPlus
+ */
+ infix fun String.args(args: Bundle) = createPreferenceScreenHierarchy(this, args)
operator fun PreferenceHierarchyNode.unaryPlus() = also { children.add(it) }
@@ -122,6 +129,14 @@ internal constructor(private val context: Context, metadata: PreferenceMetadata)
}
/**
+ * Adds parameterized preference screen with given key (as a placeholder) to the hierarchy.
+ *
+ * @see addPreferenceScreen
+ */
+ fun addParameterizedScreen(screenKey: String, args: Bundle) =
+ addPreferenceScreen(screenKey, args)
+
+ /**
* Adds preference screen with given key (as a placeholder) to the hierarchy.
*
* This is mainly to support Android Settings overlays. OEMs might want to custom some of the
@@ -132,11 +147,13 @@ internal constructor(private val context: Context, metadata: PreferenceMetadata)
*
* @throws NullPointerException if screen is not registered to [PreferenceScreenRegistry]
*/
- fun addPreferenceScreen(screenKey: String) {
- children.add(
- PreferenceHierarchy(context, PreferenceScreenRegistry.create(context, screenKey)!!)
- )
- }
+ fun addPreferenceScreen(screenKey: String) = addPreferenceScreen(screenKey, null)
+
+ private fun addPreferenceScreen(screenKey: String, args: Bundle?): PreferenceHierarchyNode =
+ createPreferenceScreenHierarchy(screenKey, args).also { children.add(it) }
+
+ private fun createPreferenceScreenHierarchy(screenKey: String, args: Bundle?) =
+ PreferenceHierarchyNode(PreferenceScreenRegistry.create(context, screenKey, args)!!)
/** Extensions to add more preferences to the hierarchy. */
operator fun PreferenceHierarchy.plusAssign(init: PreferenceHierarchy.() -> Unit) = init(this)
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenBindingKeyProvider.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenBindingKeyProvider.kt
index 84014f191f68..4fd13ede6803 100644
--- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenBindingKeyProvider.kt
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenBindingKeyProvider.kt
@@ -17,13 +17,20 @@
package com.android.settingslib.metadata
import android.content.Context
+import android.os.Bundle
/** Provides the associated preference screen key for binding. */
interface PreferenceScreenBindingKeyProvider {
/** Returns the associated preference screen key. */
fun getPreferenceScreenBindingKey(context: Context): String?
+
+ /** Returns the arguments to build preference screen. */
+ fun getPreferenceScreenBindingArgs(context: Context): Bundle?
}
/** Extra key to provide the preference screen key for binding. */
const val EXTRA_BINDING_SCREEN_KEY = "settingslib:binding_screen_key"
+
+/** Extra key to provide arguments for preference screen binding. */
+const val EXTRA_BINDING_SCREEN_ARGS = "settingslib:binding_screen_args"
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenMetadata.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenMetadata.kt
index 850d4523e96e..7f1ded71e30a 100644
--- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenMetadata.kt
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenMetadata.kt
@@ -18,12 +18,25 @@ package com.android.settingslib.metadata
import android.content.Context
import android.content.Intent
+import android.os.Bundle
import androidx.annotation.AnyThread
import androidx.fragment.app.Fragment
+import kotlinx.coroutines.flow.Flow
-/** Metadata of preference screen. */
+/**
+ * Metadata of preference screen.
+ *
+ * For parameterized preference screen that relies on additional information (e.g. package name,
+ * language code) to build its content, the subclass must:
+ * - override [arguments] in constructor
+ * - add a static method `fun parameters(context: Context): List<Bundle>` (context is optional) to
+ * provide all possible arguments
+ */
@AnyThread
interface PreferenceScreenMetadata : PreferenceMetadata {
+ /** Arguments to build the screen content. */
+ val arguments: Bundle?
+ get() = null
/**
* The screen title resource, which precedes [getScreenTitle] if provided.
@@ -65,7 +78,12 @@ interface PreferenceScreenMetadata : PreferenceMetadata {
fun getLaunchIntent(context: Context, metadata: PreferenceMetadata?): Intent? = null
}
-/** Factory of [PreferenceScreenMetadata]. */
+/**
+ * Factory of [PreferenceScreenMetadata].
+ *
+ * Annotation processor generates implementation of this interface based on
+ * [ProvidePreferenceScreen] when [ProvidePreferenceScreen.parameterized] is `false`.
+ */
fun interface PreferenceScreenMetadataFactory {
/**
@@ -75,3 +93,44 @@ fun interface PreferenceScreenMetadataFactory {
*/
fun create(context: Context): PreferenceScreenMetadata
}
+
+/**
+ * Parameterized factory of [PreferenceScreenMetadata].
+ *
+ * Annotation processor generates implementation of this interface based on
+ * [ProvidePreferenceScreen] when [ProvidePreferenceScreen.parameterized] is `true`.
+ */
+interface PreferenceScreenMetadataParameterizedFactory : PreferenceScreenMetadataFactory {
+ override fun create(context: Context) = create(context, Bundle.EMPTY)
+
+ /**
+ * Creates a new [PreferenceScreenMetadata] with given arguments.
+ *
+ * @param context application context to create the PreferenceScreenMetadata
+ * @param args arguments to create the screen metadata, [Bundle.EMPTY] is reserved for the
+ * default case when screen is migrated from normal to parameterized
+ */
+ fun create(context: Context, args: Bundle): PreferenceScreenMetadata
+
+ /**
+ * Returns all possible arguments to create [PreferenceScreenMetadata].
+ *
+ * Note that [Bundle.EMPTY] is a special arguments reserved for backward compatibility when a
+ * preference screen was a normal screen but migrated to parameterized screen later:
+ * 1. Set [ProvidePreferenceScreen.parameterizedMigration] to `true`, so that the generated
+ * [acceptEmptyArguments] will be `true`.
+ * 1. In the original [parameters] implementation, produce a [Bundle.EMPTY] for the default
+ * case.
+ *
+ * Do not use [Bundle.EMPTY] for other purpose.
+ */
+ fun parameters(context: Context): Flow<Bundle>
+
+ /**
+ * Returns true when the parameterized screen was a normal screen.
+ *
+ * The [PreferenceScreenMetadata] is expected to accept an empty arguments ([Bundle.EMPTY]) and
+ * take care of backward compatibility.
+ */
+ fun acceptEmptyArguments(): Boolean = false
+}
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt
index c74b3151abb2..246310984db9 100644
--- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt
@@ -17,10 +17,13 @@
package com.android.settingslib.metadata
import android.content.Context
+import android.os.Bundle
+import android.util.Log
import com.android.settingslib.datastore.KeyValueStore
/** Registry of all available preference screens in the app. */
object PreferenceScreenRegistry : ReadWritePermitProvider {
+ private const val TAG = "ScreenRegistry"
/** Provider of key-value store. */
private lateinit var keyValueStoreProvider: KeyValueStoreProvider
@@ -52,9 +55,28 @@ object PreferenceScreenRegistry : ReadWritePermitProvider {
fun getKeyValueStore(context: Context, preference: PreferenceMetadata): KeyValueStore? =
keyValueStoreProvider.getKeyValueStore(context, preference)
- /** Creates [PreferenceScreenMetadata] of particular screen key. */
- fun create(context: Context, screenKey: String?): PreferenceScreenMetadata? =
- screenKey?.let { preferenceScreenMetadataFactories[it]?.create(context.applicationContext) }
+ /** Creates [PreferenceScreenMetadata] of particular screen. */
+ fun create(context: Context, screenCoordinate: PreferenceScreenCoordinate) =
+ create(context, screenCoordinate.screenKey, screenCoordinate.args)
+
+ /** Creates [PreferenceScreenMetadata] of particular screen key with given arguments. */
+ fun create(context: Context, screenKey: String?, args: Bundle?): PreferenceScreenMetadata? {
+ if (screenKey == null) return null
+ val factory = preferenceScreenMetadataFactories[screenKey] ?: return null
+ val appContext = context.applicationContext
+ if (factory is PreferenceScreenMetadataParameterizedFactory) {
+ if (args != null) return factory.create(appContext, args)
+ // In case the parameterized screen was a normal scree, it is expected to accept
+ // Bundle.EMPTY arguments and take care of backward compatibility.
+ if (factory.acceptEmptyArguments()) return factory.create(appContext)
+ Log.e(TAG, "screen $screenKey is parameterized but args is not provided")
+ return null
+ } else {
+ if (args == null) return factory.create(appContext)
+ Log.e(TAG, "screen $screenKey is not parameterized but args is provided")
+ return null
+ }
+ }
/**
* Sets the provider to check read write permit. Read and write requests are denied by default.
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt
index 65fbe2b66e77..dbac17d4e8b8 100644
--- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt
@@ -22,6 +22,7 @@ import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import androidx.preference.TwoStatePreference
+import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_ARGS
import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_KEY
import com.android.settingslib.metadata.PreferenceMetadata
import com.android.settingslib.metadata.PreferenceScreenMetadata
@@ -35,9 +36,11 @@ interface PreferenceScreenBinding : PreferenceBinding {
super.bind(preference, metadata)
val context = preference.context
val screenMetadata = metadata as PreferenceScreenMetadata
+ val extras = preference.extras
// Pass the preference key to fragment, so that the fragment could find associated
// preference screen registered in PreferenceScreenRegistry
- preference.extras.putString(EXTRA_BINDING_SCREEN_KEY, preference.key)
+ extras.putString(EXTRA_BINDING_SCREEN_KEY, preference.key)
+ screenMetadata.arguments?.let { extras.putBundle(EXTRA_BINDING_SCREEN_ARGS, it) }
if (preference is PreferenceScreen) {
val screenTitle = screenMetadata.screenTitle
preference.title =
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt
index ffe181d0c350..02f91c1bb50b 100644
--- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt
@@ -23,6 +23,7 @@ import android.util.Log
import androidx.annotation.XmlRes
import androidx.lifecycle.Lifecycle
import androidx.preference.PreferenceScreen
+import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_ARGS
import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_KEY
import com.android.settingslib.metadata.PreferenceScreenBindingKeyProvider
import com.android.settingslib.metadata.PreferenceScreenRegistry
@@ -89,13 +90,19 @@ open class PreferenceFragment :
@XmlRes protected open fun getPreferenceScreenResId(context: Context): Int = 0
protected fun getPreferenceScreenCreator(context: Context): PreferenceScreenCreator? =
- (PreferenceScreenRegistry.create(context, getPreferenceScreenBindingKey(context))
- as? PreferenceScreenCreator)
+ (PreferenceScreenRegistry.create(
+ context,
+ getPreferenceScreenBindingKey(context),
+ getPreferenceScreenBindingArgs(context),
+ ) as? PreferenceScreenCreator)
?.run { if (isFlagEnabled(context)) this else null }
override fun getPreferenceScreenBindingKey(context: Context): String? =
arguments?.getString(EXTRA_BINDING_SCREEN_KEY)
+ override fun getPreferenceScreenBindingArgs(context: Context): Bundle? =
+ arguments?.getBundle(EXTRA_BINDING_SCREEN_ARGS)
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
preferenceScreenBindingHelper?.onCreate()
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
index 4a6a589cd3c9..1cb8005ddae0 100644
--- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
@@ -31,6 +31,7 @@ import com.android.settingslib.datastore.KeyValueStore
import com.android.settingslib.datastore.KeyedDataObservable
import com.android.settingslib.datastore.KeyedObservable
import com.android.settingslib.datastore.KeyedObserver
+import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_ARGS
import com.android.settingslib.metadata.PersistentPreference
import com.android.settingslib.metadata.PreferenceChangeReason
import com.android.settingslib.metadata.PreferenceHierarchy
@@ -227,14 +228,16 @@ class PreferenceScreenBindingHelper(
/** Updates preference screen that has incomplete hierarchy. */
@JvmStatic
fun bind(preferenceScreen: PreferenceScreen) {
- PreferenceScreenRegistry.create(preferenceScreen.context, preferenceScreen.key)?.run {
+ val context = preferenceScreen.context
+ val args = preferenceScreen.peekExtras()?.getBundle(EXTRA_BINDING_SCREEN_ARGS)
+ PreferenceScreenRegistry.create(context, preferenceScreen.key, args)?.run {
if (!hasCompleteHierarchy()) {
val preferenceBindingFactory =
(this as? PreferenceScreenCreator)?.preferenceBindingFactory ?: return
bindRecursively(
preferenceScreen,
preferenceBindingFactory,
- getPreferenceHierarchy(preferenceScreen.context),
+ getPreferenceHierarchy(context),
)
}
}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt
index 211b3bdaea70..88c4fe6bf188 100644
--- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt
@@ -17,10 +17,12 @@
package com.android.settingslib.preference
import android.content.Context
+import android.os.Bundle
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import androidx.preference.PreferenceScreen
+import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_ARGS
import com.android.settingslib.metadata.PreferenceScreenRegistry
/** Factory to create preference screen. */
@@ -81,8 +83,12 @@ class PreferenceScreenFactory {
*
* The screen must be registered in [PreferenceScreenFactory] and provide a complete hierarchy.
*/
- fun createBindingScreen(context: Context, screenKey: String?): PreferenceScreen? {
- val metadata = PreferenceScreenRegistry.create(context, screenKey) ?: return null
+ fun createBindingScreen(
+ context: Context,
+ screenKey: String?,
+ args: Bundle?,
+ ): PreferenceScreen? {
+ val metadata = PreferenceScreenRegistry.create(context, screenKey, args) ?: return null
if (metadata is PreferenceScreenCreator && metadata.hasCompleteHierarchy()) {
return metadata.createPreferenceScreen(this)
}
@@ -94,8 +100,9 @@ class PreferenceScreenFactory {
@JvmStatic
fun createBindingScreen(preference: Preference): PreferenceScreen? {
val context = preference.context
+ val args = preference.peekExtras()?.getBundle(EXTRA_BINDING_SCREEN_ARGS)
val preferenceScreenCreator =
- (PreferenceScreenRegistry.create(context, preference.key)
+ (PreferenceScreenRegistry.create(context, preference.key, args)
as? PreferenceScreenCreator) ?: return null
if (!preferenceScreenCreator.hasCompleteHierarchy()) return null
val factory = PreferenceScreenFactory(context)
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictionsProvider.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictionsProvider.kt
index 3309faaa8db2..3a6327962dc2 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictionsProvider.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictionsProvider.kt
@@ -32,6 +32,7 @@ import kotlinx.coroutines.flow.flowOn
data class EnhancedConfirmation(
val key: String,
val packageName: String,
+ val isRestrictedSettingAllowed: Boolean?
)
data class Restrictions(
val userId: Int = UserHandle.myUserId(),
@@ -92,6 +93,9 @@ internal class RestrictionsProviderImpl(
}
restrictions.enhancedConfirmation?.let { ec ->
+ if (ec.isRestrictedSettingAllowed == true) {
+ return NoRestricted
+ }
RestrictedLockUtilsInternal
.checkIfRequiresEnhancedConfirmation(context, ec.key,
ec.packageName)
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt
index 7466f95e3fb8..5580d2e3211b 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt
@@ -155,7 +155,7 @@ internal fun <T : AppRecord> TogglePermissionAppListModel<T>.TogglePermissionApp
}
RestrictedSwitchPreference(
model = switchModel,
- restrictions = getRestrictions(userId, packageName),
+ restrictions = getRestrictions(userId, packageName, isAllowed()),
ifBlockedByAdminOverrideCheckedValueTo = switchifBlockedByAdminOverrideCheckedValueTo,
restrictionsProviderFactory = restrictionsProviderFactory,
)
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt
index d2867af1eda6..771eb85ee21a 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt
@@ -116,12 +116,15 @@ fun <T : AppRecord> TogglePermissionAppListModel<T>.isChangeableWithSystemUidChe
fun <T : AppRecord> TogglePermissionAppListModel<T>.getRestrictions(
userId: Int,
packageName: String,
+ isRestrictedSettingAllowed: Boolean?
) =
Restrictions(
userId = userId,
keys = switchRestrictionKeys,
enhancedConfirmation =
- enhancedConfirmationKey?.let { key -> EnhancedConfirmation(key, packageName) },
+ enhancedConfirmationKey?.let {
+ key -> EnhancedConfirmation(key, packageName, isRestrictedSettingAllowed)
+ },
)
interface TogglePermissionAppListProvider {
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt
index ec44d2af4ffa..bef2bdaaefaf 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt
@@ -149,13 +149,14 @@ internal class TogglePermissionInternalAppListModel<T : AppRecord>(
@Composable
fun getSummary(record: T): () -> String {
+ val allowed = listModel.isAllowed(record)
val restrictions =
listModel.getRestrictions(
userId = record.app.userId,
packageName = record.app.packageName,
+ allowed()
)
val restrictedMode by restrictionsProviderFactory.rememberRestrictedMode(restrictions)
- val allowed = listModel.isAllowed(record)
return RestrictedSwitchPreferenceModel.getSummary(
context = context,
summaryIfNoRestricted = { getSummaryIfNoRestricted(allowed()) },
diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedDropDownPreference.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedDropDownPreference.java
index c36ade979d47..d3219c287f4b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/RestrictedDropDownPreference.java
+++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedDropDownPreference.java
@@ -41,10 +41,23 @@ public class RestrictedDropDownPreference extends DropDownPreference implements
* package. Marks the preference as disabled if so.
* @param settingIdentifier The key identifying the setting
* @param packageName the package to check the settingIdentifier for
+ * @param settingEnabled Whether the setting in question is enabled
+ */
+ public void checkEcmRestrictionAndSetDisabled(@NonNull String settingIdentifier,
+ @NonNull String packageName, boolean settingEnabled) {
+ mHelper.checkEcmRestrictionAndSetDisabled(settingIdentifier, packageName, settingEnabled);
+ }
+
+ /**
+ * Checks if the given setting is subject to Enhanced Confirmation Mode restrictions for this
+ * package. Marks the preference as disabled if so.
+ * TODO b/390196024: remove this and update all callers to use the "settingEnabled" version
+ * @param settingIdentifier The key identifying the setting
+ * @param packageName the package to check the settingIdentifier for
*/
public void checkEcmRestrictionAndSetDisabled(@NonNull String settingIdentifier,
@NonNull String packageName) {
- mHelper.checkEcmRestrictionAndSetDisabled(settingIdentifier, packageName);
+ mHelper.checkEcmRestrictionAndSetDisabled(settingIdentifier, packageName, false);
}
@Override
diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedPreference.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedPreference.java
index 332042a5c4f9..ec1b2b3e2589 100644
--- a/packages/SettingsLib/src/com/android/settingslib/RestrictedPreference.java
+++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedPreference.java
@@ -107,12 +107,25 @@ public class RestrictedPreference extends TwoTargetPreference implements
/**
* Checks if the given setting is subject to Enhanced Confirmation Mode restrictions for this
* package. Marks the preference as disabled if so.
+ * TODO b/390196024: remove this and update all callers to use the "settingEnabled" version
* @param settingIdentifier The key identifying the setting
* @param packageName the package to check the settingIdentifier for
*/
public void checkEcmRestrictionAndSetDisabled(@NonNull String settingIdentifier,
@NonNull String packageName) {
- mHelper.checkEcmRestrictionAndSetDisabled(settingIdentifier, packageName);
+ mHelper.checkEcmRestrictionAndSetDisabled(settingIdentifier, packageName, false);
+ }
+
+ /**
+ * Checks if the given setting is subject to Enhanced Confirmation Mode restrictions for this
+ * package. Marks the preference as disabled if so.
+ * @param settingIdentifier The key identifying the setting
+ * @param packageName the package to check the settingIdentifier for
+ * @param settingEnabled Whether the setting in question is enabled
+ */
+ public void checkEcmRestrictionAndSetDisabled(@NonNull String settingIdentifier,
+ @NonNull String packageName, boolean settingEnabled) {
+ mHelper.checkEcmRestrictionAndSetDisabled(settingIdentifier, packageName, settingEnabled);
}
@Override
diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java
index 25628fba1b66..212e43aa4044 100644
--- a/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java
+++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java
@@ -204,16 +204,33 @@ public class RestrictedPreferenceHelper {
* package. Marks the preference as disabled if so.
* @param settingIdentifier The key identifying the setting
* @param packageName the package to check the settingIdentifier for
+ * @param settingEnabled Whether the setting in question is enabled
*/
public void checkEcmRestrictionAndSetDisabled(@NonNull String settingIdentifier,
- @NonNull String packageName) {
+ @NonNull String packageName, boolean settingEnabled) {
updatePackageDetails(packageName, android.os.Process.INVALID_UID);
+ if (settingEnabled) {
+ setDisabledByEcm(null);
+ return;
+ }
Intent intent = RestrictedLockUtilsInternal.checkIfRequiresEnhancedConfirmation(
mContext, settingIdentifier, packageName);
setDisabledByEcm(intent);
}
/**
+ * Checks if the given setting is subject to Enhanced Confirmation Mode restrictions for this
+ * package. Marks the preference as disabled if so.
+ * TODO b/390196024: remove this and update all callers to use the "settingEnabled" version
+ * @param settingIdentifier The key identifying the setting
+ * @param packageName the package to check the settingIdentifier for
+ */
+ public void checkEcmRestrictionAndSetDisabled(@NonNull String settingIdentifier,
+ @NonNull String packageName) {
+ checkEcmRestrictionAndSetDisabled(settingIdentifier, packageName, false);
+ }
+
+ /**
* @return EnforcedAdmin if we have been passed the restriction in the xml.
*/
public EnforcedAdmin checkRestrictionEnforced() {
diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedSelectorWithWidgetPreference.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedSelectorWithWidgetPreference.java
index 573869db5073..f8f16a9dd63b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/RestrictedSelectorWithWidgetPreference.java
+++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedSelectorWithWidgetPreference.java
@@ -134,10 +134,11 @@ public class RestrictedSelectorWithWidgetPreference extends SelectorWithWidgetPr
*
* @param settingIdentifier The key identifying the setting
* @param packageName the package to check the settingIdentifier for
+ * @param settingEnabled Whether the setting in question is enabled
*/
- public void checkEcmRestrictionAndSetDisabled(
- @NonNull String settingIdentifier, @NonNull String packageName) {
- mHelper.checkEcmRestrictionAndSetDisabled(settingIdentifier, packageName);
+ public void checkEcmRestrictionAndSetDisabled(@NonNull String settingIdentifier,
+ @NonNull String packageName, boolean settingEnabled) {
+ mHelper.checkEcmRestrictionAndSetDisabled(settingIdentifier, packageName, settingEnabled);
}
@Override
diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java
index 0aac9a1104e9..a5fa6a854e97 100644
--- a/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java
+++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java
@@ -220,10 +220,23 @@ public class RestrictedSwitchPreference extends SwitchPreferenceCompat implement
* package. Marks the preference as disabled if so.
* @param settingIdentifier The key identifying the setting
* @param packageName the package to check the settingIdentifier for
+ * @param settingEnabled Whether the setting in question is enabled
*/
public void checkEcmRestrictionAndSetDisabled(@NonNull String settingIdentifier,
- @NonNull String packageName) {
- mHelper.checkEcmRestrictionAndSetDisabled(settingIdentifier, packageName);
+ @NonNull String packageName, boolean settingEnabled) {
+ mHelper.checkEcmRestrictionAndSetDisabled(settingIdentifier, packageName, settingEnabled);
+ }
+
+ /**
+ * Checks if the given setting is subject to Enhanced Confirmation Mode restrictions for this
+ * package. Marks the preference as disabled if so.
+ * TODO b/390196024: remove this and update all callers to use the "settingEnabled" version
+ * @param settingIdentifier The key identifying the setting
+ * @param packageName the package to check the settingIdentifier for
+ */
+ public void checkEcmRestrictionAndSetDisabled(@NonNull String settingIdentifier,
+ @NonNull String packageName) {
+ mHelper.checkEcmRestrictionAndSetDisabled(settingIdentifier, packageName, false);
}
@Override
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
index 68e9fe703090..a00484ac28ab 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
@@ -1169,4 +1169,38 @@ public class BluetoothUtils {
String metadataValue = getFastPairCustomizedField(bluetoothDevice, TEMP_BOND_TYPE);
return Objects.equals(metadataValue, TEMP_BOND_DEVICE_METADATA_VALUE);
}
+
+ /**
+ * Set temp bond metadata to device
+ *
+ * @param device the BluetoothDevice to be marked as temp bond
+ *
+ * Note: It is a workaround since Bluetooth API is not ready.
+ * Avoid using this method if possible
+ */
+ public static void setTemporaryBondMetadata(@Nullable BluetoothDevice device) {
+ if (device == null) return;
+ if (!Flags.enableTemporaryBondDevicesUi()) {
+ Log.d(TAG, "Skip setTemporaryBondMetadata, flag is disabled");
+ return;
+ }
+ String fastPairCustomizedMeta = getStringMetaData(device,
+ METADATA_FAST_PAIR_CUSTOMIZED_FIELDS);
+ String fullContentWithTag = generateExpressionWithTag(TEMP_BOND_TYPE,
+ TEMP_BOND_DEVICE_METADATA_VALUE);
+ if (TextUtils.isEmpty(fastPairCustomizedMeta)) {
+ fastPairCustomizedMeta = fullContentWithTag;
+ } else {
+ String oldValue = extraTagValue(TEMP_BOND_TYPE, fastPairCustomizedMeta);
+ if (TextUtils.isEmpty(oldValue)) {
+ fastPairCustomizedMeta += fullContentWithTag;
+ } else {
+ fastPairCustomizedMeta =
+ fastPairCustomizedMeta.replace(
+ generateExpressionWithTag(TEMP_BOND_TYPE, oldValue),
+ fullContentWithTag);
+ }
+ }
+ device.setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, fastPairCustomizedMeta.getBytes());
+ }
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
index 7d5eece6c30e..84156429809b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
@@ -1087,8 +1087,9 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile {
private String generateRandomPassword() {
String randomUUID = UUID.randomUUID().toString();
- // first 12 chars from xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
- return randomUUID.substring(0, 8) + randomUUID.substring(9, 13);
+ // first 16 chars from xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
+ return randomUUID.substring(0, 8) + randomUUID.substring(9, 13) + randomUUID.substring(14,
+ 18);
}
private void registerContentObserver() {
diff --git a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java
index e3d7902f34b2..00973811dbf0 100644
--- a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java
+++ b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java
@@ -84,7 +84,11 @@ public final class ActionDisabledByAdminControllerFactory {
return false;
}
DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class);
- return ParentalControlsUtilsInternal.parentConsentRequired(context, dpm,
+ final SupervisionManager sm =
+ android.app.supervision.flags.Flags.deprecateDpmSupervisionApis()
+ ? context.getSystemService(SupervisionManager.class)
+ : null;
+ return ParentalControlsUtilsInternal.parentConsentRequired(context, dpm, sm,
BiometricAuthenticator.TYPE_ANY_BIOMETRIC, new UserHandle(UserHandle.myUserId()));
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
index cafe19ff9a9b..7c46db96595f 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
@@ -22,9 +22,11 @@ import static com.android.settingslib.flags.Flags.FLAG_ENABLE_DETERMINING_ADVANC
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -44,6 +46,8 @@ import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.net.Uri;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.Settings;
import android.util.Pair;
@@ -109,6 +113,7 @@ public class BluetoothUtilsTest {
+ "</HEARABLE_CONTROL_SLICE_WITH_WIDTH>";
private static final String TEMP_BOND_METADATA =
"<TEMP_BOND_TYPE>" + LE_AUDIO_SHARING_METADATA + "</TEMP_BOND_TYPE>";
+ private static final String FAKE_TEMP_BOND_METADATA = "<TEMP_BOND_TYPE>fake</TEMP_BOND_TYPE>";
private static final String TEST_EXCLUSIVE_MANAGER_PACKAGE = "com.test.manager";
private static final String TEST_EXCLUSIVE_MANAGER_COMPONENT = "com.test.manager/.component";
private static final int TEST_BROADCAST_ID = 25;
@@ -1348,4 +1353,34 @@ public class BluetoothUtilsTest {
assertThat(BluetoothUtils.isTemporaryBondDevice(mBluetoothDevice)).isTrue();
}
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI)
+ public void setTemporaryBondDevice_flagOff_doNothing() {
+ when(mBluetoothDevice.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS))
+ .thenReturn(new byte[]{});
+ BluetoothUtils.setTemporaryBondMetadata(mBluetoothDevice);
+ verify(mBluetoothDevice, never()).setMetadata(eq(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS),
+ any());
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI)
+ public void setTemporaryBondDevice_flagOn_setCorrectValue() {
+ when(mBluetoothDevice.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS))
+ .thenReturn(new byte[]{});
+ BluetoothUtils.setTemporaryBondMetadata(mBluetoothDevice);
+ verify(mBluetoothDevice).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS,
+ TEMP_BOND_METADATA.getBytes());
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI)
+ public void setTemporaryBondDevice_flagOff_replaceAndSetCorrectValue() {
+ when(mBluetoothDevice.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS))
+ .thenReturn(FAKE_TEMP_BOND_METADATA.getBytes());
+ BluetoothUtils.setTemporaryBondMetadata(mBluetoothDevice);
+ verify(mBluetoothDevice).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS,
+ TEMP_BOND_METADATA.getBytes());
+ }
}
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index d936a5c699c7..27a3cf1198b2 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -291,5 +291,6 @@ public class SecureSettings {
Settings.Secure.FACE_KEYGUARD_ENABLED,
Settings.Secure.FINGERPRINT_APP_ENABLED,
Settings.Secure.FINGERPRINT_KEYGUARD_ENABLED,
+ Settings.Secure.DUAL_SHADE,
};
}
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index 919c3c4721f2..8dca39fdc107 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -460,5 +460,6 @@ public class SecureSettingsValidators {
VALIDATORS.put(Secure.FACE_KEYGUARD_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.FINGERPRINT_APP_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.FINGERPRINT_KEYGUARD_ENABLED, BOOLEAN_VALIDATOR);
+ VALIDATORS.put(Secure.DUAL_SHADE, BOOLEAN_VALIDATOR);
}
}
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
index 14b2dfe414a4..fc402d45c3ec 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
@@ -89,6 +89,7 @@ import java.io.OutputStream;
import java.time.DateTimeException;
import java.util.Arrays;
import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
@@ -97,7 +98,6 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
-import java.util.HashMap;
import java.util.zip.CRC32;
/**
@@ -1753,8 +1753,8 @@ public class SettingsBackupAgent extends BackupAgentHelper {
if (previousDensity == null || previousDensity != newDensity) {
// From nothing to something is a change.
- DisplayDensityConfiguration.setForcedDisplayDensity(
- Display.DEFAULT_DISPLAY, newDensity);
+ DisplayDensityConfiguration.setForcedDisplayDensity(getBaseContext(),
+ info -> info.type == Display.TYPE_INTERNAL, newDensity);
}
}
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 9aad5d5f8367..246aa7158cab 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -178,6 +178,7 @@ public class SettingsBackupTest {
Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT,
Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS,
Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES,
+ Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_EXPERIENCE_FEATURES,
Settings.Global.DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES,
Settings.Global.DEVELOPMENT_FORCE_RTL,
Settings.Global.DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW,
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index b53198d8ae98..6491bf5c8f5b 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -392,6 +392,9 @@
<!-- To be able to decipher default applications for certain roles in shortcut helper -->
<uses-permission android:name="android.permission.MANAGE_DEFAULT_APPLICATIONS" />
+ <!-- To be able to set unrestricted system gesture exclusion rects -->
+ <uses-permission android:name="android.permission.SET_UNRESTRICTED_GESTURE_EXCLUSION"/>
+
<protected-broadcast android:name="com.android.settingslib.action.REGISTER_SLICE_RECEIVER" />
<protected-broadcast android:name="com.android.settingslib.action.UNREGISTER_SLICE_RECEIVER" />
<protected-broadcast android:name="com.android.settings.flashlight.action.FLASHLIGHT_CHANGED" />
@@ -548,10 +551,13 @@
android:exported="true" />
<service android:name=".wallpapers.GradientColorWallpaper"
- android:featureFlag="android.app.enable_connected_displays_wallpaper"
android:singleUser="true"
android:permission="android.permission.BIND_WALLPAPER"
- android:exported="true" />
+ android:exported="true">
+ <meta-data android:name="android.service.wallpaper"
+ android:resource="@xml/gradient_color_wallpaper">
+ </meta-data>
+ </service>
<activity android:name=".tuner.TunerActivity"
android:enabled="false"
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/aconfig/Android.bp b/packages/SystemUI/accessibility/accessibilitymenu/aconfig/Android.bp
index 0ff856e0b91e..1d74774c7c11 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/aconfig/Android.bp
+++ b/packages/SystemUI/accessibility/accessibilitymenu/aconfig/Android.bp
@@ -5,7 +5,7 @@ package {
aconfig_declarations {
name: "com_android_a11y_menu_flags",
package: "com.android.systemui.accessibility.accessibilitymenu",
- container: "system",
+ container: "system_ext",
srcs: [
"accessibility.aconfig",
],
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig b/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig
index 6d790114803a..bdf6d4242e68 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig
+++ b/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig
@@ -1,5 +1,5 @@
package: "com.android.systemui.accessibility.accessibilitymenu"
-container: "system"
+container: "system_ext"
# NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors.
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index d8d1a74448c5..a92df8026715 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -186,6 +186,16 @@ flag {
}
flag {
+ name: "notifications_pinned_hun_in_shade"
+ namespace: "systemui"
+ description: "Fixes displaying pinned HUNs in the Shade, making sure that their y and z positions are correct."
+ bug: "385041854"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "pss_app_selector_recents_split_screen"
namespace: "systemui"
description: "Allows recent apps selected for partial screenshare to be launched in split screen mode"
@@ -227,6 +237,13 @@ flag {
}
flag {
+ name: "notification_bundle_ui"
+ namespace: "systemui"
+ description: "Three-level group UI for notification bundles"
+ bug: "367996732"
+}
+
+flag {
name: "notification_background_tint_optimization"
namespace: "systemui"
description: "Re-enable the codepath that removed tinting of notifications when the"
@@ -524,6 +541,16 @@ flag {
}
flag {
+ name: "indication_text_a11y_fix"
+ namespace: "systemui"
+ description: "add double shadow to the indication text at the bottom of the lock screen"
+ bug: "349297241"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "rest_to_unlock"
namespace: "systemui"
description: "Require prolonged touch for fingerprint authentication"
@@ -1832,6 +1859,13 @@ flag {
}
flag {
+ name: "double_tap_to_sleep"
+ namespace: "systemui"
+ description: "Enable Double Tap to Sleep"
+ bug: "385194612"
+}
+
+flag{
name: "gsf_bouncer"
namespace: "systemui"
description: "Applies GSF font styles to Bouncer surfaces."
@@ -1846,13 +1880,6 @@ flag {
}
flag {
- name: "glanceable_hub_shortcut_button"
- namespace: "systemui"
- description: "Adds a shortcut button to lockscreen to show glanceable hub."
- bug: "378173531"
-}
-
-flag {
name: "spatial_model_launcher_pushback"
namespace: "systemui"
description: "Implement the depth push scaling effect on Launcher when users pull down shade."
@@ -1922,16 +1949,6 @@ flag {
}
flag {
- name: "stabilize_heads_up_group"
- namespace: "systemui"
- description: "Stabilize heads up groups in VisualStabilityCoordinator"
- bug: "381864715"
- metadata {
- purpose: PURPOSE_BUGFIX
- }
-}
-
-flag {
name: "magnetic_notification_horizontal_swipe"
namespace: "systemui"
description: "Add support for magnetic behavior on horizontal notification swipes."
@@ -1949,6 +1966,13 @@ flag {
}
flag {
+ name: "permission_helper_ui_rich_ongoing"
+ namespace: "systemui"
+ description: "[RONs] Guards inline permission helper for demoting RONs"
+ bug: "379186372"
+}
+
+flag {
name: "aod_ui_rich_ongoing"
namespace: "systemui"
description: "Show a rich ongoing notification on the always-on display (depends on ui_rich_ongoing)"
@@ -1963,4 +1987,14 @@ flag {
metadata {
purpose: PURPOSE_BUGFIX
}
-} \ No newline at end of file
+}
+
+flag {
+ name: "shade_launch_accessibility"
+ namespace: "systemui"
+ description: "Intercept accessibility focus events for the Shade during launch animations to avoid stray TalkBack events."
+ bug: "379222226"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt
index 78ae4af258fc..9545bda80b2d 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt
@@ -1,9 +1,12 @@
package com.android.systemui.animation
-private const val TAG_WGHT = "wght"
-private const val TAG_WDTH = "wdth"
-private const val TAG_OPSZ = "opsz"
-private const val TAG_ROND = "ROND"
+object GSFAxes {
+ const val WEIGHT = "wght"
+ const val WIDTH = "wdth"
+ const val SLANT = "slnt"
+ const val ROUND = "ROND"
+ const val OPTICAL_SIZE = "opsz"
+}
class FontVariationUtils {
private var mWeight = -1
@@ -21,7 +24,7 @@ class FontVariationUtils {
weight: Int = -1,
width: Int = -1,
opticalSize: Int = -1,
- roundness: Int = -1
+ roundness: Int = -1,
): String {
isUpdated = false
if (weight >= 0 && mWeight != weight) {
@@ -43,16 +46,20 @@ class FontVariationUtils {
}
var resultString = ""
if (mWeight >= 0) {
- resultString += "'$TAG_WGHT' $mWeight"
+ resultString += "'${GSFAxes.WEIGHT}' $mWeight"
}
if (mWidth >= 0) {
- resultString += (if (resultString.isBlank()) "" else ", ") + "'$TAG_WDTH' $mWidth"
+ resultString +=
+ (if (resultString.isBlank()) "" else ", ") + "'${GSFAxes.WIDTH}' $mWidth"
}
if (mOpticalSize >= 0) {
- resultString += (if (resultString.isBlank()) "" else ", ") + "'$TAG_OPSZ' $mOpticalSize"
+ resultString +=
+ (if (resultString.isBlank()) "" else ", ") +
+ "'${GSFAxes.OPTICAL_SIZE}' $mOpticalSize"
}
if (mRoundness >= 0) {
- resultString += (if (resultString.isBlank()) "" else ", ") + "'$TAG_ROND' $mRoundness"
+ resultString +=
+ (if (resultString.isBlank()) "" else ", ") + "'${GSFAxes.ROUND}' $mRoundness"
}
return if (isUpdated) resultString else ""
}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt b/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt
index e74b185851c4..1bb8ae5019fb 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt
@@ -40,7 +40,7 @@ import kotlinx.coroutines.CoroutineScope
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
fun rememberOffsetOverscrollEffect(
- animationSpec: AnimationSpec<Float> = MaterialTheme.motionScheme.defaultSpatialSpec()
+ animationSpec: AnimationSpec<Float> = MaterialTheme.motionScheme.slowSpatialSpec()
): OffsetOverscrollEffect {
val animationScope = rememberCoroutineScope()
return remember(animationScope, animationSpec) {
@@ -51,7 +51,7 @@ fun rememberOffsetOverscrollEffect(
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
fun rememberOffsetOverscrollEffectFactory(
- animationSpec: AnimationSpec<Float> = MaterialTheme.motionScheme.defaultSpatialSpec()
+ animationSpec: AnimationSpec<Float> = MaterialTheme.motionScheme.slowSpatialSpec()
): OverscrollFactory {
val animationScope = rememberCoroutineScope()
return remember(animationScope, animationSpec) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt
index c7930549abe8..44c375d6ac5e 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt
@@ -272,9 +272,8 @@ private fun calculateNumCellsWidth(width: Dp) =
}
private fun calculateNumCellsHeight(height: Dp) =
- // See https://developer.android.com/develop/ui/views/layout/use-window-size-classes
when {
- height >= 900.dp -> 3
+ height >= 1000.dp -> 3
height >= 480.dp -> 2
else -> 1
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/MediaCarouselSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/MediaCarouselSection.kt
index 0ff567bf90ad..d8b3f742b447 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/MediaCarouselSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/MediaCarouselSection.kt
@@ -19,12 +19,11 @@ package com.android.systemui.keyguard.ui.composable.section
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.animation.scene.ContentScope
import com.android.systemui.keyguard.ui.viewmodel.KeyguardMediaViewModel
+import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.media.controls.ui.composable.MediaCarousel
import com.android.systemui.media.controls.ui.controller.MediaCarouselController
import com.android.systemui.media.controls.ui.view.MediaHost
@@ -38,7 +37,7 @@ class MediaCarouselSection
constructor(
private val mediaCarouselController: MediaCarouselController,
@param:Named(MediaModule.KEYGUARD) private val mediaHost: MediaHost,
- private val keyguardMediaViewModel: KeyguardMediaViewModel,
+ private val keyguardMediaViewModelFactory: KeyguardMediaViewModel.Factory,
) {
@Composable
@@ -46,7 +45,10 @@ constructor(
isShadeLayoutWide: Boolean,
modifier: Modifier = Modifier,
) {
- val isMediaVisible by keyguardMediaViewModel.isMediaVisible.collectAsStateWithLifecycle()
+ val viewModel =
+ rememberViewModel(traceName = "KeyguardMediaCarousel") {
+ keyguardMediaViewModelFactory.create()
+ }
val horizontalPadding =
if (isShadeLayoutWide) {
dimensionResource(id = R.dimen.notification_side_paddings)
@@ -55,7 +57,7 @@ constructor(
dimensionResource(id = R.dimen.notification_panel_margin_horizontal)
}
MediaCarousel(
- isVisible = isMediaVisible,
+ isVisible = viewModel.isMediaVisible,
mediaHost = mediaHost,
modifier = modifier.fillMaxWidth().padding(horizontal = horizontalPadding),
carouselController = mediaCarouselController,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
index bdd0da9ce4a4..4e10ff689b19 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
@@ -67,11 +67,10 @@ import com.android.systemui.Flags
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.ui.compose.Icon
import com.android.systemui.compose.modifiers.sysuiResTag
-import com.android.systemui.haptics.slider.SeekableSliderTrackerConfig
-import com.android.systemui.haptics.slider.SliderHapticFeedbackConfig
import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.res.R
+import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider
import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderState
import kotlin.math.round
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -103,6 +102,10 @@ fun VolumeSlider(
}
val value by valueState(state)
+ val interactionSource = remember { MutableInteractionSource() }
+ val hapticsViewModel: SliderHapticsViewModel? =
+ setUpHapticsViewModel(value, state.valueRange, interactionSource, hapticsViewModelFactory)
+
Column(modifier = modifier.animateContentSize(), verticalArrangement = Arrangement.Top) {
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
@@ -127,8 +130,14 @@ fun VolumeSlider(
Slider(
value = value,
valueRange = state.valueRange,
- onValueChange = onValueChange,
- onValueChangeFinished = onValueChangeFinished,
+ onValueChange = { newValue ->
+ hapticsViewModel?.addVelocityDataPoint(newValue)
+ onValueChange(newValue)
+ },
+ onValueChangeFinished = {
+ hapticsViewModel?.onValueChangeEnded()
+ onValueChangeFinished?.invoke()
+ },
enabled = state.isEnabled,
modifier =
Modifier.height(40.dp)
@@ -210,41 +219,8 @@ private fun LegacyVolumeSlider(
) {
val value by valueState(state)
val interactionSource = remember { MutableInteractionSource() }
- val sliderStepSize = 1f / (state.valueRange.endInclusive - state.valueRange.start)
val hapticsViewModel: SliderHapticsViewModel? =
- hapticsViewModelFactory?.let {
- rememberViewModel(traceName = "SliderHapticsViewModel") {
- it.create(
- interactionSource,
- state.valueRange,
- Orientation.Horizontal,
- SliderHapticFeedbackConfig(
- lowerBookendScale = 0.2f,
- progressBasedDragMinScale = 0.2f,
- progressBasedDragMaxScale = 0.5f,
- deltaProgressForDragThreshold = 0f,
- additionalVelocityMaxBump = 0.2f,
- maxVelocityToScale = 0.1f, /* slider progress(from 0 to 1) per sec */
- sliderStepSize = sliderStepSize,
- ),
- SeekableSliderTrackerConfig(
- lowerBookendThreshold = 0f,
- upperBookendThreshold = 1f,
- ),
- )
- }
- }
- var lastDiscreteStep by remember { mutableFloatStateOf(round(value)) }
- LaunchedEffect(value) {
- snapshotFlow { value }
- .map { round(it) }
- .filter { it != lastDiscreteStep }
- .distinctUntilChanged()
- .collect { discreteStep ->
- lastDiscreteStep = discreteStep
- hapticsViewModel?.onValueChange(discreteStep)
- }
- }
+ setUpHapticsViewModel(value, state.valueRange, interactionSource, hapticsViewModelFactory)
PlatformSlider(
modifier =
@@ -357,3 +333,36 @@ private fun SliderIcon(
content = { Icon(modifier = Modifier.size(24.dp), icon = icon) },
)
}
+
+@Composable
+fun setUpHapticsViewModel(
+ value: Float,
+ valueRange: ClosedFloatingPointRange<Float>,
+ interactionSource: MutableInteractionSource,
+ hapticsViewModelFactory: SliderHapticsViewModel.Factory?,
+): SliderHapticsViewModel? {
+ return hapticsViewModelFactory?.let {
+ rememberViewModel(traceName = "SliderHapticsViewModel") {
+ it.create(
+ interactionSource,
+ valueRange,
+ Orientation.Horizontal,
+ VolumeHapticsConfigsProvider.sliderHapticFeedbackConfig(valueRange),
+ VolumeHapticsConfigsProvider.seekableSliderTrackerConfig,
+ )
+ }
+ .also { hapticsViewModel ->
+ var lastDiscreteStep by remember { mutableFloatStateOf(round(value)) }
+ LaunchedEffect(value) {
+ snapshotFlow { value }
+ .map { round(it) }
+ .filter { it != lastDiscreteStep }
+ .distinctUntilChanged()
+ .collect { discreteStep ->
+ lastDiscreteStep = discreteStep
+ hapticsViewModel.onValueChange(discreteStep)
+ }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index ab3f6396e5c0..70ff47baa7a9 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -127,7 +127,14 @@ internal class DraggableHandler(
directionChangeSlop = layoutImpl.directionChangeSlop,
)
- return createSwipeAnimation(layoutImpl, result, isUpOrLeft, orientation, gestureContext)
+ return createSwipeAnimation(
+ layoutImpl,
+ result,
+ isUpOrLeft,
+ orientation,
+ gestureContext,
+ layoutImpl.decayAnimationSpec,
+ )
}
private fun resolveSwipeSource(startedPosition: Offset): SwipeSource.Resolved? {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt
index 96d68ff03acd..41279d338896 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt
@@ -18,19 +18,26 @@ package com.android.compose.animation.scene
import androidx.activity.BackEventCompat
import androidx.activity.compose.PredictiveBackHandler
+import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.snap
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.runtime.Composable
+import androidx.compose.ui.util.fastCoerceIn
import com.android.compose.animation.scene.UserActionResult.ChangeScene
import com.android.compose.animation.scene.UserActionResult.HideOverlay
import com.android.compose.animation.scene.UserActionResult.ReplaceByOverlay
import com.android.compose.animation.scene.UserActionResult.ShowOverlay
-import com.android.compose.animation.scene.transition.animateProgress
import com.android.mechanics.ProvidedGestureContext
import com.android.mechanics.spec.InputDirection
+import kotlin.coroutines.cancellation.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
@Composable
internal fun PredictiveBackHandler(
@@ -63,6 +70,7 @@ internal fun PredictiveBackHandler(
distance = 1f,
gestureContext =
ProvidedGestureContext(dragOffset = 0f, direction = InputDirection.Max),
+ decayAnimationSpec = layoutImpl.decayAnimationSpec,
)
animateProgress(
@@ -93,3 +101,63 @@ private fun UserActionResult.copy(
is ReplaceByOverlay -> copy(transitionKey = transitionKey)
}
}
+
+private suspend fun <T : ContentKey> animateProgress(
+ state: MutableSceneTransitionLayoutStateImpl,
+ animation: SwipeAnimation<T>,
+ progress: Flow<Float>,
+ commitSpec: AnimationSpec<Float>?,
+ cancelSpec: AnimationSpec<Float>?,
+ animationScope: CoroutineScope? = null,
+) {
+ suspend fun animateOffset(targetContent: T, spec: AnimationSpec<Float>?) {
+ if (state.transitionState != animation.contentTransition || animation.isAnimatingOffset()) {
+ return
+ }
+
+ animation.animateOffset(
+ initialVelocity = 0f,
+ targetContent = targetContent,
+
+ // Important: we have to specify a spec that correctly animates *progress* (low
+ // visibility threshold) and not *offset* (higher visibility threshold).
+ spec = spec ?: animation.contentTransition.transformationSpec.progressSpec,
+ )
+ }
+
+ coroutineScope {
+ val collectionJob = launch {
+ try {
+ progress.collectLatest { progress ->
+ // Progress based animation should never overscroll given that the
+ // absoluteDistance exposed to overscroll builders is always 1f and will not
+ // lead to any noticeable transformation.
+ animation.dragOffset = progress.fastCoerceIn(0f, 1f)
+ }
+
+ // Transition committed.
+ animateOffset(animation.toContent, commitSpec)
+ } catch (e: CancellationException) {
+ // Transition cancelled.
+ animateOffset(animation.fromContent, cancelSpec)
+ }
+ }
+
+ // Start the transition.
+ animationScope?.launch { startTransition(state, animation, collectionJob) }
+ ?: startTransition(state, animation, collectionJob)
+ }
+}
+
+private suspend fun <T : ContentKey> startTransition(
+ state: MutableSceneTransitionLayoutStateImpl,
+ animation: SwipeAnimation<T>,
+ progressCollectionJob: Job,
+) {
+ state.startTransition(animation.contentTransition)
+ // The transition is done. Cancel the collection in case the transition was finished
+ // because it was interrupted by another transition.
+ if (progressCollectionJob.isActive) {
+ progressCollectionJob.cancel()
+ }
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index f6d40eef53a3..72bb82bd41bb 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -17,6 +17,7 @@
package com.android.compose.animation.scene
import androidx.annotation.FloatRange
+import androidx.compose.animation.rememberSplineBasedDecay
import androidx.compose.foundation.LocalOverscrollFactory
import androidx.compose.foundation.OverscrollEffect
import androidx.compose.foundation.OverscrollFactory
@@ -747,6 +748,7 @@ internal fun SceneTransitionLayoutForTesting(
val layoutDirection = LocalLayoutDirection.current
val defaultEffectFactory = checkNotNull(LocalOverscrollFactory.current)
val animationScope = rememberCoroutineScope()
+ val decayAnimationSpec = rememberSplineBasedDecay<Float>()
val layoutImpl = remember {
SceneTransitionLayoutImpl(
state = state as MutableSceneTransitionLayoutStateImpl,
@@ -762,6 +764,7 @@ internal fun SceneTransitionLayoutForTesting(
lookaheadScope = lookaheadScope,
directionChangeSlop = directionChangeSlop,
defaultEffectFactory = defaultEffectFactory,
+ decayAnimationSpec = decayAnimationSpec,
)
.also { onLayoutImpl?.invoke(it) }
}
@@ -801,6 +804,7 @@ internal fun SceneTransitionLayoutForTesting(
layoutImpl.swipeSourceDetector = swipeSourceDetector
layoutImpl.swipeDetector = swipeDetector
layoutImpl.transitionInterceptionThreshold = transitionInterceptionThreshold
+ layoutImpl.decayAnimationSpec = decayAnimationSpec
}
layoutImpl.Content(modifier)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 585da0633131..53996d25afdb 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -17,6 +17,7 @@
package com.android.compose.animation.scene
import androidx.annotation.VisibleForTesting
+import androidx.compose.animation.core.DecayAnimationSpec
import androidx.compose.foundation.OverscrollFactory
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
@@ -82,6 +83,7 @@ internal class SceneTransitionLayoutImpl(
internal var swipeSourceDetector: SwipeSourceDetector,
internal var swipeDetector: SwipeDetector,
internal var transitionInterceptionThreshold: Float,
+ internal var decayAnimationSpec: DecayAnimationSpec<Float>,
builder: SceneTransitionLayoutScope<InternalContentScope>.() -> Unit,
/**
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt
index 3bd37ad018b0..cb0d33cf5205 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt
@@ -21,6 +21,8 @@ package com.android.compose.animation.scene
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.DecayAnimationSpec
+import androidx.compose.animation.core.calculateTargetValue
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.runtime.getValue
@@ -42,6 +44,7 @@ internal fun createSwipeAnimation(
orientation: Orientation,
distance: Float,
gestureContext: MutableDragOffsetGestureContext,
+ decayAnimationSpec: DecayAnimationSpec<Float>,
): SwipeAnimation<*> {
return createSwipeAnimation(
layoutState,
@@ -53,6 +56,7 @@ internal fun createSwipeAnimation(
error("Computing contentForUserActions requires a SceneTransitionLayoutImpl")
},
gestureContext = gestureContext,
+ decayAnimationSpec = decayAnimationSpec,
)
}
@@ -62,6 +66,7 @@ internal fun createSwipeAnimation(
isUpOrLeft: Boolean,
orientation: Orientation,
gestureContext: MutableDragOffsetGestureContext,
+ decayAnimationSpec: DecayAnimationSpec<Float>,
distance: Float = DistanceUnspecified,
): SwipeAnimation<*> {
var lastDistance = distance
@@ -106,6 +111,7 @@ internal fun createSwipeAnimation(
distance = ::distance,
contentForUserActions = { layoutImpl.contentForUserActions().key },
gestureContext = gestureContext,
+ decayAnimationSpec = decayAnimationSpec,
)
}
@@ -117,6 +123,7 @@ private fun createSwipeAnimation(
distance: (SwipeAnimation<*>) -> Float,
contentForUserActions: () -> ContentKey,
gestureContext: MutableDragOffsetGestureContext,
+ decayAnimationSpec: DecayAnimationSpec<Float>,
): SwipeAnimation<*> {
fun <T : ContentKey> swipeAnimation(fromContent: T, toContent: T): SwipeAnimation<T> {
return SwipeAnimation(
@@ -128,6 +135,7 @@ private fun createSwipeAnimation(
requiresFullDistanceSwipe = result.requiresFullDistanceSwipe,
distance = distance,
gestureContext = gestureContext,
+ decayAnimationSpec = decayAnimationSpec,
)
}
@@ -201,6 +209,7 @@ internal class SwipeAnimation<T : ContentKey>(
private val distance: (SwipeAnimation<T>) -> Float,
currentContent: T = fromContent,
private val gestureContext: MutableDragOffsetGestureContext,
+ private val decayAnimationSpec: DecayAnimationSpec<Float>,
) : MutableDragOffsetGestureContext by gestureContext {
/** The [TransitionState.Transition] whose implementation delegates to this [SwipeAnimation]. */
lateinit var contentTransition: TransitionState.Transition
@@ -367,20 +376,10 @@ internal class SwipeAnimation<T : ContentKey>(
check(isAnimatingOffset())
- val motionSpatialSpec = spec ?: layoutState.motionScheme.defaultSpatialSpec()
-
val velocityConsumed = CompletableDeferred<Float>()
offsetAnimationRunnable.complete {
- val result =
- animatable.animateTo(
- targetValue = targetOffset,
- animationSpec = motionSpatialSpec,
- initialVelocity = initialVelocity,
- )
-
- // We are no longer able to consume the velocity, the rest can be consumed by another
- // component in the hierarchy.
- velocityConsumed.complete(initialVelocity - result.endState.velocity)
+ val consumed = animateOffset(animatable, targetOffset, initialVelocity, spec)
+ velocityConsumed.complete(consumed)
// Wait for overscroll to finish so that the transition is removed from the STLState
// only after the overscroll is done, to avoid dropping frame right when the user lifts
@@ -391,6 +390,53 @@ internal class SwipeAnimation<T : ContentKey>(
return velocityConsumed.await()
}
+ private suspend fun animateOffset(
+ animatable: Animatable<Float, AnimationVector1D>,
+ targetOffset: Float,
+ initialVelocity: Float,
+ spec: AnimationSpec<Float>?,
+ ): Float {
+ val initialOffset = animatable.value
+ val decayOffset =
+ decayAnimationSpec.calculateTargetValue(
+ initialVelocity = initialVelocity,
+ initialValue = initialOffset,
+ )
+
+ val willDecayReachBounds =
+ when {
+ targetOffset > initialOffset -> decayOffset >= targetOffset
+ targetOffset < initialOffset -> decayOffset <= targetOffset
+ else -> true
+ }
+
+ if (willDecayReachBounds) {
+ val result = animatable.animateDecay(initialVelocity, decayAnimationSpec)
+ check(animatable.value == targetOffset) {
+ buildString {
+ appendLine(
+ "animatable.value = ${animatable.value} != $targetOffset = targetOffset"
+ )
+ appendLine(" initialOffset=$initialOffset")
+ appendLine(" targetOffset=$targetOffset")
+ appendLine(" initialVelocity=$initialVelocity")
+ appendLine(" decayOffset=$decayOffset")
+ }
+ }
+ return initialVelocity - result.endState.velocity
+ }
+
+ val motionSpatialSpec = spec ?: layoutState.motionScheme.defaultSpatialSpec()
+ animatable.animateTo(
+ targetValue = targetOffset,
+ animationSpec = motionSpatialSpec,
+ initialVelocity = initialVelocity,
+ )
+
+ // We consumed the whole velocity.
+ return initialVelocity
+ }
+
private fun canChangeContent(targetContent: ContentKey): Boolean {
return when (val transition = contentTransition) {
is TransitionState.Transition.ChangeScene ->
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/Seek.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/Seek.kt
deleted file mode 100644
index 819cec712808..000000000000
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/Seek.kt
+++ /dev/null
@@ -1,203 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.compose.animation.scene.transition
-
-import androidx.annotation.FloatRange
-import androidx.compose.animation.core.AnimationSpec
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.ui.util.fastCoerceIn
-import com.android.compose.animation.scene.ContentKey
-import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
-import com.android.compose.animation.scene.MutableSceneTransitionLayoutStateImpl
-import com.android.compose.animation.scene.OverlayKey
-import com.android.compose.animation.scene.SceneKey
-import com.android.compose.animation.scene.SwipeAnimation
-import com.android.compose.animation.scene.TransitionKey
-import com.android.compose.animation.scene.UserActionResult
-import com.android.compose.animation.scene.createSwipeAnimation
-import com.android.mechanics.ProvidedGestureContext
-import com.android.mechanics.spec.InputDirection
-import kotlin.coroutines.cancellation.CancellationException
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.launch
-
-/**
- * Seek to the given [scene] using [progress].
- *
- * This will start a transition from the
- * [current scene][MutableSceneTransitionLayoutState.currentScene] to [scene], driven by the
- * progress in [progress]. Once [progress] stops emitting, we will animate progress to 1f (using
- * [animationSpec]) if it stopped normally or to 0f if it stopped with a
- * [kotlin.coroutines.cancellation.CancellationException].
- */
-suspend fun MutableSceneTransitionLayoutState.seekToScene(
- scene: SceneKey,
- @FloatRange(0.0, 1.0) progress: Flow<Float>,
- transitionKey: TransitionKey? = null,
- animationSpec: AnimationSpec<Float>? = null,
-) {
- require(scene != currentScene) {
- "seekToScene($scene) has to be called with a different scene than the current scene"
- }
-
- seek(UserActionResult.ChangeScene(scene, transitionKey), progress, animationSpec)
-}
-
-/**
- * Seek to show the given [overlay] using [progress].
- *
- * This will start a transition to show [overlay] from the
- * [current scene][MutableSceneTransitionLayoutState.currentScene], driven by the progress in
- * [progress]. Once [progress] stops emitting, we will animate progress to 1f (using
- * [animationSpec]) if it stopped normally or to 0f if it stopped with a
- * [kotlin.coroutines.cancellation.CancellationException].
- */
-suspend fun MutableSceneTransitionLayoutState.seekToShowOverlay(
- overlay: OverlayKey,
- @FloatRange(0.0, 1.0) progress: Flow<Float>,
- transitionKey: TransitionKey? = null,
- animationSpec: AnimationSpec<Float>? = null,
-) {
- require(overlay in currentOverlays) {
- "seekToShowOverlay($overlay) can be called only when the overlay is in currentOverlays"
- }
-
- seek(UserActionResult.ShowOverlay(overlay, transitionKey), progress, animationSpec)
-}
-
-/**
- * Seek to hide the given [overlay] using [progress].
- *
- * This will start a transition to hide [overlay] to the
- * [current scene][MutableSceneTransitionLayoutState.currentScene], driven by the progress in
- * [progress]. Once [progress] stops emitting, we will animate progress to 1f (using
- * [animationSpec]) if it stopped normally or to 0f if it stopped with a
- * [kotlin.coroutines.cancellation.CancellationException].
- */
-suspend fun MutableSceneTransitionLayoutState.seekToHideOverlay(
- overlay: OverlayKey,
- @FloatRange(0.0, 1.0) progress: Flow<Float>,
- transitionKey: TransitionKey? = null,
- animationSpec: AnimationSpec<Float>? = null,
-) {
- require(overlay !in currentOverlays) {
- "seekToHideOverlay($overlay) can be called only when the overlay is *not* in " +
- "currentOverlays"
- }
-
- seek(UserActionResult.HideOverlay(overlay, transitionKey), progress, animationSpec)
-}
-
-private suspend fun MutableSceneTransitionLayoutState.seek(
- result: UserActionResult,
- progress: Flow<Float>,
- animationSpec: AnimationSpec<Float>?,
-) {
- val layoutState =
- when (this) {
- is MutableSceneTransitionLayoutStateImpl -> this
- }
-
- val swipeAnimation =
- createSwipeAnimation(
- layoutState = layoutState,
- result = result,
-
- // We are animating progress, so distance is always 1f.
- distance = 1f,
-
- // The orientation and isUpOrLeft don't matter here given that they are only used during
- // overscroll, which is disabled for progress-based transitions.
- orientation = Orientation.Horizontal,
- isUpOrLeft = false,
- // There is no gesture information available here - animateProgress
- // will set the progress as the dragOffset.
- gestureContext = ProvidedGestureContext(0f, InputDirection.Max),
- )
-
- animateProgress(
- state = layoutState,
- animation = swipeAnimation,
- progress = progress,
- commitSpec = animationSpec,
- cancelSpec = animationSpec,
- )
-}
-
-internal suspend fun <T : ContentKey> animateProgress(
- state: MutableSceneTransitionLayoutStateImpl,
- animation: SwipeAnimation<T>,
- progress: Flow<Float>,
- commitSpec: AnimationSpec<Float>?,
- cancelSpec: AnimationSpec<Float>?,
- animationScope: CoroutineScope? = null,
-) {
- suspend fun animateOffset(targetContent: T, spec: AnimationSpec<Float>?) {
- if (state.transitionState != animation.contentTransition || animation.isAnimatingOffset()) {
- return
- }
-
- animation.animateOffset(
- initialVelocity = 0f,
- targetContent = targetContent,
-
- // Important: we have to specify a spec that correctly animates *progress* (low
- // visibility threshold) and not *offset* (higher visibility threshold).
- spec = spec ?: animation.contentTransition.transformationSpec.progressSpec,
- )
- }
-
- coroutineScope {
- val collectionJob = launch {
- try {
- progress.collectLatest { progress ->
- // Progress based animation should never overscroll given that the
- // absoluteDistance exposed to overscroll builders is always 1f and will not
- // lead to any noticeable transformation.
- animation.dragOffset = progress.fastCoerceIn(0f, 1f)
- }
-
- // Transition committed.
- animateOffset(animation.toContent, commitSpec)
- } catch (e: CancellationException) {
- // Transition cancelled.
- animateOffset(animation.fromContent, cancelSpec)
- }
- }
-
- // Start the transition.
- animationScope?.launch { startTransition(state, animation, collectionJob) }
- ?: startTransition(state, animation, collectionJob)
- }
-}
-
-private suspend fun <T : ContentKey> startTransition(
- state: MutableSceneTransitionLayoutStateImpl,
- animation: SwipeAnimation<T>,
- progressCollectionJob: Job,
-) {
- state.startTransition(animation.contentTransition)
- // The transition is done. Cancel the collection in case the transition was finished
- // because it was interrupted by another transition.
- if (progressCollectionJob.isActive) {
- progressCollectionJob.cancel()
- }
-}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index 6b439980cc68..969003cb92f3 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -18,7 +18,9 @@
package com.android.compose.animation.scene
+import androidx.compose.animation.SplineBasedFloatDecayAnimationSpec
import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.generateDecayAnimationSpec
import androidx.compose.animation.core.spring
import androidx.compose.foundation.overscroll
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
@@ -119,10 +121,11 @@ class DraggableHandlerTest {
val transitionInterceptionThreshold = 0.05f
val directionChangeSlop = 10f
+ private val density = Density(1f)
private val layoutImpl =
SceneTransitionLayoutImpl(
state = layoutState,
- density = Density(1f),
+ density = density,
layoutDirection = LayoutDirection.Ltr,
swipeSourceDetector = DefaultEdgeDetector,
swipeDetector = DefaultSwipeDetector,
@@ -134,6 +137,8 @@ class DraggableHandlerTest {
animationScope = testScope,
directionChangeSlop = directionChangeSlop,
defaultEffectFactory = defaultEffectFactory,
+ decayAnimationSpec =
+ SplineBasedFloatDecayAnimationSpec(density).generateDecayAnimationSpec(),
)
.apply { setContentsAndLayoutTargetSizeForTest(LAYOUT_SIZE) }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
index c5e4061e834a..26f3c259dca9 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
@@ -24,18 +24,14 @@ import com.android.compose.animation.scene.TestScenes.SceneB
import com.android.compose.animation.scene.TestScenes.SceneC
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.subjects.assertThat
-import com.android.compose.animation.scene.transition.seekToScene
import com.android.compose.test.TestSceneTransition
import com.android.compose.test.runMonotonicClockTest
import com.android.compose.test.transition
import com.google.common.truth.Truth.assertThat
-import kotlin.coroutines.cancellation.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.cancelAndJoin
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runCurrent
@@ -282,77 +278,6 @@ class SceneTransitionLayoutStateTest {
}
@Test
- fun seekToScene() = runMonotonicClockTest {
- val state = MutableSceneTransitionLayoutStateForTests(SceneA)
- val progress = Channel<Float>()
-
- val job =
- launch(start = CoroutineStart.UNDISPATCHED) {
- state.seekToScene(SceneB, progress.consumeAsFlow())
- }
-
- val transition = assertThat(state.transitionState).isSceneTransition()
- assertThat(transition).hasFromScene(SceneA)
- assertThat(transition).hasToScene(SceneB)
- assertThat(transition).hasProgress(0f)
-
- // Change progress.
- progress.send(0.4f)
- assertThat(transition).hasProgress(0.4f)
-
- // Close the channel normally to confirm the transition.
- progress.close()
- job.join()
- assertThat(state.transitionState).isIdle()
- assertThat(state.transitionState).hasCurrentScene(SceneB)
- }
-
- @Test
- fun seekToScene_cancelled() = runMonotonicClockTest {
- val state = MutableSceneTransitionLayoutStateForTests(SceneA)
- val progress = Channel<Float>()
-
- val job =
- launch(start = CoroutineStart.UNDISPATCHED) {
- state.seekToScene(SceneB, progress.consumeAsFlow())
- }
-
- val transition = assertThat(state.transitionState).isSceneTransition()
- assertThat(transition).hasFromScene(SceneA)
- assertThat(transition).hasToScene(SceneB)
- assertThat(transition).hasProgress(0f)
-
- // Change progress.
- progress.send(0.4f)
- assertThat(transition).hasProgress(0.4f)
-
- // Close the channel with a CancellationException to cancel the transition.
- progress.close(CancellationException())
- job.join()
- assertThat(state.transitionState).isIdle()
- assertThat(state.transitionState).hasCurrentScene(SceneA)
- }
-
- @Test
- fun seekToScene_interrupted() = runMonotonicClockTest {
- val state = MutableSceneTransitionLayoutStateForTests(SceneA)
- val progress = Channel<Float>()
-
- val job =
- launch(start = CoroutineStart.UNDISPATCHED) {
- state.seekToScene(SceneB, progress.consumeAsFlow())
- }
-
- assertThat(state.transitionState).isSceneTransition()
-
- // Start a new transition, interrupting the seek transition.
- state.setTargetScene(SceneB, animationScope = this)
-
- // The previous job is cancelled and does not infinitely collect the progress.
- job.join()
- }
-
- @Test
fun replacedTransitionIsRemovedFromFinishedTransitions() = runTest {
val state = MutableSceneTransitionLayoutStateForTests(SceneA)
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
index f6ff3268fca4..36029177d4f6 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
@@ -17,6 +17,7 @@ import android.content.Context
import android.content.res.Resources
import android.graphics.Typeface
import android.view.LayoutInflater
+import com.android.systemui.animation.GSFAxes
import com.android.systemui.customization.R
import com.android.systemui.log.core.MessageBuffer
import com.android.systemui.plugins.clocks.ClockController
@@ -107,18 +108,18 @@ class DefaultClockProvider(
// TODO(b/364681643): Variations for retargetted DIGITAL_CLOCK_FLEX
val LEGACY_FLEX_LS_VARIATION =
listOf(
- ClockFontAxisSetting("wght", 600f),
- ClockFontAxisSetting("wdth", 100f),
- ClockFontAxisSetting("ROND", 100f),
- ClockFontAxisSetting("slnt", 0f),
+ ClockFontAxisSetting(GSFAxes.WEIGHT, 600f),
+ ClockFontAxisSetting(GSFAxes.WIDTH, 100f),
+ ClockFontAxisSetting(GSFAxes.ROUND, 100f),
+ ClockFontAxisSetting(GSFAxes.SLANT, 0f),
)
val LEGACY_FLEX_AOD_VARIATION =
listOf(
- ClockFontAxisSetting("wght", 74f),
- ClockFontAxisSetting("wdth", 43f),
- ClockFontAxisSetting("ROND", 100f),
- ClockFontAxisSetting("slnt", 0f),
+ ClockFontAxisSetting(GSFAxes.WEIGHT, 74f),
+ ClockFontAxisSetting(GSFAxes.WIDTH, 43f),
+ ClockFontAxisSetting(GSFAxes.ROUND, 100f),
+ ClockFontAxisSetting(GSFAxes.SLANT, 0f),
)
val FLEX_TYPEFACE by lazy {
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
index e69fa994931d..cc3769e0a568 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
@@ -16,6 +16,7 @@
package com.android.systemui.shared.clocks
+import com.android.systemui.animation.GSFAxes
import com.android.systemui.customization.R
import com.android.systemui.plugins.clocks.AlarmData
import com.android.systemui.plugins.clocks.AxisType
@@ -122,16 +123,16 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController
val FONT_AXES =
listOf(
ClockFontAxis(
- key = "wght",
+ key = GSFAxes.WEIGHT,
type = AxisType.Float,
- minValue = 1f,
+ minValue = 25f,
currentValue = 400f,
maxValue = 1000f,
name = "Weight",
description = "Glyph Weight",
),
ClockFontAxis(
- key = "wdth",
+ key = GSFAxes.WIDTH,
type = AxisType.Float,
minValue = 25f,
currentValue = 100f,
@@ -140,7 +141,7 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController
description = "Glyph Width",
),
ClockFontAxis(
- key = "ROND",
+ key = GSFAxes.ROUND,
type = AxisType.Boolean,
minValue = 0f,
currentValue = 0f,
@@ -149,7 +150,7 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController
description = "Glyph Roundness",
),
ClockFontAxis(
- key = "slnt",
+ key = GSFAxes.SLANT,
type = AxisType.Boolean,
minValue = 0f,
currentValue = 0f,
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
index 827bd6898310..e2bbe0fef3c0 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
@@ -21,6 +21,7 @@ import android.view.Gravity
import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout
+import com.android.systemui.animation.GSFAxes
import com.android.systemui.customization.R
import com.android.systemui.plugins.clocks.AlarmData
import com.android.systemui.plugins.clocks.ClockAnimations
@@ -125,7 +126,19 @@ class FlexClockFaceController(clockCtx: ClockContext, private val isLargeClock:
layerController.faceEvents.onThemeChanged(theme)
}
- override fun onFontAxesChanged(axes: List<ClockFontAxisSetting>) {
+ override fun onFontAxesChanged(settings: List<ClockFontAxisSetting>) {
+ var axes = settings
+ if (!isLargeClock) {
+ axes =
+ axes.map { axis ->
+ if (axis.key == GSFAxes.WIDTH && axis.value > SMALL_CLOCK_MAX_WDTH) {
+ axis.copy(value = SMALL_CLOCK_MAX_WDTH)
+ } else {
+ axis
+ }
+ }
+ }
+
layerController.events.onFontAxesChanged(axes)
}
@@ -236,6 +249,7 @@ class FlexClockFaceController(clockCtx: ClockContext, private val isLargeClock:
}
companion object {
+ val SMALL_CLOCK_MAX_WDTH = 120f
val SMALL_LAYER_CONFIG =
LayerConfig(
timespec = DigitalTimespec.TIME_FULL_FORMAT,
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
index c40bb9a5ebea..3eb519186a3e 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
@@ -139,13 +139,49 @@ class FlexClockView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) {
digitalClockTextViewMap.forEach { (_, textView) -> textView.refreshText() }
}
+ override fun setVisibility(visibility: Int) {
+ if (visibility != this.visibility) {
+ logger.d({ "setVisibility(${str1 ?: int1})" }) {
+ int1 = visibility
+ str1 =
+ when (visibility) {
+ VISIBLE -> "VISIBLE"
+ INVISIBLE -> "INVISIBLE"
+ GONE -> "GONE"
+ else -> null
+ }
+ }
+ }
+
+ super.setVisibility(visibility)
+ }
+
+ private var loggedAlpha = 1000f
+
+ override fun setAlpha(alpha: Float) {
+ val delta = if (alpha <= 0f || alpha >= 1f) 0.001f else 0.5f
+ if (abs(loggedAlpha - alpha) >= delta) {
+ loggedAlpha = alpha
+ logger.d({ "setAlpha($double1)" }) { double1 = alpha.toDouble() }
+ }
+ super.setAlpha(alpha)
+ }
+
+ private val isDrawn: Boolean
+ get() = (mPrivateFlags and 0x20 /* PFLAG_DRAWN */) > 0
+
override fun invalidate() {
- logger.d("invalidate()")
+ if (isDrawn && visibility == VISIBLE) {
+ logger.d("invalidate()")
+ }
+
super.invalidate()
}
override fun requestLayout() {
- logger.d("requestLayout()")
+ if (!isLayoutRequested()) {
+ logger.d("requestLayout()")
+ }
super.requestLayout()
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
index 2b0825f39243..fbd5887c5b54 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
@@ -34,6 +34,7 @@ import android.view.View.MeasureSpec.EXACTLY
import android.view.animation.Interpolator
import android.widget.TextView
import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.animation.GSFAxes
import com.android.systemui.animation.TextAnimator
import com.android.systemui.customization.R
import com.android.systemui.log.core.Logger
@@ -44,6 +45,7 @@ import com.android.systemui.shared.clocks.DimensionParser
import com.android.systemui.shared.clocks.FontTextStyle
import com.android.systemui.shared.clocks.LogUtil
import java.lang.Thread
+import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
@@ -206,7 +208,10 @@ open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSe
}
override fun onDraw(canvas: Canvas) {
- logger.d({ "onDraw(); ls: $str1" }) { str1 = textAnimator.textInterpolator.shapedText }
+ logger.d({ "onDraw(${str1?.replace("\n", "\\n")})" }) {
+ str1 = textAnimator.textInterpolator.shapedText
+ }
+
val translation = getLocalTranslation()
canvas.translate(translation.x.toFloat(), translation.y.toFloat())
digitTranslateAnimator?.let {
@@ -221,8 +226,42 @@ open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSe
canvas.translate(-translation.x.toFloat(), -translation.y.toFloat())
}
+ override fun setVisibility(visibility: Int) {
+ if (visibility != this.visibility) {
+ logger.d({ "setVisibility(${str1 ?: int1})" }) {
+ int1 = visibility
+ str1 =
+ when (visibility) {
+ VISIBLE -> "VISIBLE"
+ INVISIBLE -> "INVISIBLE"
+ GONE -> "GONE"
+ else -> null
+ }
+ }
+ }
+
+ super.setVisibility(visibility)
+ }
+
+ private var loggedAlpha = 1000f
+
+ override fun setAlpha(alpha: Float) {
+ val delta = if (alpha <= 0f || alpha >= 1f) 0.001f else 0.5f
+ if (abs(loggedAlpha - alpha) >= delta) {
+ loggedAlpha = alpha
+ logger.d({ "setAlpha($double1)" }) { double1 = alpha.toDouble() }
+ }
+ super.setAlpha(alpha)
+ }
+
+ private val isDrawn: Boolean
+ get() = (mPrivateFlags and 0x20 /* PFLAG_DRAWN */) > 0
+
override fun invalidate() {
- logger.d("invalidate()")
+ if (isDrawn && visibility == VISIBLE) {
+ logger.d("invalidate()")
+ }
+
super.invalidate()
(parent as? FlexClockView)?.invalidate()
}
@@ -490,22 +529,22 @@ open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSe
Paint().also { it.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT) }
val AOD_COLOR = Color.WHITE
- val OPTICAL_SIZE_AXIS = ClockFontAxisSetting("opsz", 144f)
+ val OPTICAL_SIZE_AXIS = ClockFontAxisSetting(GSFAxes.OPTICAL_SIZE, 144f)
val DEFAULT_LS_VARIATION =
listOf(
OPTICAL_SIZE_AXIS,
- ClockFontAxisSetting("wght", 400f),
- ClockFontAxisSetting("wdth", 100f),
- ClockFontAxisSetting("ROND", 0f),
- ClockFontAxisSetting("slnt", 0f),
+ ClockFontAxisSetting(GSFAxes.WEIGHT, 400f),
+ ClockFontAxisSetting(GSFAxes.WIDTH, 100f),
+ ClockFontAxisSetting(GSFAxes.ROUND, 0f),
+ ClockFontAxisSetting(GSFAxes.SLANT, 0f),
)
val DEFAULT_AOD_VARIATION =
listOf(
OPTICAL_SIZE_AXIS,
- ClockFontAxisSetting("wght", 200f),
- ClockFontAxisSetting("wdth", 100f),
- ClockFontAxisSetting("ROND", 0f),
- ClockFontAxisSetting("slnt", 0f),
+ ClockFontAxisSetting(GSFAxes.WEIGHT, 200f),
+ ClockFontAxisSetting(GSFAxes.WIDTH, 100f),
+ ClockFontAxisSetting(GSFAxes.ROUND, 0f),
+ ClockFontAxisSetting(GSFAxes.SLANT, 0f),
)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt
index b0f81c012cca..f44769d522eb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt
@@ -7,11 +7,6 @@ import junit.framework.Assert
import org.junit.Test
import org.junit.runner.RunWith
-private const val TAG_WGHT = "wght"
-private const val TAG_WDTH = "wdth"
-private const val TAG_OPSZ = "opsz"
-private const val TAG_ROND = "ROND"
-
@RunWith(AndroidJUnit4::class)
@SmallTest
class FontVariationUtilsTest : SysuiTestCase() {
@@ -23,19 +18,22 @@ class FontVariationUtilsTest : SysuiTestCase() {
weight = 100,
width = 100,
opticalSize = -1,
- roundness = 100
+ roundness = 100,
)
- Assert.assertEquals("'$TAG_WGHT' 100, '$TAG_WDTH' 100, '$TAG_ROND' 100", initFvar)
+ Assert.assertEquals(
+ "'${GSFAxes.WEIGHT}' 100, '${GSFAxes.WIDTH}' 100, '${GSFAxes.ROUND}' 100",
+ initFvar,
+ )
val updatedFvar =
fontVariationUtils.updateFontVariation(
weight = 200,
width = 100,
opticalSize = 0,
- roundness = 100
+ roundness = 100,
)
Assert.assertEquals(
- "'$TAG_WGHT' 200, '$TAG_WDTH' 100, '$TAG_OPSZ' 0, '$TAG_ROND' 100",
- updatedFvar
+ "'${GSFAxes.WEIGHT}' 200, '${GSFAxes.WIDTH}' 100, '${GSFAxes.OPTICAL_SIZE}' 0, '${GSFAxes.ROUND}' 100",
+ updatedFvar,
)
}
@@ -46,14 +44,14 @@ class FontVariationUtilsTest : SysuiTestCase() {
weight = 100,
width = 100,
opticalSize = 0,
- roundness = 100
+ roundness = 100,
)
val updatedFvar1 =
fontVariationUtils.updateFontVariation(
weight = 100,
width = 100,
opticalSize = 0,
- roundness = 100
+ roundness = 100,
)
Assert.assertEquals("", updatedFvar1)
val updatedFvar2 = fontVariationUtils.updateFontVariation()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/DefaultShortcutCategoriesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/DefaultShortcutCategoriesRepositoryTest.kt
index 3bf59f34db76..cd05980385e0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/DefaultShortcutCategoriesRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/DefaultShortcutCategoriesRepositoryTest.kt
@@ -50,6 +50,7 @@ import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCommand
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
+import com.android.systemui.keyboard.shortcut.shortcutHelperAccessibilityShortcutsSource
import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource
import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource
import com.android.systemui.keyboard.shortcut.shortcutHelperInputShortcutsSource
@@ -88,6 +89,7 @@ class DefaultShortcutCategoriesRepositoryTest : SysuiTestCase() {
it.shortcutHelperAppCategoriesShortcutsSource = fakeAppCategoriesSource
it.shortcutHelperInputShortcutsSource = FakeKeyboardShortcutGroupsSource()
it.shortcutHelperCurrentAppShortcutsSource = FakeKeyboardShortcutGroupsSource()
+ it.shortcutHelperAccessibilityShortcutsSource = FakeKeyboardShortcutGroupsSource()
}
private val repo = kosmos.defaultShortcutCategoriesRepository
@@ -284,14 +286,20 @@ class DefaultShortcutCategoriesRepositoryTest : SysuiTestCase() {
val categories by collectLastValue(repo.categories)
val cycleForwardThroughRecentAppsShortcut =
- categories?.first { it.type == ShortcutCategoryType.MultiTasking }
- ?.subCategories?.first { it.label == recentAppsGroup.label }
- ?.shortcuts?.first { it.label == CYCLE_FORWARD_THROUGH_RECENT_APPS_SHORTCUT_LABEL }
+ categories
+ ?.first { it.type == ShortcutCategoryType.MultiTasking }
+ ?.subCategories
+ ?.first { it.label == recentAppsGroup.label }
+ ?.shortcuts
+ ?.first { it.label == CYCLE_FORWARD_THROUGH_RECENT_APPS_SHORTCUT_LABEL }
val cycleBackThroughRecentAppsShortcut =
- categories?.first { it.type == ShortcutCategoryType.MultiTasking }
- ?.subCategories?.first { it.label == recentAppsGroup.label }
- ?.shortcuts?.first { it.label == CYCLE_BACK_THROUGH_RECENT_APPS_SHORTCUT_LABEL }
+ categories
+ ?.first { it.type == ShortcutCategoryType.MultiTasking }
+ ?.subCategories
+ ?.first { it.label == recentAppsGroup.label }
+ ?.shortcuts
+ ?.first { it.label == CYCLE_BACK_THROUGH_RECENT_APPS_SHORTCUT_LABEL }
assertThat(cycleForwardThroughRecentAppsShortcut?.isCustomizable).isFalse()
assertThat(cycleBackThroughRecentAppsShortcut?.isCustomizable).isFalse()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt
index 8f0bc640f0eb..61490986f4a9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt
@@ -38,6 +38,7 @@ import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.InputMethodEditor
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System
+import com.android.systemui.keyboard.shortcut.shortcutHelperAccessibilityShortcutsSource
import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource
import com.android.systemui.keyboard.shortcut.shortcutHelperCategoriesInteractor
import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource
@@ -76,6 +77,7 @@ class ShortcutHelperCategoriesInteractorTest : SysuiTestCase() {
it.shortcutHelperMultiTaskingShortcutsSource = multitaskingShortcutsSource
it.shortcutHelperAppCategoriesShortcutsSource = FakeKeyboardShortcutGroupsSource()
it.shortcutHelperCurrentAppShortcutsSource = FakeKeyboardShortcutGroupsSource()
+ it.shortcutHelperAccessibilityShortcutsSource = FakeKeyboardShortcutGroupsSource()
it.userTracker = FakeUserTracker(onCreateCurrentUserContext = { mockUserContext })
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarterTest.kt
index 000024f9b814..7a343351ef64 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarterTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarterTest.kt
@@ -26,6 +26,7 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.keyboard.shortcut.data.source.FakeKeyboardShortcutGroupsSource
import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts
import com.android.systemui.keyboard.shortcut.shortcutCustomizationDialogStarterFactory
+import com.android.systemui.keyboard.shortcut.shortcutHelperAccessibilityShortcutsSource
import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource
import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource
import com.android.systemui.keyboard.shortcut.shortcutHelperInputShortcutsSource
@@ -66,6 +67,7 @@ class ShortcutHelperDialogStarterTest : SysuiTestCase() {
it.testDispatcher = UnconfinedTestDispatcher()
it.shortcutHelperSystemShortcutsSource = fakeSystemSource
it.shortcutHelperMultiTaskingShortcutsSource = fakeMultiTaskingSource
+ it.shortcutHelperAccessibilityShortcutsSource = FakeKeyboardShortcutGroupsSource()
it.shortcutHelperAppCategoriesShortcutsSource = FakeKeyboardShortcutGroupsSource()
it.shortcutHelperInputShortcutsSource = FakeKeyboardShortcutGroupsSource()
it.shortcutHelperCurrentAppShortcutsSource = FakeKeyboardShortcutGroupsSource()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt
index 3fc46b973959..cf38072912e9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt
@@ -45,6 +45,7 @@ import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
import com.android.systemui.keyboard.shortcut.shared.model.shortcut
+import com.android.systemui.keyboard.shortcut.shortcutHelperAccessibilityShortcutsSource
import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource
import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource
import com.android.systemui.keyboard.shortcut.shortcutHelperInputShortcutsSource
@@ -95,6 +96,7 @@ class ShortcutHelperViewModelTest : SysuiTestCase() {
it.shortcutHelperMultiTaskingShortcutsSource = fakeMultiTaskingSource
it.shortcutHelperAppCategoriesShortcutsSource = FakeKeyboardShortcutGroupsSource()
it.shortcutHelperInputShortcutsSource = FakeKeyboardShortcutGroupsSource()
+ it.shortcutHelperAccessibilityShortcutsSource = FakeKeyboardShortcutGroupsSource()
it.shortcutHelperCurrentAppShortcutsSource = fakeCurrentAppsSource
it.userTracker = FakeUserTracker(onCreateCurrentUserContext = { mockUserContext })
}
@@ -112,9 +114,12 @@ class ShortcutHelperViewModelTest : SysuiTestCase() {
fakeSystemSource.setGroups(TestShortcuts.systemGroups)
fakeMultiTaskingSource.setGroups(TestShortcuts.multitaskingGroups)
fakeCurrentAppsSource.setGroups(TestShortcuts.currentAppGroups)
- whenever(mockPackageManager.getApplicationInfo(anyString(), eq(0))).thenReturn(mockApplicationInfo)
- whenever(mockPackageManager.getApplicationLabel(mockApplicationInfo)).thenReturn("Current App")
- whenever(mockPackageManager.getApplicationIcon(anyString())).thenThrow(NameNotFoundException())
+ whenever(mockPackageManager.getApplicationInfo(anyString(), eq(0)))
+ .thenReturn(mockApplicationInfo)
+ whenever(mockPackageManager.getApplicationLabel(mockApplicationInfo))
+ .thenReturn("Current App")
+ whenever(mockPackageManager.getApplicationIcon(anyString()))
+ .thenThrow(NameNotFoundException())
whenever(mockUserContext.packageManager).thenReturn(mockPackageManager)
whenever(mockUserContext.getSystemService(INPUT_SERVICE)).thenReturn(inputManager)
}
@@ -278,11 +283,11 @@ class ShortcutHelperViewModelTest : SysuiTestCase() {
fun shortcutsUiState_currentAppIsLauncher_defaultSelectedCategoryIsSystem() =
testScope.runTest {
whenever(
- mockRoleManager.getRoleHoldersAsUser(
- RoleManager.ROLE_HOME,
- fakeUserTracker.userHandle,
+ mockRoleManager.getRoleHoldersAsUser(
+ RoleManager.ROLE_HOME,
+ fakeUserTracker.userHandle,
+ )
)
- )
.thenReturn(listOf(TestShortcuts.currentAppPackageName))
val uiState by collectLastValue(viewModel.shortcutsUiState)
@@ -318,23 +323,23 @@ class ShortcutHelperViewModelTest : SysuiTestCase() {
label = "System",
iconSource = IconSource(imageVector = Icons.Default.Tv),
shortcutCategory =
- ShortcutCategory(
- System,
- subCategoryWithShortcutLabels("first Foo shortcut1"),
- subCategoryWithShortcutLabels(
- "second foO shortcut2",
- subCategoryLabel = SECOND_SIMPLE_GROUP_LABEL,
+ ShortcutCategory(
+ System,
+ subCategoryWithShortcutLabels("first Foo shortcut1"),
+ subCategoryWithShortcutLabels(
+ "second foO shortcut2",
+ subCategoryLabel = SECOND_SIMPLE_GROUP_LABEL,
+ ),
),
- ),
),
ShortcutCategoryUi(
label = "Multitasking",
iconSource = IconSource(imageVector = Icons.Default.VerticalSplit),
shortcutCategory =
- ShortcutCategory(
- MultiTasking,
- subCategoryWithShortcutLabels("third FoO shortcut1"),
- ),
+ ShortcutCategory(
+ MultiTasking,
+ subCategoryWithShortcutLabels("third FoO shortcut1"),
+ ),
),
)
}
@@ -420,9 +425,8 @@ class ShortcutHelperViewModelTest : SysuiTestCase() {
@Test
fun shortcutsUiState_shouldShowResetButton_isTrueWhenThereAreCustomShortcuts() =
testScope.runTest {
- whenever(
- inputManager.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY)
- ).thenReturn(listOf(allAppsInputGestureData))
+ whenever(inputManager.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY))
+ .thenReturn(listOf(allAppsInputGestureData))
val uiState by collectLastValue(viewModel.shortcutsUiState)
testHelper.showFromActivity()
@@ -433,7 +437,7 @@ class ShortcutHelperViewModelTest : SysuiTestCase() {
@Test
fun shortcutsUiState_searchQuery_isResetAfterHelperIsClosedAndReOpened() =
- testScope.runTest{
+ testScope.runTest {
val uiState by collectLastValue(viewModel.shortcutsUiState)
openHelperAndSearchForFooString()
@@ -443,7 +447,7 @@ class ShortcutHelperViewModelTest : SysuiTestCase() {
assertThat((uiState as? ShortcutsUiState.Active)?.searchQuery).isEqualTo("")
}
- private fun openHelperAndSearchForFooString(){
+ private fun openHelperAndSearchForFooString() {
testHelper.showFromActivity()
viewModel.onSearchQueryChanged("foo")
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfigTest.kt
deleted file mode 100644
index ac06a3b9293c..000000000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfigTest.kt
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.keyguard.data.quickaffordance
-
-import android.platform.test.annotations.DisableFlags
-import android.platform.test.annotations.EnableFlags
-import android.platform.test.flag.junit.FlagsParameterization
-import androidx.test.filters.SmallTest
-import com.android.systemui.Flags
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.communal.data.repository.communalSceneRepository
-import com.android.systemui.communal.domain.interactor.setCommunalV2Available
-import com.android.systemui.communal.domain.interactor.setCommunalV2Enabled
-import com.android.systemui.communal.shared.model.CommunalScenes
-import com.android.systemui.flags.parameterizeSceneContainerFlag
-import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.collectLastValue
-import com.android.systemui.kosmos.runCurrent
-import com.android.systemui.kosmos.runTest
-import com.android.systemui.scene.data.repository.sceneContainerRepository
-import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.testKosmos
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.MockitoAnnotations
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4
-import platform.test.runner.parameterized.Parameters
-
-@SmallTest
-@EnableFlags(Flags.FLAG_GLANCEABLE_HUB_V2)
-@RunWith(ParameterizedAndroidJunit4::class)
-class GlanceableHubQuickAffordanceConfigTest(flags: FlagsParameterization?) : SysuiTestCase() {
- private val kosmos = testKosmos()
- private val Kosmos.underTest by Kosmos.Fixture { glanceableHubQuickAffordanceConfig }
-
- init {
- mSetFlagsRule.setFlagsParameterization(flags!!)
- }
-
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
-
- // Access the class immediately so that flows are instantiated.
- // GlanceableHubQuickAffordanceConfig accesses StateFlow.value directly so we need the flows
- // to start flowing before runCurrent is called in the tests.
- kosmos.underTest
- }
-
- @Test
- fun lockscreenState_whenGlanceableHubEnabled_returnsVisible() =
- kosmos.runTest {
- kosmos.setCommunalV2Available(true)
- runCurrent()
-
- val lockScreenState by collectLastValue(underTest.lockScreenState)
-
- assertThat(lockScreenState)
- .isInstanceOf(KeyguardQuickAffordanceConfig.LockScreenState.Visible::class.java)
- }
-
- @Test
- fun lockscreenState_whenGlanceableHubDisabled_returnsHidden() =
- kosmos.runTest {
- setCommunalV2Enabled(false)
- val lockScreenState by collectLastValue(underTest.lockScreenState)
- runCurrent()
-
- assertThat(lockScreenState)
- .isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
- }
-
- @Test
- fun lockscreenState_whenGlanceableHubNotAvailable_returnsHidden() =
- kosmos.runTest {
- // Hub is enabled, but not available.
- setCommunalV2Enabled(true)
- fakeKeyguardRepository.setKeyguardShowing(false)
- val lockScreenState by collectLastValue(underTest.lockScreenState)
- runCurrent()
-
- assertThat(lockScreenState)
- .isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
- }
-
- @Test
- fun pickerScreenState_whenGlanceableHubEnabled_returnsDefault() =
- kosmos.runTest {
- setCommunalV2Enabled(true)
- runCurrent()
-
- assertThat(underTest.getPickerScreenState())
- .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.Default())
- }
-
- @Test
- fun pickerScreenState_whenGlanceableHubDisabled_returnsDisabled() =
- kosmos.runTest {
- setCommunalV2Enabled(false)
- runCurrent()
-
- assertThat(
- underTest.getPickerScreenState()
- is KeyguardQuickAffordanceConfig.PickerScreenState.Disabled
- )
- }
-
- @Test
- @DisableFlags(Flags.FLAG_SCENE_CONTAINER)
- fun onTriggered_changesSceneToCommunal() =
- kosmos.runTest {
- underTest.onTriggered(expandable = null)
- runCurrent()
-
- assertThat(kosmos.communalSceneRepository.currentScene.value)
- .isEqualTo(CommunalScenes.Communal)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_SCENE_CONTAINER)
- fun testTransitionToGlanceableHub_sceneContainer() =
- kosmos.runTest {
- underTest.onTriggered(expandable = null)
- runCurrent()
-
- assertThat(kosmos.sceneContainerRepository.currentScene.value)
- .isEqualTo(Scenes.Communal)
- }
-
- companion object {
- @JvmStatic
- @Parameters(name = "{0}")
- fun getParams(): List<FlagsParameterization> {
- return parameterizeSceneContainerFlag()
- }
- }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt
index 83b821619659..286f8bfd63a2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt
@@ -123,8 +123,8 @@ class AlternateBouncerToPrimaryBouncerTransitionViewModelTest : SysuiTestCase()
kosmos.keyguardWindowBlurTestUtil.assertTransitionToBlurRadius(
transitionProgress = listOf(0f, 0f, 0.1f, 0.2f, 0.3f, 1f),
- startValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f,
- endValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f,
+ startValue = kosmos.blurConfig.maxBlurRadiusPx,
+ endValue = kosmos.blurConfig.maxBlurRadiusPx,
transitionFactory = ::step,
actualValuesProvider = { values },
checkInterpolatedValues = false,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelTest.kt
new file mode 100644
index 000000000000..38829da69c28
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelTest.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.data.repository.keyguardRepository
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.media.controls.data.repository.mediaFilterRepository
+import com.android.systemui.media.controls.shared.model.MediaData
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class KeyguardMediaViewModelTest : SysuiTestCase() {
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+
+ private val underTest = kosmos.keyguardMediaViewModelFactory.create()
+
+ @Before
+ fun setUp() {
+ underTest.activateIn(kosmos.testScope)
+ }
+
+ @Test
+ fun onDozing_noActiveMedia_mediaIsHidden() =
+ kosmos.runTest {
+ keyguardRepository.setIsDozing(true)
+
+ assertThat(underTest.isMediaVisible).isFalse()
+ }
+
+ @Test
+ fun onDozing_activeMediaExists_mediaIsHidden() =
+ kosmos.runTest {
+ val userMedia = MediaData(active = true)
+
+ mediaFilterRepository.addSelectedUserMediaEntry(userMedia)
+ keyguardRepository.setIsDozing(true)
+
+ assertThat(underTest.isMediaVisible).isFalse()
+ }
+
+ @Test
+ fun onDeviceAwake_activeMediaExists_mediaIsVisible() =
+ kosmos.runTest {
+ val userMedia = MediaData(active = true)
+
+ mediaFilterRepository.addSelectedUserMediaEntry(userMedia)
+ keyguardRepository.setIsDozing(false)
+
+ assertThat(underTest.isMediaVisible).isTrue()
+ }
+
+ @Test
+ fun onDeviceAwake_noActiveMedia_mediaIsHidden() =
+ kosmos.runTest {
+ keyguardRepository.setIsDozing(false)
+
+ assertThat(underTest.isMediaVisible).isFalse()
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt
index fdee8d0544f0..60a19a4c7d07 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt
@@ -198,8 +198,8 @@ class LockscreenToPrimaryBouncerTransitionViewModelTest(flags: FlagsParameteriza
kosmos.keyguardWindowBlurTestUtil.assertTransitionToBlurRadius(
transitionProgress = listOf(0f, 0f, 0.1f, 0.2f, 0.3f, 1f),
- startValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f,
- endValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f,
+ startValue = kosmos.blurConfig.maxBlurRadiusPx,
+ endValue = kosmos.blurConfig.maxBlurRadiusPx,
transitionFactory = ::step,
actualValuesProvider = { values },
checkInterpolatedValues = false,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java
index 7f9313cbeb5b..17a1c27b029b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java
@@ -16,9 +16,9 @@
package com.android.systemui.navigationbar.views;
-import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT;
-import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN;
-import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN;
+import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_DISMISS_IME;
+import static android.app.StatusBarManager.NAVIGATION_HINT_IME_VISIBLE;
+import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE;
import static android.inputmethodservice.InputMethodService.BACK_DISPOSITION_ADJUST_NOTHING;
import static android.inputmethodservice.InputMethodService.BACK_DISPOSITION_DEFAULT;
import static android.inputmethodservice.InputMethodService.IME_VISIBLE;
@@ -31,8 +31,9 @@ import static com.android.systemui.assist.AssistManager.INVOCATION_TYPE_HOME_BUT
import static com.android.systemui.navigationbar.views.NavigationBar.NavBarActionEvent.NAVBAR_ASSIST_LONGPRESS;
import static com.android.systemui.navigationbar.views.buttons.KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_LONGPRESS;
import static com.android.systemui.navigationbar.views.buttons.KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_TAP;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BACK_DISMISS_IME;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_VISIBLE;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
import static com.google.common.truth.Truth.assertThat;
@@ -500,8 +501,9 @@ public class NavigationBarTest extends SysuiTestCase {
mNavigationBar.setImeWindowStatus(DEFAULT_DISPLAY, IME_VISIBLE,
BACK_DISPOSITION_DEFAULT, true /* showImeSwitcher */);
- verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_SHOWING), eq(true));
- verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_SWITCHER_SHOWING), eq(true));
+ verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_VISIBLE), eq(true));
+ verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE), eq(true));
+ verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_BACK_DISMISS_IME), eq(true));
}
/**
@@ -514,8 +516,9 @@ public class NavigationBarTest extends SysuiTestCase {
mNavigationBar.setImeWindowStatus(DEFAULT_DISPLAY, IME_VISIBLE,
BACK_DISPOSITION_DEFAULT, false /* showImeSwitcher */);
- verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_SHOWING), eq(true));
- verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_SWITCHER_SHOWING), eq(false));
+ verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_VISIBLE), eq(true));
+ verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE), eq(false));
+ verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_BACK_DISMISS_IME), eq(true));
}
/**
@@ -531,8 +534,9 @@ public class NavigationBarTest extends SysuiTestCase {
mNavigationBar.setImeWindowStatus(DEFAULT_DISPLAY, 0 /* vis */,
BACK_DISPOSITION_DEFAULT, true /* showImeSwitcher */);
- verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_SHOWING), eq(false));
- verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_SWITCHER_SHOWING), eq(false));
+ verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_VISIBLE), eq(false));
+ verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE), eq(false));
+ verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_BACK_DISMISS_IME), eq(false));
}
/**
@@ -545,8 +549,9 @@ public class NavigationBarTest extends SysuiTestCase {
mNavigationBar.setImeWindowStatus(DEFAULT_DISPLAY, IME_VISIBLE,
BACK_DISPOSITION_ADJUST_NOTHING, true /* showImeSwitcher */);
- verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_SHOWING), eq(true));
- verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_SWITCHER_SHOWING), eq(true));
+ verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_VISIBLE), eq(true));
+ verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE), eq(true));
+ verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_BACK_DISMISS_IME), eq(false));
}
@Test
@@ -567,26 +572,28 @@ public class NavigationBarTest extends SysuiTestCase {
BACK_DISPOSITION_DEFAULT, true);
// Verify IME window state will be updated in default NavBar & external NavBar state reset.
- assertEquals(NAVIGATION_HINT_BACK_ALT | NAVIGATION_HINT_IME_SHOWN
- | NAVIGATION_HINT_IME_SWITCHER_SHOWN,
+ assertEquals(NAVIGATION_HINT_BACK_DISMISS_IME | NAVIGATION_HINT_IME_VISIBLE
+ | NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE,
defaultNavBar.getNavigationIconHints());
- assertFalse((externalNavBar.getNavigationIconHints() & NAVIGATION_HINT_BACK_ALT) != 0);
- assertFalse((externalNavBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SHOWN) != 0);
- assertFalse((externalNavBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SWITCHER_SHOWN)
- != 0);
+ assertFalse((externalNavBar.getNavigationIconHints()
+ & NAVIGATION_HINT_BACK_DISMISS_IME) != 0);
+ assertFalse((externalNavBar.getNavigationIconHints() & NAVIGATION_HINT_IME_VISIBLE) != 0);
+ assertFalse((externalNavBar.getNavigationIconHints()
+ & NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE) != 0);
externalNavBar.setImeWindowStatus(EXTERNAL_DISPLAY_ID, IME_VISIBLE,
BACK_DISPOSITION_DEFAULT, true);
defaultNavBar.setImeWindowStatus(DEFAULT_DISPLAY, 0 /* vis */,
BACK_DISPOSITION_DEFAULT, false);
// Verify IME window state will be updated in external NavBar & default NavBar state reset.
- assertEquals(NAVIGATION_HINT_BACK_ALT | NAVIGATION_HINT_IME_SHOWN
- | NAVIGATION_HINT_IME_SWITCHER_SHOWN,
+ assertEquals(NAVIGATION_HINT_BACK_DISMISS_IME | NAVIGATION_HINT_IME_VISIBLE
+ | NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE,
externalNavBar.getNavigationIconHints());
- assertFalse((defaultNavBar.getNavigationIconHints() & NAVIGATION_HINT_BACK_ALT) != 0);
- assertFalse((defaultNavBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SHOWN) != 0);
- assertFalse((defaultNavBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SWITCHER_SHOWN)
- != 0);
+ assertFalse((defaultNavBar.getNavigationIconHints()
+ & NAVIGATION_HINT_BACK_DISMISS_IME) != 0);
+ assertFalse((defaultNavBar.getNavigationIconHints() & NAVIGATION_HINT_IME_VISIBLE) != 0);
+ assertFalse((defaultNavBar.getNavigationIconHints()
+ & NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE) != 0);
}
@Test
@@ -602,20 +609,22 @@ public class NavigationBarTest extends SysuiTestCase {
// Verify navbar altered back icon when an app is showing IME
mNavigationBar.setImeWindowStatus(DEFAULT_DISPLAY, IME_VISIBLE,
BACK_DISPOSITION_DEFAULT, true);
- assertTrue((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_BACK_ALT) != 0);
- assertTrue((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SHOWN) != 0);
- assertTrue((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SWITCHER_SHOWN)
- != 0);
+ assertTrue((mNavigationBar.getNavigationIconHints()
+ & NAVIGATION_HINT_BACK_DISMISS_IME) != 0);
+ assertTrue((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_IME_VISIBLE) != 0);
+ assertTrue((mNavigationBar.getNavigationIconHints()
+ & NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE) != 0);
// Verify navbar didn't alter and showing back icon when the keyguard is showing without
// requesting IME insets visible.
doReturn(true).when(mKeyguardStateController).isShowing();
mNavigationBar.setImeWindowStatus(DEFAULT_DISPLAY, IME_VISIBLE,
BACK_DISPOSITION_DEFAULT, true);
- assertFalse((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_BACK_ALT) != 0);
- assertFalse((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SHOWN) != 0);
- assertFalse((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SWITCHER_SHOWN)
- != 0);
+ assertFalse((mNavigationBar.getNavigationIconHints()
+ & NAVIGATION_HINT_BACK_DISMISS_IME) != 0);
+ assertFalse((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_IME_VISIBLE) != 0);
+ assertFalse((mNavigationBar.getNavigationIconHints()
+ & NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE) != 0);
// Verify navbar altered and showing back icon when the keyguard is showing and
// requesting IME insets visible.
@@ -623,10 +632,11 @@ public class NavigationBarTest extends SysuiTestCase {
doReturn(windowInsets).when(mockShadeWindowView).getRootWindowInsets();
mNavigationBar.setImeWindowStatus(DEFAULT_DISPLAY, IME_VISIBLE,
BACK_DISPOSITION_DEFAULT, true);
- assertTrue((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_BACK_ALT) != 0);
- assertTrue((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SHOWN) != 0);
- assertTrue((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SWITCHER_SHOWN)
- != 0);
+ assertTrue((mNavigationBar.getNavigationIconHints()
+ & NAVIGATION_HINT_BACK_DISMISS_IME) != 0);
+ assertTrue((mNavigationBar.getNavigationIconHints() & NAVIGATION_HINT_IME_VISIBLE) != 0);
+ assertTrue((mNavigationBar.getNavigationIconHints()
+ & NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE) != 0);
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModelTest.kt
deleted file mode 100644
index 46b02e92a4f9..000000000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModelTest.kt
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.notifications.ui.viewmodel
-
-import android.testing.TestableLooper
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.compose.animation.scene.Swipe
-import com.android.compose.animation.scene.UserActionResult
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
-import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
-import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
-import com.android.systemui.flags.EnableSceneContainer
-import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
-import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor
-import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
-import com.android.systemui.kosmos.testScope
-import com.android.systemui.lifecycle.activateIn
-import com.android.systemui.scene.domain.interactor.sceneInteractor
-import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver
-import com.android.systemui.scene.shared.model.SceneFamilies
-import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.shade.ui.viewmodel.notificationsShadeUserActionsViewModel
-import com.android.systemui.testKosmos
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-@TestableLooper.RunWithLooper
-@EnableSceneContainer
-class NotificationsShadeUserActionsViewModelTest : SysuiTestCase() {
-
- private val kosmos = testKosmos()
- private val testScope = kosmos.testScope
- private val sceneInteractor by lazy { kosmos.sceneInteractor }
- private val deviceUnlockedInteractor by lazy { kosmos.deviceUnlockedInteractor }
-
- private val underTest by lazy { kosmos.notificationsShadeUserActionsViewModel }
-
- @Test
- fun upTransitionSceneKey_deviceLocked_lockscreen() =
- testScope.runTest {
- val actions by collectLastValue(underTest.actions)
- lockDevice()
- underTest.activateIn(this)
-
- assertThat((actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene)
- .isEqualTo(SceneFamilies.Home)
- assertThat(actions?.get(Swipe.Down)).isNull()
- assertThat(kosmos.homeSceneFamilyResolver.resolvedScene.value)
- .isEqualTo(Scenes.Lockscreen)
- }
-
- @Test
- fun upTransitionSceneKey_deviceLocked_keyguardDisabled_gone() =
- testScope.runTest {
- val actions by collectLastValue(underTest.actions)
- lockDevice()
- kosmos.keyguardEnabledInteractor.notifyKeyguardEnabled(false)
- underTest.activateIn(this)
-
- assertThat((actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene)
- .isEqualTo(SceneFamilies.Home)
- assertThat(kosmos.homeSceneFamilyResolver.resolvedScene.value).isEqualTo(Scenes.Gone)
- }
-
- @Test
- fun upTransitionSceneKey_deviceUnlocked_gone() =
- testScope.runTest {
- val actions by collectLastValue(underTest.actions)
- lockDevice()
- unlockDevice()
- underTest.activateIn(this)
-
- assertThat((actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene)
- .isEqualTo(SceneFamilies.Home)
- assertThat(actions?.get(Swipe.Down)).isNull()
- assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Gone)
- }
-
- @Test
- fun upTransitionSceneKey_authMethodSwipe_lockscreenNotDismissed_goesToLockscreen() =
- testScope.runTest {
- val actions by collectLastValue(underTest.actions)
- kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true)
- kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
- AuthenticationMethodModel.None
- )
- sceneInteractor.changeScene(Scenes.Lockscreen, "reason")
- underTest.activateIn(this)
-
- assertThat((actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene)
- .isEqualTo(SceneFamilies.Home)
- assertThat(kosmos.homeSceneFamilyResolver.resolvedScene.value)
- .isEqualTo(Scenes.Lockscreen)
- }
-
- @Test
- fun upTransitionSceneKey_authMethodSwipe_lockscreenDismissed_goesToGone() =
- testScope.runTest {
- val deviceUnlockStatus by collectLastValue(deviceUnlockedInteractor.deviceUnlockStatus)
- val actions by collectLastValue(underTest.actions)
- kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true)
- kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
- AuthenticationMethodModel.None
- )
- assertThat(deviceUnlockStatus?.isUnlocked).isTrue()
- sceneInteractor // force the lazy; this will kick off StateFlows
- runCurrent()
- sceneInteractor.changeScene(Scenes.Gone, "reason")
- underTest.activateIn(this)
-
- assertThat((actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene)
- .isEqualTo(SceneFamilies.Home)
- assertThat(kosmos.homeSceneFamilyResolver.resolvedScene.value).isEqualTo(Scenes.Gone)
- }
-
- private fun TestScope.lockDevice() {
- val deviceUnlockStatus by collectLastValue(deviceUnlockedInteractor.deviceUnlockStatus)
-
- kosmos.fakeAuthenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
- assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
- sceneInteractor.changeScene(Scenes.Lockscreen, "reason")
- runCurrent()
- }
-
- private fun TestScope.unlockDevice() {
- val deviceUnlockStatus by collectLastValue(deviceUnlockedInteractor.deviceUnlockStatus)
-
- kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
- SuccessFingerprintAuthenticationStatus(0, true)
- )
- assertThat(deviceUnlockStatus?.isUnlocked).isTrue()
- sceneInteractor.changeScene(Scenes.Gone, "reason")
- runCurrent()
- }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ModesTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ModesTileTest.kt
index f005375a2ef9..7bb28dbabd5e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ModesTileTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ModesTileTest.kt
@@ -49,6 +49,7 @@ import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate
import com.android.systemui.statusbar.policy.ui.dialog.modesDialogEventLogger
+import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.modesDialogViewModel
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
import com.android.systemui.util.settings.FakeSettings
@@ -146,6 +147,7 @@ class ModesTileTest : SysuiTestCase() {
tileDataInteractor,
mapper,
userActionInteractor,
+ kosmos.modesDialogViewModel,
)
underTest.initialize()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
index 27e9f07af168..3d5daf6cf9c2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
@@ -318,7 +318,7 @@ class TakeScreenshotExecutorTest : SysuiTestCase() {
val displayId = 1
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = displayId))
val onSaved = { _: Uri? -> }
- focusedDisplayRepository.emit(displayId)
+ focusedDisplayRepository.setDisplayId(displayId)
screenshotExecutor.executeScreenshots(
createScreenshotRequest(
@@ -345,7 +345,7 @@ class TakeScreenshotExecutorTest : SysuiTestCase() {
display(TYPE_INTERNAL, id = Display.DEFAULT_DISPLAY),
display(TYPE_EXTERNAL, id = 1),
)
- focusedDisplayRepository.emit(5) // invalid display
+ focusedDisplayRepository.setDisplayId(5) // invalid display
val onSaved = { _: Uri? -> }
screenshotExecutor.executeScreenshots(
createScreenshotRequest(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
index ee9cb141a700..555c717e1e65 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
@@ -20,7 +20,7 @@ import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.testing.TestableLooper.RunWithLooper
import android.view.Choreographer
-import android.view.MotionEvent
+import android.view.accessibility.AccessibilityEvent
import android.widget.FrameLayout
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -45,7 +45,10 @@ import com.android.systemui.res.R
import com.android.systemui.settings.brightness.data.repository.BrightnessMirrorShowingRepository
import com.android.systemui.settings.brightness.domain.interactor.BrightnessMirrorShowingInteractor
import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler
+import com.android.systemui.shade.data.repository.ShadeAnimationRepository
+import com.android.systemui.shade.data.repository.ShadeRepositoryImpl
import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor
+import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractorLegacyImpl
import com.android.systemui.statusbar.BlurUtils
import com.android.systemui.statusbar.DragDownHelper
import com.android.systemui.statusbar.LockscreenShadeTransitionController
@@ -185,6 +188,10 @@ class NotificationShadeWindowViewTest : SysuiTestCase() {
notificationShadeDepthController,
underTest,
shadeViewController,
+ ShadeAnimationInteractorLegacyImpl(
+ ShadeAnimationRepository(),
+ ShadeRepositoryImpl(testScope),
+ ),
panelExpansionInteractor,
ShadeExpansionStateManager(),
notificationStackScrollLayoutController,
@@ -259,6 +266,20 @@ class NotificationShadeWindowViewTest : SysuiTestCase() {
verify(configurationForwarder).dispatchOnMovedToDisplay(eq(1), eq(config))
}
+ @Test
+ @EnableFlags(AConfigFlags.FLAG_SHADE_LAUNCH_ACCESSIBILITY)
+ fun requestSendAccessibilityEvent_duringLaunchAnimation_blocksFocusEvent() {
+ underTest.setAnimatingContentLaunch(true)
+
+ assertThat(
+ underTest.requestSendAccessibilityEvent(
+ underTest.getChildAt(0),
+ AccessibilityEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED),
+ )
+ )
+ .isFalse()
+ }
+
private fun captureInteractionEventHandler() {
verify(underTest).setInteractionEventHandler(interactionEventHandlerCaptor.capture())
interactionEventHandler = interactionEventHandlerCaptor.value
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QsBatteryModeControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QsBatteryModeControllerTest.kt
index ab5fa8ef43fb..5566c10dc9e9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QsBatteryModeControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QsBatteryModeControllerTest.kt
@@ -38,7 +38,7 @@ class QsBatteryModeControllerTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val insetsProviderStore = kosmos.fakeStatusBarContentInsetsProviderStore
- private val insetsProvider = insetsProviderStore.defaultDisplay
+ private val insetsProvider = insetsProviderStore.forDisplay(context.displayId)
@JvmField @Rule val mockitoRule = MockitoJUnit.rule()!!
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt
index 007a0fb87953..4f332d4bbed8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt
@@ -17,15 +17,21 @@
package com.android.systemui.shade.data.repository
import android.provider.Settings.Global.DEVELOPMENT_SHADE_DISPLAY_AWARENESS
+import android.view.Display
+import android.view.Display.TYPE_EXTERNAL
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
+import com.android.systemui.display.data.repository.display
import com.android.systemui.display.data.repository.displayRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.shade.display.AnyExternalShadeDisplayPolicy
import com.android.systemui.shade.display.DefaultDisplayShadePolicy
+import com.android.systemui.shade.display.FakeShadeDisplayPolicy
import com.android.systemui.shade.display.StatusBarTouchShadeDisplayPolicy
import com.android.systemui.testKosmos
import com.android.systemui.util.settings.fakeGlobalSettings
@@ -37,24 +43,28 @@ import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidJUnit4::class)
class ShadeDisplaysRepositoryTest : SysuiTestCase() {
- private val kosmos = testKosmos()
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
private val testScope = kosmos.testScope
private val globalSettings = kosmos.fakeGlobalSettings
private val displayRepository = kosmos.displayRepository
private val defaultPolicy = DefaultDisplayShadePolicy()
private val policies = kosmos.shadeDisplayPolicies
+ private val keyguardRepository = kosmos.fakeKeyguardRepository
- private val underTest =
+ private fun createUnderTest(shadeOnDefaultDisplayWhenLocked: Boolean = false) =
ShadeDisplaysRepositoryImpl(
globalSettings,
defaultPolicy,
testScope.backgroundScope,
policies,
+ shadeOnDefaultDisplayWhenLocked = shadeOnDefaultDisplayWhenLocked,
+ keyguardRepository,
)
@Test
fun policy_changing_propagatedFromTheLatestPolicy() =
testScope.runTest {
+ val underTest = createUnderTest()
val displayIds by collectValues(underTest.displayId)
assertThat(displayIds).containsExactly(0)
@@ -81,30 +91,54 @@ class ShadeDisplaysRepositoryTest : SysuiTestCase() {
@Test
fun policy_updatesBasedOnSettingValue_defaultDisplay() =
testScope.runTest {
- val policy by collectLastValue(underTest.policy)
-
+ val underTest = createUnderTest()
globalSettings.putString(DEVELOPMENT_SHADE_DISPLAY_AWARENESS, "default_display")
- assertThat(policy).isInstanceOf(DefaultDisplayShadePolicy::class.java)
+ assertThat(underTest.currentPolicy).isInstanceOf(DefaultDisplayShadePolicy::class.java)
}
@Test
fun policy_updatesBasedOnSettingValue_anyExternal() =
testScope.runTest {
- val policy by collectLastValue(underTest.policy)
-
+ val underTest = createUnderTest()
globalSettings.putString(DEVELOPMENT_SHADE_DISPLAY_AWARENESS, "any_external_display")
- assertThat(policy).isInstanceOf(AnyExternalShadeDisplayPolicy::class.java)
+ assertThat(underTest.currentPolicy)
+ .isInstanceOf(AnyExternalShadeDisplayPolicy::class.java)
}
@Test
fun policy_updatesBasedOnSettingValue_focusBased() =
testScope.runTest {
- val policy by collectLastValue(underTest.policy)
-
+ val underTest = createUnderTest()
globalSettings.putString(DEVELOPMENT_SHADE_DISPLAY_AWARENESS, "status_bar_latest_touch")
- assertThat(policy).isInstanceOf(StatusBarTouchShadeDisplayPolicy::class.java)
+ assertThat(underTest.currentPolicy)
+ .isInstanceOf(StatusBarTouchShadeDisplayPolicy::class.java)
+ }
+
+ @Test
+ fun displayId_afterKeyguardHides_goesBackToPreviousDisplay() =
+ testScope.runTest {
+ val underTest = createUnderTest(shadeOnDefaultDisplayWhenLocked = true)
+ globalSettings.putString(
+ DEVELOPMENT_SHADE_DISPLAY_AWARENESS,
+ FakeShadeDisplayPolicy.name,
+ )
+
+ val displayId by collectLastValue(underTest.displayId)
+
+ displayRepository.addDisplays(display(id = 2, type = TYPE_EXTERNAL))
+ FakeShadeDisplayPolicy.setDisplayId(2)
+
+ assertThat(displayId).isEqualTo(2)
+
+ keyguardRepository.setKeyguardShowing(true)
+
+ assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY)
+
+ keyguardRepository.setKeyguardShowing(false)
+
+ assertThat(displayId).isEqualTo(2)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt
index eeb3e6b31c69..fd6bc98b006c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt
@@ -25,7 +25,7 @@ import com.android.systemui.display.data.repository.displayRepository
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.shade.ShadePrimaryDisplayCommand
-import com.android.systemui.shade.display.ShadeDisplayPolicy
+import com.android.systemui.shade.display.FakeShadeDisplayPolicy
import com.android.systemui.statusbar.commandline.commandRegistry
import com.android.systemui.testKosmos
import com.android.systemui.util.settings.fakeGlobalSettings
@@ -33,8 +33,6 @@ import com.google.common.truth.StringSubject
import com.google.common.truth.Truth.assertThat
import java.io.PrintWriter
import java.io.StringWriter
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
@@ -118,23 +116,12 @@ class ShadePrimaryDisplayCommandTest : SysuiTestCase() {
@Test
fun policies_setsNewPolicy() =
testScope.runTest {
- val policy by collectLastValue(shadeDisplaysRepository.policy)
- val newPolicy = policies.last().name
+ val newPolicy = FakeShadeDisplayPolicy.name
commandRegistry.onShellCommand(pw, arrayOf("shade_display_override", newPolicy))
- assertThat(policy!!.name).isEqualTo(newPolicy)
+ assertThat(shadeDisplaysRepository.currentPolicy.name).isEqualTo(newPolicy)
}
-
- private fun makePolicy(policyName: String): ShadeDisplayPolicy {
- return object : ShadeDisplayPolicy {
- override val name: String
- get() = policyName
-
- override val displayId: StateFlow<Int>
- get() = MutableStateFlow(0)
- }
- }
}
private fun StringSubject.containsAllIn(strings: List<String>) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/display/FocusShadeDisplayPolicyTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/display/FocusShadeDisplayPolicyTest.kt
new file mode 100644
index 000000000000..b4249ef72e62
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/display/FocusShadeDisplayPolicyTest.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade.display
+
+import android.view.Display
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.shade.data.repository.fakeFocusedDisplayRepository
+import com.android.systemui.shade.data.repository.focusShadeDisplayPolicy
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.test.runTest
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FocusShadeDisplayPolicyTest : SysuiTestCase() {
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+ private val testScope = kosmos.testScope
+ private val focusedDisplayRepository = kosmos.fakeFocusedDisplayRepository
+
+ private val underTest = kosmos.focusShadeDisplayPolicy
+
+ @Test
+ fun displayId_propagatedFromRepository() =
+ testScope.runTest {
+ val displayId by collectLastValue(underTest.displayId)
+
+ assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY)
+
+ focusedDisplayRepository.setDisplayId(2)
+
+ assertThat(displayId).isEqualTo(2)
+
+ focusedDisplayRepository.setDisplayId(3)
+
+ assertThat(displayId).isEqualTo(3)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicyTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicyTest.kt
index 20dfd3e11947..e43c46b36a06 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicyTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicyTest.kt
@@ -26,12 +26,11 @@ import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.display.data.repository.display
import com.android.systemui.display.data.repository.displayRepository
-import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.shade.data.repository.statusBarTouchShadeDisplayPolicy
import com.android.systemui.shade.domain.interactor.notificationElement
import com.android.systemui.shade.domain.interactor.qsElement
-import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
@@ -45,22 +44,9 @@ import org.mockito.kotlin.mock
class StatusBarTouchShadeDisplayPolicyTest : SysuiTestCase() {
private val kosmos = testKosmos().useUnconfinedTestDispatcher()
private val testScope = kosmos.testScope
- private val keyguardRepository = kosmos.fakeKeyguardRepository
private val displayRepository = kosmos.displayRepository
- private fun createUnderTest(
- shadeOnDefaultDisplayWhenLocked: Boolean = false
- ): StatusBarTouchShadeDisplayPolicy {
- return StatusBarTouchShadeDisplayPolicy(
- displayRepository,
- keyguardRepository,
- testScope.backgroundScope,
- shadeOnDefaultDisplayWhenLocked = shadeOnDefaultDisplayWhenLocked,
- shadeInteractor = { kosmos.shadeInteractor },
- { kosmos.qsElement },
- { kosmos.notificationElement },
- )
- }
+ private val underTest = kosmos.statusBarTouchShadeDisplayPolicy
private fun createMotionEventForDisplay(displayId: Int, xCoordinate: Float = 0f): MotionEvent {
return mock<MotionEvent> {
@@ -71,15 +57,12 @@ class StatusBarTouchShadeDisplayPolicyTest : SysuiTestCase() {
@Test
fun displayId_defaultToDefaultDisplay() {
- val underTest = createUnderTest()
-
assertThat(underTest.displayId.value).isEqualTo(Display.DEFAULT_DISPLAY)
}
@Test
fun onStatusBarTouched_called_updatesDisplayId() =
testScope.runTest {
- val underTest = createUnderTest()
val displayId by collectLastValue(underTest.displayId)
displayRepository.addDisplays(display(id = 2, type = TYPE_EXTERNAL))
@@ -91,7 +74,6 @@ class StatusBarTouchShadeDisplayPolicyTest : SysuiTestCase() {
@Test
fun onStatusBarTouched_notExistentDisplay_displayIdNotUpdated() =
testScope.runTest {
- val underTest = createUnderTest()
val displayIds by collectValues(underTest.displayId)
assertThat(displayIds).isEqualTo(listOf(Display.DEFAULT_DISPLAY))
@@ -104,7 +86,6 @@ class StatusBarTouchShadeDisplayPolicyTest : SysuiTestCase() {
@Test
fun onStatusBarTouched_afterDisplayRemoved_goesBackToDefaultDisplay() =
testScope.runTest {
- val underTest = createUnderTest()
val displayId by collectLastValue(underTest.displayId)
displayRepository.addDisplays(display(id = 2, type = TYPE_EXTERNAL))
@@ -118,46 +99,8 @@ class StatusBarTouchShadeDisplayPolicyTest : SysuiTestCase() {
}
@Test
- fun onStatusBarTouched_afterKeyguardVisible_goesBackToDefaultDisplay() =
- testScope.runTest {
- val underTest = createUnderTest(shadeOnDefaultDisplayWhenLocked = true)
- val displayId by collectLastValue(underTest.displayId)
-
- displayRepository.addDisplays(display(id = 2, type = TYPE_EXTERNAL))
- underTest.onStatusBarTouched(createMotionEventForDisplay(2), STATUS_BAR_WIDTH)
-
- assertThat(displayId).isEqualTo(2)
-
- keyguardRepository.setKeyguardShowing(true)
-
- assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY)
- }
-
- @Test
- fun onStatusBarTouched_afterKeyguardHides_goesBackToPreviousDisplay() =
- testScope.runTest {
- val underTest = createUnderTest(shadeOnDefaultDisplayWhenLocked = true)
- val displayId by collectLastValue(underTest.displayId)
-
- displayRepository.addDisplays(display(id = 2, type = TYPE_EXTERNAL))
- underTest.onStatusBarTouched(createMotionEventForDisplay(2), STATUS_BAR_WIDTH)
-
- assertThat(displayId).isEqualTo(2)
-
- keyguardRepository.setKeyguardShowing(true)
-
- assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY)
-
- keyguardRepository.setKeyguardShowing(false)
-
- assertThat(displayId).isEqualTo(2)
- }
-
- @Test
fun onStatusBarTouched_leftSide_intentSetToNotifications() =
testScope.runTest {
- val underTest = createUnderTest(shadeOnDefaultDisplayWhenLocked = true)
-
underTest.onStatusBarTouched(
createMotionEventForDisplay(2, STATUS_BAR_WIDTH * 0.1f),
STATUS_BAR_WIDTH,
@@ -169,8 +112,6 @@ class StatusBarTouchShadeDisplayPolicyTest : SysuiTestCase() {
@Test
fun onStatusBarTouched_rightSide_intentSetToQs() =
testScope.runTest {
- val underTest = createUnderTest(shadeOnDefaultDisplayWhenLocked = true)
-
underTest.onStatusBarTouched(
createMotionEventForDisplay(2, STATUS_BAR_WIDTH * 0.95f),
STATUS_BAR_WIDTH,
@@ -182,8 +123,6 @@ class StatusBarTouchShadeDisplayPolicyTest : SysuiTestCase() {
@Test
fun onStatusBarTouched_nullAfterConsumed() =
testScope.runTest {
- val underTest = createUnderTest(shadeOnDefaultDisplayWhenLocked = true)
-
underTest.onStatusBarTouched(
createMotionEventForDisplay(2, STATUS_BAR_WIDTH * 0.1f),
STATUS_BAR_WIDTH,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt
index a47db2ec728b..668f568d7f46 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt
@@ -16,15 +16,11 @@
package com.android.systemui.shade.domain.interactor
-import android.platform.test.annotations.DisableFlags
-import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
-import com.android.systemui.shade.data.repository.shadeRepository
-import com.android.systemui.shade.shared.flag.DualShade
import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
@@ -48,82 +44,79 @@ class ShadeModeInteractorImplTest : SysuiTestCase() {
}
@Test
- @DisableFlags(DualShade.FLAG_NAME)
fun legacyShadeMode_narrowScreen_singleShade() =
testScope.runTest {
val shadeMode by collectLastValue(underTest.shadeMode)
- kosmos.shadeRepository.setShadeLayoutWide(false)
+ kosmos.enableSingleShade()
assertThat(shadeMode).isEqualTo(ShadeMode.Single)
}
@Test
- @DisableFlags(DualShade.FLAG_NAME)
fun legacyShadeMode_wideScreen_splitShade() =
testScope.runTest {
val shadeMode by collectLastValue(underTest.shadeMode)
- kosmos.shadeRepository.setShadeLayoutWide(true)
+ kosmos.enableSplitShade()
assertThat(shadeMode).isEqualTo(ShadeMode.Split)
}
@Test
- @EnableFlags(DualShade.FLAG_NAME)
fun shadeMode_wideScreen_isDual() =
testScope.runTest {
val shadeMode by collectLastValue(underTest.shadeMode)
- kosmos.shadeRepository.setShadeLayoutWide(true)
+ kosmos.enableDualShade(wideLayout = true)
assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
}
@Test
- @EnableFlags(DualShade.FLAG_NAME)
fun shadeMode_narrowScreen_isDual() =
testScope.runTest {
val shadeMode by collectLastValue(underTest.shadeMode)
- kosmos.shadeRepository.setShadeLayoutWide(false)
+ kosmos.enableDualShade(wideLayout = false)
assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
}
@Test
- @EnableFlags(DualShade.FLAG_NAME)
- fun isDualShade_flagEnabled_true() =
+ fun isDualShade_settingEnabled_returnsTrue() =
testScope.runTest {
- // Initiate collection.
+ // TODO(b/391578667): Add a test case for user switching once the bug is fixed.
val shadeMode by collectLastValue(underTest.shadeMode)
+ kosmos.enableDualShade()
+ assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
assertThat(underTest.isDualShade).isTrue()
}
@Test
- @DisableFlags(DualShade.FLAG_NAME)
- fun isDualShade_flagDisabled_false() =
+ fun isDualShade_settingDisabled_returnsFalse() =
testScope.runTest {
- // Initiate collection.
val shadeMode by collectLastValue(underTest.shadeMode)
+ kosmos.disableDualShade()
+ assertThat(shadeMode).isNotEqualTo(ShadeMode.Dual)
assertThat(underTest.isDualShade).isFalse()
}
@Test
fun getTopEdgeSplitFraction_narrowScreen_splitInHalf() =
testScope.runTest {
- // Ensure isShadeLayoutWide is collected.
- val isShadeLayoutWide by collectLastValue(underTest.isShadeLayoutWide)
- kosmos.shadeRepository.setShadeLayoutWide(false)
+ val shadeMode by collectLastValue(underTest.shadeMode)
+ kosmos.enableDualShade(wideLayout = false)
+ assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
assertThat(underTest.getTopEdgeSplitFraction()).isEqualTo(0.5f)
}
@Test
fun getTopEdgeSplitFraction_wideScreen_splitInHalf() =
testScope.runTest {
- // Ensure isShadeLayoutWide is collected.
- val isShadeLayoutWide by collectLastValue(underTest.isShadeLayoutWide)
- kosmos.shadeRepository.setShadeLayoutWide(true)
+ val shadeMode by collectLastValue(underTest.shadeMode)
+ kosmos.enableDualShade(wideLayout = true)
+ assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
assertThat(underTest.getTopEdgeSplitFraction()).isEqualTo(0.5f)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
index eec23d3ffb1a..55f3717535b7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
@@ -609,7 +609,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
listOf(
activeNotificationModel(
key = "notif",
- statusBarChipIcon = mock<StatusBarIconView>(),
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
promotedContent = promotedContentBuilder.build(),
)
)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolverTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolverTest.kt
index 22906b8724b5..c515d940d2aa 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolverTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolverTest.kt
@@ -144,7 +144,8 @@ class TargetSdkResolverTest : SysuiTestCase() {
/* rankingAdjustment = */ 0,
/* isBubble = */ false,
/* proposedImportance = */ 0,
- /* sensitiveContent = */ false
+ /* sensitiveContent = */ false,
+ /* summarization = */ null
)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java
index 7b120947b1d6..2aa1efaa429f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java
@@ -107,7 +107,7 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase {
@Parameters(name = "{0}")
public static List<FlagsParameterization> getParams() {
return SceneContainerFlagParameterizationKt
- .andSceneContainer(allCombinationsOf(Flags.FLAG_STABILIZE_HEADS_UP_GROUP));
+ .andSceneContainer(allCombinationsOf(Flags.FLAG_STABILIZE_HEADS_UP_GROUP_V2));
}
private VisualStabilityCoordinator mCoordinator;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt
index 3c772fdbe0b2..356eedbc9a45 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt
@@ -29,6 +29,7 @@ import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE
import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_PUBLIC
+import com.android.systemui.statusbar.RankingBuilder
import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection
@@ -242,4 +243,42 @@ class NotifUiAdjustmentProviderTest : SysuiTestCase() {
// Then: need no re-inflation
assertFalse(NotifUiAdjustment.needReinflate(oldAdjustment, newAdjustment))
}
+
+ @Test
+ @EnableFlags(android.app.Flags.FLAG_NM_SUMMARIZATION_UI)
+ fun changeIsSummarization_needReInflation_newlySummarized() {
+ // Given: an Entry with no summarization
+ val oldAdjustment = adjustmentProvider.calculateAdjustment(entry)
+ assertThat(oldAdjustment.summarization).isNull()
+
+ // When: the Entry now has a summarization
+ val rb = RankingBuilder(entry.ranking)
+ rb.setSummarization("summary!")
+ entry.ranking = rb.build()
+ val newAdjustment = adjustmentProvider.calculateAdjustment(entry)
+ assertThat(newAdjustment).isNotEqualTo(oldAdjustment)
+
+ // Then: Need re-inflation
+ assertTrue(NotifUiAdjustment.needReinflate(oldAdjustment, newAdjustment))
+ }
+
+ @Test
+ @EnableFlags(android.app.Flags.FLAG_NM_SUMMARIZATION_UI)
+ fun changeIsSummarization_needReInflation_summarizationChanged() {
+ // Given: an Entry with no summarization
+ val rb = RankingBuilder(entry.ranking)
+ rb.setSummarization("summary!")
+ entry.ranking = rb.build()
+ val oldAdjustment = adjustmentProvider.calculateAdjustment(entry)
+
+ // When: the Entry now has a new summarization
+ val rb2 = RankingBuilder(entry.ranking)
+ rb2.setSummarization("summary new!")
+ entry.ranking = rb2.build()
+ val newAdjustment = adjustmentProvider.calculateAdjustment(entry)
+ assertThat(newAdjustment).isNotEqualTo(oldAdjustment)
+
+ // Then: Need re-inflation
+ assertTrue(NotifUiAdjustment.needReinflate(oldAdjustment, newAdjustment))
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/BundleNotificationInfoTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/BundleNotificationInfoTest.java
index b2962eeb9001..66277e2d13a6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/BundleNotificationInfoTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/BundleNotificationInfoTest.java
@@ -198,7 +198,8 @@ public class BundleNotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
// and the feedback button is clicked,
final View feedbackButton = mInfo.findViewById(R.id.notification_guts_bundle_feedback);
feedbackButton.performClick();
@@ -253,7 +254,8 @@ public class BundleNotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
final View feedbackButton = mInfo.findViewById(R.id.notification_guts_bundle_feedback);
feedbackButton.performClick();
@@ -294,7 +296,8 @@ public class BundleNotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
final View feedbackButton = mInfo.findViewById(R.id.notification_guts_bundle_feedback);
feedbackButton.performClick();
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.kt
index 6a0a5bb3b191..39c42f183481 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.kt
@@ -544,6 +544,7 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase(
/* wasShownHighPriority = */ eq(true),
eq(assistantFeedbackController),
eq(metricsLogger),
+ any<View.OnClickListener>(),
)
}
@@ -580,6 +581,7 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase(
/* wasShownHighPriority = */ eq(false),
eq(assistantFeedbackController),
eq(metricsLogger),
+ any<View.OnClickListener>(),
)
}
@@ -614,6 +616,7 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase(
/* wasShownHighPriority = */ eq(false),
eq(assistantFeedbackController),
eq(metricsLogger),
+ any<View.OnClickListener>(),
)
}
@@ -651,6 +654,7 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase(
/* wasShownHighPriority = */ eq(false),
eq(assistantFeedbackController),
eq(metricsLogger),
+ any<View.OnClickListener>(),
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java
index 245a6a0b130c..fdba7ba34855 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java
@@ -187,7 +187,7 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger, null);
final TextView textView = mNotificationInfo.findViewById(R.id.pkg_name);
assertTrue(textView.getText().toString().contains("App Name"));
assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.header).getVisibility());
@@ -213,7 +213,7 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger, null);
final ImageView iconView = mNotificationInfo.findViewById(R.id.pkg_icon);
assertEquals(iconDrawable, iconView.getDrawable());
}
@@ -235,7 +235,7 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger, null);
final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name);
assertEquals(GONE, nameView.getVisibility());
}
@@ -266,7 +266,7 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger, null);
final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name);
assertEquals(VISIBLE, nameView.getVisibility());
assertTrue(nameView.getText().toString().contains("Proxied"));
@@ -289,7 +289,7 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger, null);
final TextView groupNameView = mNotificationInfo.findViewById(R.id.group_name);
assertEquals(GONE, groupNameView.getVisibility());
}
@@ -317,7 +317,7 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger, null);
final TextView groupNameView = mNotificationInfo.findViewById(R.id.group_name);
assertEquals(View.VISIBLE, groupNameView.getVisibility());
assertEquals("Test Group Name", groupNameView.getText());
@@ -340,7 +340,7 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger, null);
final TextView textView = mNotificationInfo.findViewById(R.id.channel_name);
assertEquals(TEST_CHANNEL_NAME, textView.getText());
}
@@ -362,7 +362,7 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger, null);
final TextView textView = mNotificationInfo.findViewById(R.id.channel_name);
assertEquals(GONE, textView.getVisibility());
}
@@ -388,7 +388,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
final TextView textView = mNotificationInfo.findViewById(R.id.channel_name);
assertEquals(VISIBLE, textView.getVisibility());
}
@@ -410,7 +411,8 @@ public class NotificationInfoTest extends SysuiTestCase {
true,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
final TextView textView = mNotificationInfo.findViewById(R.id.channel_name);
assertEquals(VISIBLE, textView.getVisibility());
}
@@ -436,7 +438,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
final View settingsButton = mNotificationInfo.findViewById(R.id.info);
settingsButton.performClick();
@@ -461,7 +464,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
final View settingsButton = mNotificationInfo.findViewById(R.id.info);
assertTrue(settingsButton.getVisibility() != View.VISIBLE);
}
@@ -486,7 +490,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
final View settingsButton = mNotificationInfo.findViewById(R.id.info);
assertTrue(settingsButton.getVisibility() != View.VISIBLE);
}
@@ -508,7 +513,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
mNotificationInfo.bindNotification(
mMockPackageManager,
mMockINotificationManager,
@@ -524,7 +530,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
final View settingsButton = mNotificationInfo.findViewById(R.id.info);
assertEquals(View.VISIBLE, settingsButton.getVisibility());
}
@@ -546,7 +553,8 @@ public class NotificationInfoTest extends SysuiTestCase {
true,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
final TextView view = mNotificationInfo.findViewById(R.id.non_configurable_text);
assertEquals(View.VISIBLE, view.getVisibility());
assertEquals(mContext.getString(R.string.notification_unblockable_desc),
@@ -589,7 +597,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
final TextView view = mNotificationInfo.findViewById(R.id.non_configurable_call_text);
assertEquals(View.VISIBLE, view.getVisibility());
assertEquals(mContext.getString(R.string.notification_unblockable_call_desc),
@@ -632,7 +641,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
assertEquals(GONE,
mNotificationInfo.findViewById(R.id.non_configurable_call_text).getVisibility());
assertEquals(VISIBLE,
@@ -659,7 +669,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.automatic).getVisibility());
assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.automatic_summary).getVisibility());
}
@@ -681,7 +692,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
assertEquals(GONE, mNotificationInfo.findViewById(R.id.automatic).getVisibility());
assertEquals(GONE, mNotificationInfo.findViewById(R.id.automatic_summary).getVisibility());
}
@@ -705,7 +717,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
assertTrue(mNotificationInfo.findViewById(R.id.automatic).isSelected());
}
@@ -726,7 +739,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
assertTrue(mNotificationInfo.findViewById(R.id.alert).isSelected());
}
@@ -747,7 +761,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
false,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
assertTrue(mNotificationInfo.findViewById(R.id.silence).isSelected());
}
@@ -768,7 +783,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
mTestableLooper.processAllMessages();
verify(mMockINotificationManager, never()).updateNotificationChannelForPackage(
anyString(), eq(TEST_UID), any());
@@ -791,7 +807,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
assertEquals(1, mUiEventLogger.numLogs());
assertEquals(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.getId(),
mUiEventLogger.eventId(0));
@@ -815,7 +832,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
false,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
mNotificationInfo.findViewById(R.id.alert).performClick();
mTestableLooper.processAllMessages();
@@ -842,7 +860,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
mNotificationInfo.findViewById(R.id.silence).performClick();
mTestableLooper.processAllMessages();
@@ -869,7 +888,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
mNotificationInfo.findViewById(R.id.automatic).performClick();
mTestableLooper.processAllMessages();
@@ -897,7 +917,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
mNotificationInfo.handleCloseControls(true, false);
mTestableLooper.processAllMessages();
@@ -924,7 +945,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
mNotificationInfo.handleCloseControls(true, false);
mTestableLooper.processAllMessages();
@@ -959,7 +981,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
mNotificationInfo.handleCloseControls(true, false);
@@ -987,7 +1010,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
mNotificationInfo.findViewById(R.id.silence).performClick();
mNotificationInfo.findViewById(R.id.done).performClick();
@@ -1028,7 +1052,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
false,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
mNotificationInfo.findViewById(R.id.alert).performClick();
mNotificationInfo.findViewById(R.id.done).performClick();
@@ -1065,7 +1090,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
mNotificationInfo.findViewById(R.id.automatic).performClick();
mNotificationInfo.findViewById(R.id.done).performClick();
@@ -1097,7 +1123,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
mNotificationInfo.findViewById(R.id.silence).performClick();
mNotificationInfo.findViewById(R.id.done).performClick();
@@ -1133,7 +1160,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
false,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
assertEquals(mContext.getString(R.string.inline_done_button),
((TextView) mNotificationInfo.findViewById(R.id.done)).getText());
@@ -1171,7 +1199,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
false,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
mNotificationInfo.findViewById(R.id.silence).performClick();
mNotificationInfo.handleCloseControls(false, false);
@@ -1202,7 +1231,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
false,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
assertEquals(mContext.getString(R.string.inline_done_button),
((TextView) mNotificationInfo.findViewById(R.id.done)).getText());
@@ -1240,7 +1270,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
true,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
mNotificationInfo.findViewById(R.id.silence).performClick();
mNotificationInfo.findViewById(R.id.done).performClick();
@@ -1269,7 +1300,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
false,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
assertEquals(mContext.getString(R.string.inline_done_button),
((TextView) mNotificationInfo.findViewById(R.id.done)).getText());
@@ -1300,7 +1332,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
false,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
mNotificationInfo.findViewById(R.id.alert).performClick();
mNotificationInfo.findViewById(R.id.done).performClick();
@@ -1335,7 +1368,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
false,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
mNotificationInfo.findViewById(R.id.alert).performClick();
mNotificationInfo.findViewById(R.id.done).performClick();
@@ -1368,7 +1402,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
false,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
mNotificationInfo.findViewById(R.id.alert).performClick();
mNotificationInfo.findViewById(R.id.done).performClick();
@@ -1401,7 +1436,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
false,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
mNotificationInfo.findViewById(R.id.alert).performClick();
@@ -1427,7 +1463,8 @@ public class NotificationInfoTest extends SysuiTestCase {
false,
false,
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ null);
assertFalse(mNotificationInfo.willBeRemoved());
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfoTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfoTest.java
new file mode 100644
index 000000000000..b33f93d5b523
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfoTest.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row;
+
+import static android.app.Notification.EXTRA_BUILDER_APPLICATION_INFO;
+import static android.app.NotificationManager.IMPORTANCE_LOW;
+
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.INotificationManager;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.UserHandle;
+import android.platform.test.annotations.EnableFlags;
+import android.service.notification.StatusBarNotification;
+import android.telecom.TelecomManager;
+import android.testing.TestableLooper;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.testing.UiEventLoggerFake;
+import com.android.systemui.Dependency;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.res.R;
+import com.android.systemui.statusbar.notification.AssistantFeedbackController;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.concurrent.CountDownLatch;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@TestableLooper.RunWithLooper
+public class PromotedNotificationInfoTest extends SysuiTestCase {
+ private static final String TEST_PACKAGE_NAME = "test_package";
+ private static final int TEST_UID = 1;
+ private static final String TEST_CHANNEL = "test_channel";
+ private static final String TEST_CHANNEL_NAME = "TEST CHANNEL NAME";
+
+ private TestableLooper mTestableLooper;
+ private PromotedNotificationInfo mInfo;
+ private NotificationChannel mNotificationChannel;
+ private StatusBarNotification mSbn;
+ private NotificationEntry mEntry;
+ private UiEventLoggerFake mUiEventLogger = new UiEventLoggerFake();
+
+ @Rule
+ public MockitoRule mockito = MockitoJUnit.rule();
+ @Mock
+ private MetricsLogger mMetricsLogger;
+ @Mock
+ private INotificationManager mMockINotificationManager;
+ @Mock
+ private PackageManager mMockPackageManager;
+ @Mock
+ private OnUserInteractionCallback mOnUserInteractionCallback;
+ @Mock
+ private ChannelEditorDialogController mChannelEditorDialogController;
+ @Mock
+ private AssistantFeedbackController mAssistantFeedbackController;
+ @Mock
+ private TelecomManager mTelecomManager;
+
+ @Before
+ public void setUp() throws Exception {
+ final ApplicationInfo applicationInfo = new ApplicationInfo();
+ applicationInfo.uid = TEST_UID; // non-zero
+
+ mNotificationChannel = new NotificationChannel(
+ TEST_CHANNEL, TEST_CHANNEL_NAME, IMPORTANCE_LOW);
+ Notification notification = new Notification();
+ notification.extras.putParcelable(EXTRA_BUILDER_APPLICATION_INFO, applicationInfo);
+ mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID, 0,
+ notification, UserHandle.getUserHandleForUid(TEST_UID), null, 0);
+ mEntry = new NotificationEntryBuilder().setSbn(mSbn).build();
+ when(mAssistantFeedbackController.isFeedbackEnabled()).thenReturn(false);
+
+ mTestableLooper = TestableLooper.get(this);
+
+ mContext.addMockSystemService(TelecomManager.class, mTelecomManager);
+
+ mDependency.injectTestDependency(Dependency.BG_LOOPER, mTestableLooper.getLooper());
+ // Inflate the layout
+ final LayoutInflater layoutInflater = LayoutInflater.from(mContext);
+ mInfo = (PromotedNotificationInfo) layoutInflater.inflate(
+ R.layout.promoted_notification_info, null);
+ mInfo.setGutsParent(mock(NotificationGuts.class));
+ // Our view is never attached to a window so the View#post methods in
+ // BundleNotificationInfo never get called. Setting this will skip the post and do the
+ // action immediately.
+ mInfo.mSkipPost = true;
+ }
+
+ @Test
+ @EnableFlags(android.app.Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI)
+ public void testBindNotification_setsOnClickListenerForFeedback() throws Exception {
+
+ // Bind the notification to the Info object
+ final CountDownLatch latch = new CountDownLatch(1);
+ mInfo.bindNotification(
+ mMockPackageManager,
+ mMockINotificationManager,
+ mOnUserInteractionCallback,
+ mChannelEditorDialogController,
+ TEST_PACKAGE_NAME,
+ mNotificationChannel,
+ mEntry,
+ null,
+ null,
+ mUiEventLogger,
+ true,
+ false,
+ true,
+ mAssistantFeedbackController,
+ mMetricsLogger,
+ null);
+ // Click demote button
+ final View demoteButton = mInfo.findViewById(R.id.promoted_demote);
+ demoteButton.performClick();
+ // verify that notiManager tried to demote
+ verify(mMockINotificationManager, atLeastOnce()).setCanBePromoted(TEST_PACKAGE_NAME,
+ mSbn.getUid(), false, true);
+
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt
index 50db9f7268e4..4b8a0c21f03d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.notification.stack
import android.annotation.DimenRes
+import android.platform.test.annotations.EnableFlags
import android.service.notification.StatusBarNotification
import android.view.View.VISIBLE
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -30,6 +31,7 @@ import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
+import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.ExpandableView
import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController
@@ -152,6 +154,29 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() {
}
@Test
+ @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME)
+ fun maxKeyguardNotificationsForPromotedOngoing_onLockscreenSpaceForMinHeightButNotIntrinsicHeight_returnsOne() {
+ setGapHeight(0f)
+ // No divider height since we're testing one element where index = 0
+
+ whenever(sysuiStatusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
+ whenever(lockscreenShadeTransitionController.fractionToShade).thenReturn(0f)
+
+ val row = createMockRow(10f, isPromotedOngoing = true)
+ whenever(row.getMinHeight(any())).thenReturn(5)
+
+ val maxNotifications =
+ computeMaxKeyguardNotifications(
+ listOf(row),
+ /* spaceForNotifications= */ 5f,
+ /* spaceForShelf= */ 0f,
+ /* shelfHeight= */ 0f,
+ )
+
+ assertThat(maxNotifications).isEqualTo(1)
+ }
+
+ @Test
fun computeMaxKeyguardNotifications_spaceForTwo_returnsTwo() {
setGapHeight(gapHeight)
val shelfHeight = shelfHeight + dividerHeight
@@ -257,6 +282,26 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() {
}
@Test
+ @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME)
+ fun getSpaceNeeded_onLockscreenEnoughSpacePromotedOngoing_intrinsicHeight() {
+ setGapHeight(0f)
+ // No divider height since we're testing one element where index = 0
+
+ val row = createMockRow(10f, isPromotedOngoing = true)
+ whenever(row.getMinHeight(any())).thenReturn(5)
+
+ val space =
+ sizeCalculator.getSpaceNeeded(
+ row,
+ visibleIndex = 0,
+ previousView = null,
+ stack = stackLayout,
+ onLockscreen = true,
+ )
+ assertThat(space.whenEnoughSpace).isEqualTo(10f)
+ }
+
+ @Test
fun getSpaceNeeded_onLockscreenEnoughSpaceNotStickyHun_minHeight() {
setGapHeight(0f)
// No divider height since we're testing one element where index = 0
@@ -296,6 +341,26 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() {
}
@Test
+ @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME)
+ fun getSpaceNeeded_onLockscreenSavingSpacePromotedOngoing_minHeight() {
+ setGapHeight(0f)
+ // No divider height since we're testing one element where index = 0
+
+ val expandableView = createMockRow(10f, isPromotedOngoing = true)
+ whenever(expandableView.getMinHeight(any())).thenReturn(5)
+
+ val space =
+ sizeCalculator.getSpaceNeeded(
+ expandableView,
+ visibleIndex = 0,
+ previousView = null,
+ stack = stackLayout,
+ onLockscreen = true,
+ )
+ assertThat(space.whenSavingSpace).isEqualTo(5)
+ }
+
+ @Test
fun getSpaceNeeded_onLockscreenSavingSpaceNotStickyHun_minHeight() {
setGapHeight(0f)
// No divider height since we're testing one element where index = 0
@@ -366,6 +431,7 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() {
isSticky: Boolean = false,
isRemoved: Boolean = false,
visibility: Int = VISIBLE,
+ isPromotedOngoing: Boolean = false,
): ExpandableNotificationRow {
val row = mock(ExpandableNotificationRow::class.java)
val entry = mock(NotificationEntry::class.java)
@@ -378,6 +444,7 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() {
whenever(row.getMinHeight(any())).thenReturn(height.toInt())
whenever(row.intrinsicHeight).thenReturn(height.toInt())
whenever(row.heightWithoutLockscreenConstraints).thenReturn(height.toInt())
+ whenever(row.isPromotedOngoing).thenReturn(isPromotedOngoing)
return row
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt
index 43ad042ecf78..57b7df7a8d31 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt
@@ -162,7 +162,7 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() {
Mockito.`when`(iconManagerFactory.create(ArgumentMatchers.any(), ArgumentMatchers.any()))
.thenReturn(iconManager)
- Mockito.`when`(statusBarContentInsetsProviderStore.defaultDisplay)
+ Mockito.`when`(statusBarContentInsetsProviderStore.forDisplay(context.displayId))
.thenReturn(kosmos.mockStatusBarContentInsetsProvider)
allowTestableLooperAsMainThread()
looper.runWithLooper {
@@ -180,6 +180,7 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() {
private fun createController(): KeyguardStatusBarViewController {
return KeyguardStatusBarViewController(
kosmos.testDispatcher,
+ context,
keyguardStatusBarView,
carrierTextController,
configurationController,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorTest.kt
index fec186e862be..b837253f44a4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorTest.kt
@@ -16,12 +16,16 @@
package com.android.systemui.volume.dialog.domain.interactor
+import android.media.AudioManager.RINGER_MODE_NORMAL
+import android.media.AudioManager.RINGER_MODE_SILENT
+import android.media.AudioManager.RINGER_MODE_VIBRATE
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.plugins.fakeVolumeDialogController
import com.android.systemui.testKosmos
import com.android.systemui.volume.dialog.domain.model.VolumeDialogEventModel
import com.google.common.truth.Truth.assertThat
@@ -44,4 +48,30 @@ class VolumeDialogCallbacksInteractorTest : SysuiTestCase() {
val event by collectLastValue(underTest.event)
assertThat(event).isInstanceOf(VolumeDialogEventModel.SubscribedToEvents::class.java)
}
+
+ @Test
+ fun showSilentHint_setsRingerModeToNormal() =
+ kosmos.runTest {
+ fakeVolumeDialogController.setRingerMode(RINGER_MODE_VIBRATE, false)
+
+ underTest // It should eagerly collect the values and update the controller
+ fakeVolumeDialogController.onShowSilentHint()
+ fakeVolumeDialogController.getState()
+
+ assertThat(fakeVolumeDialogController.state.ringerModeInternal)
+ .isEqualTo(RINGER_MODE_NORMAL)
+ }
+
+ @Test
+ fun showVibrateHint_setsRingerModeToSilent() =
+ kosmos.runTest {
+ fakeVolumeDialogController.setRingerMode(RINGER_MODE_VIBRATE, false)
+
+ underTest // It should eagerly collect the values and update the controller
+ fakeVolumeDialogController.onShowVibrateHint()
+ fakeVolumeDialogController.getState()
+
+ assertThat(fakeVolumeDialogController.state.ringerModeInternal)
+ .isEqualTo(RINGER_MODE_SILENT)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorTest.kt
index 7d5559933cd8..12885a83a70b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorTest.kt
@@ -25,14 +25,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.volume.shared.model.RingerMode
import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.plugins.fakeVolumeDialogController
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -43,8 +42,7 @@ import org.junit.runner.RunWith
@TestableLooper.RunWithLooper
class VolumeDialogRingerInteractorTest : SysuiTestCase() {
- private val kosmos = testKosmos()
- private val testScope = kosmos.testScope
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
private val controller = kosmos.fakeVolumeDialogController
private lateinit var underTest: VolumeDialogRingerInteractor
@@ -57,13 +55,11 @@ class VolumeDialogRingerInteractorTest : SysuiTestCase() {
@Test
fun setRingerMode_normal() =
- testScope.runTest {
- runCurrent()
+ kosmos.runTest {
val ringerModel by collectLastValue(underTest.ringerModel)
underTest.setRingerMode(RingerMode(RINGER_MODE_NORMAL))
controller.getState()
- runCurrent()
assertThat(ringerModel).isNotNull()
assertThat(ringerModel?.currentRingerMode).isEqualTo(RingerMode(RINGER_MODE_NORMAL))
@@ -71,13 +67,11 @@ class VolumeDialogRingerInteractorTest : SysuiTestCase() {
@Test
fun setRingerMode_silent() =
- testScope.runTest {
- runCurrent()
+ kosmos.runTest {
val ringerModel by collectLastValue(underTest.ringerModel)
underTest.setRingerMode(RingerMode(RINGER_MODE_SILENT))
controller.getState()
- runCurrent()
assertThat(ringerModel).isNotNull()
assertThat(ringerModel?.currentRingerMode).isEqualTo(RingerMode(RINGER_MODE_SILENT))
@@ -85,13 +79,11 @@ class VolumeDialogRingerInteractorTest : SysuiTestCase() {
@Test
fun setRingerMode_vibrate() =
- testScope.runTest {
- runCurrent()
+ kosmos.runTest {
val ringerModel by collectLastValue(underTest.ringerModel)
underTest.setRingerMode(RingerMode(RINGER_MODE_VIBRATE))
controller.getState()
- runCurrent()
assertThat(ringerModel).isNotNull()
assertThat(ringerModel?.currentRingerMode).isEqualTo(RingerMode(RINGER_MODE_VIBRATE))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInputEventsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInputEventsInteractorTest.kt
index 799ca4a49038..0a50722d8fed 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInputEventsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInputEventsInteractorTest.kt
@@ -18,7 +18,6 @@ package com.android.systemui.volume.dialog.sliders.domain.interactor
import android.app.ActivityManager
import android.testing.TestableLooper
-import android.view.MotionEvent
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -29,6 +28,7 @@ import com.android.systemui.testKosmos
import com.android.systemui.volume.Events
import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor
import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel
+import com.android.systemui.volume.dialog.sliders.shared.model.SliderInputEvent
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import kotlin.time.Duration.Companion.seconds
@@ -73,16 +73,7 @@ class VolumeDialogSliderInputEventsInteractorTest : SysuiTestCase() {
assertThat(dialogVisibility)
.isInstanceOf(VolumeDialogVisibilityModel.Visible::class.java)
- underTest.onTouchEvent(
- MotionEvent.obtain(
- /* downTime = */ 0,
- /* eventTime = */ 0,
- /* action = */ 0,
- /* x = */ 0f,
- /* y = */ 0f,
- /* metaState = */ 0,
- )
- )
+ underTest.onTouchEvent(SliderInputEvent.Touch.Start(0f, 0f))
advanceTimeBy(volumeDialogTimeout / 2)
assertThat(dialogVisibility)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt
index ba6ea9f5e8bb..89410593fe62 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt
@@ -16,11 +16,14 @@
package com.android.systemui.wallpapers
+import android.app.Flags
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.service.wallpaper.WallpaperService.Engine
import android.testing.TestableLooper.RunWithLooper
import android.view.Surface
@@ -37,6 +40,7 @@ import org.mockito.Mockito.spy
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.any
import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyZeroInteractions
import org.mockito.kotlin.whenever
@SmallTest
@@ -70,6 +74,18 @@ class GradientColorWallpaperTest : SysuiTestCase() {
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER)
+ fun onSurfaceRedrawNeeded_flagDisabled_shouldNotDrawInCanvas() {
+ val engine = createGradientColorWallpaperEngine()
+ engine.onCreate(surfaceHolder)
+
+ engine.onSurfaceRedrawNeeded(surfaceHolder)
+
+ verifyZeroInteractions(canvas)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER)
fun onSurfaceRedrawNeeded_shouldDrawInCanvas() {
val engine = createGradientColorWallpaperEngine()
engine.onCreate(surfaceHolder)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt
index 2985053f56d5..d6343c840d9b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt
@@ -26,4 +26,5 @@ class FakeWallpaperRepository : WallpaperRepository {
override val wallpaperInfo = MutableStateFlow<WallpaperInfo?>(null)
override val wallpaperSupportsAmbientMode = flowOf(false)
override var rootView: View? = null
+ override val shouldSendFocalArea = MutableStateFlow(false)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt
index 03753d9aa884..115edd0d3bb0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt
@@ -67,7 +67,6 @@ class WallpaperRepositoryImplTest : SysuiTestCase() {
fakeBroadcastDispatcher,
userRepository,
keyguardRepository,
- keyguardClockRepository,
wallpaperManager,
context,
)
@@ -252,7 +251,7 @@ class WallpaperRepositoryImplTest : SysuiTestCase() {
@EnableFlags(Flags.FLAG_MAGIC_PORTRAIT_WALLPAPERS)
fun shouldSendNotificationLayout_setMagicPortraitWallpaper_launchSendLayoutJob() =
testScope.runTest {
- val latest by collectLastValue(underTest.shouldSendNotificationLayout)
+ val latest by collectLastValue(underTest.shouldSendFocalArea)
val magicPortraitWallpaper =
mock<WallpaperInfo>().apply {
whenever(this.component)
@@ -273,7 +272,7 @@ class WallpaperRepositoryImplTest : SysuiTestCase() {
@EnableFlags(Flags.FLAG_MAGIC_PORTRAIT_WALLPAPERS)
fun shouldSendNotificationLayout_setNotMagicPortraitWallpaper_cancelSendLayoutJob() =
testScope.runTest {
- val latest by collectLastValue(underTest.shouldSendNotificationLayout)
+ val latest by collectLastValue(underTest.shouldSendFocalArea)
val magicPortraitWallpaper = MAGIC_PORTRAIT_WP
whenever(wallpaperManager.getWallpaperInfoForUser(any()))
.thenReturn(magicPortraitWallpaper)
diff --git a/packages/SystemUI/res-keyguard/layout/bouncer_message_view.xml b/packages/SystemUI/res-keyguard/layout/bouncer_message_view.xml
index f7dac13888c0..ea494b4642d5 100644
--- a/packages/SystemUI/res-keyguard/layout/bouncer_message_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/bouncer_message_view.xml
@@ -21,7 +21,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/keyguard_lock_padding"
- android:focusable="true"
+ android:focusable="false"
/>
<com.android.keyguard.BouncerKeyguardMessageArea
@@ -30,6 +30,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/secondary_message_padding"
- android:focusable="true" />
+ android:focusable="false" />
</merge>
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml b/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml
index 2a8f1b596711..f231df2f1a10 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml
@@ -66,7 +66,7 @@
<com.android.systemui.bouncer.ui.BouncerMessageView
android:id="@+id/bouncer_message_view"
- android:importantForAccessibility="noHideDescendants"
+ android:screenReaderFocusable="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml
index 76f6f599c54c..04457229d573 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml
@@ -31,7 +31,7 @@
<com.android.systemui.bouncer.ui.BouncerMessageView
android:id="@+id/bouncer_message_view"
- android:importantForAccessibility="noHideDescendants"
+ android:screenReaderFocusable="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml
index 5879c110d8a1..b184344f2f24 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml
@@ -67,7 +67,7 @@
<com.android.systemui.bouncer.ui.BouncerMessageView
android:id="@+id/bouncer_message_view"
- android:importantForAccessibility="noHideDescendants"
+ android:screenReaderFocusable="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml
index 3f7b02835357..0e15ff66f3ee 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml
@@ -35,7 +35,7 @@
<com.android.systemui.bouncer.ui.BouncerMessageView
android:id="@+id/bouncer_message_view"
- android:importantForAccessibility="noHideDescendants"
+ android:screenReaderFocusable="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml
index b464fb3bafed..f6ac02aee657 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml
@@ -74,7 +74,7 @@
<com.android.systemui.bouncer.ui.BouncerMessageView
android:id="@+id/bouncer_message_view"
- android:importantForAccessibility="noHideDescendants"
+ android:screenReaderFocusable="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml
index 21580731aed2..ba4da794d777 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml
@@ -32,7 +32,7 @@
<com.android.systemui.bouncer.ui.BouncerMessageView
android:id="@+id/bouncer_message_view"
- android:importantForAccessibility="noHideDescendants"
+ android:screenReaderFocusable="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
diff --git a/packages/SystemUI/res-keyguard/values/styles.xml b/packages/SystemUI/res-keyguard/values/styles.xml
index 433c0a71008d..e7d6b2fe08f4 100644
--- a/packages/SystemUI/res-keyguard/values/styles.xml
+++ b/packages/SystemUI/res-keyguard/values/styles.xml
@@ -159,6 +159,17 @@
<item name="android:shadowRadius">?attr/shadowRadius</item>
</style>
+ <style name="TextAppearance.Keyguard.BottomArea.DoubleShadow">
+ <item name="keyShadowBlur">0.5dp</item>
+ <item name="keyShadowOffsetX">0.5dp</item>
+ <item name="keyShadowOffsetY">0.5dp</item>
+ <item name="keyShadowAlpha">0.8</item>
+ <item name="ambientShadowBlur">0.5dp</item>
+ <item name="ambientShadowOffsetX">0.5dp</item>
+ <item name="ambientShadowOffsetY">0.5dp</item>
+ <item name="ambientShadowAlpha">0.6</item>
+ </style>
+
<style name="TextAppearance.Keyguard.BottomArea.Button">
<item name="android:shadowRadius">0</item>
</style>
diff --git a/packages/SystemUI/res/drawable/ic_widgets.xml b/packages/SystemUI/res/drawable/ic_widgets.xml
deleted file mode 100644
index 9e05809bfb33..000000000000
--- a/packages/SystemUI/res/drawable/ic_widgets.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<!--
- ~ Copyright (C) 2024 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:tint="?attr/colorControlNormal"
- android:viewportHeight="960"
- android:viewportWidth="960">
- <path
- android:fillColor="@android:color/black"
- android:pathData="M666,520L440,294L666,68L892,294L666,520ZM120,440L120,120L440,120L440,440L120,440ZM520,840L520,520L840,520L840,840L520,840ZM120,840L120,520L440,520L440,840L120,840ZM200,360L360,360L360,200L200,200L200,360ZM667,408L780,295L667,182L554,295L667,408ZM600,760L760,760L760,600L600,600L600,760ZM200,760L360,760L360,600L200,600L200,760ZM360,360L360,360L360,360L360,360L360,360ZM554,295L554,295L554,295L554,295L554,295ZM360,600L360,600L360,600L360,600L360,600ZM600,600L600,600L600,600L600,600L600,600Z" />
-</vector>
diff --git a/packages/SystemUI/res/drawable/notif_footer_btn_background.xml b/packages/SystemUI/res/drawable/notif_footer_btn_background.xml
index 1d5e09d9b260..e1e60920ab01 100644
--- a/packages/SystemUI/res/drawable/notif_footer_btn_background.xml
+++ b/packages/SystemUI/res/drawable/notif_footer_btn_background.xml
@@ -26,6 +26,9 @@
<padding
android:left="20dp"
android:right="20dp" />
+ <!-- TODO(b/294830092): Update to the blur surface effect color token when
+ the resource workaround is resolved and
+ notification_shade_blur is enabled. -->
<solid android:color="@androidprv:color/materialColorSurfaceContainerHigh" />
</shape>
</inset>
diff --git a/packages/SystemUI/res/layout/promoted_notification_info.xml b/packages/SystemUI/res/layout/promoted_notification_info.xml
new file mode 100644
index 000000000000..5d170a98a806
--- /dev/null
+++ b/packages/SystemUI/res/layout/promoted_notification_info.xml
@@ -0,0 +1,387 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2024, The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<com.android.systemui.statusbar.notification.row.PromotedNotificationInfo
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:id="@+id/notification_guts"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:focusable="true"
+ android:clipChildren="false"
+ android:clipToPadding="true"
+ android:orientation="vertical"
+ android:paddingStart="@dimen/notification_shade_content_margin_horizontal">
+
+ <!-- Package Info -->
+ <LinearLayout
+ android:id="@+id/header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:clipChildren="false"
+ android:paddingTop="@dimen/notification_guts_header_top_padding"
+ android:clipToPadding="true">
+ <ImageView
+ android:id="@+id/pkg_icon"
+ android:layout_width="@dimen/notification_guts_conversation_icon_size"
+ android:layout_height="@dimen/notification_guts_conversation_icon_size"
+ android:layout_centerVertical="true"
+ android:layout_alignParentStart="true"
+ android:layout_marginEnd="15dp" />
+ <LinearLayout
+ android:id="@+id/names"
+ android:layout_weight="1"
+ android:layout_width="0dp"
+ android:orientation="vertical"
+ android:layout_height="wrap_content"
+ android:minHeight="@dimen/notification_guts_conversation_icon_size"
+ android:layout_centerVertical="true"
+ android:gravity="center_vertical"
+ android:layout_alignEnd="@id/pkg_icon"
+ android:layout_toEndOf="@id/pkg_icon">
+ <TextView
+ android:id="@+id/channel_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textDirection="locale"
+ style="@style/TextAppearance.NotificationImportanceChannel"/>
+ <TextView
+ android:id="@+id/group_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textDirection="locale"
+ android:ellipsize="end"
+ style="@style/TextAppearance.NotificationImportanceChannelGroup"/>
+ <TextView
+ android:id="@+id/pkg_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@style/TextAppearance.NotificationImportanceApp"
+ android:ellipsize="end"
+ android:textDirection="locale"
+ android:maxLines="1"/>
+ <TextView
+ android:id="@+id/delegate_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ style="@style/TextAppearance.NotificationImportanceHeader"
+ android:layout_marginStart="2dp"
+ android:layout_marginEnd="2dp"
+ android:ellipsize="end"
+ android:textDirection="locale"
+ android:text="@string/notification_delegate_header"
+ android:maxLines="1" />
+
+ </LinearLayout>
+
+ <!-- end aligned fields -->
+ <!-- Optional link to app. Only appears if the channel is not disabled and the app
+asked for it -->
+ <ImageButton
+ android:id="@+id/app_settings"
+ android:layout_width="@dimen/notification_importance_toggle_size"
+ android:layout_height="@dimen/notification_importance_toggle_size"
+ android:layout_centerVertical="true"
+ android:visibility="gone"
+ android:background="@drawable/ripple_drawable"
+ android:contentDescription="@string/notification_app_settings"
+ android:src="@drawable/ic_info"
+ android:layout_toStartOf="@id/info"
+ android:tint="@androidprv:color/materialColorPrimary"/>
+ <ImageButton
+ android:id="@+id/info"
+ android:layout_width="@dimen/notification_importance_toggle_size"
+ android:layout_height="@dimen/notification_importance_toggle_size"
+ android:layout_centerVertical="true"
+ android:contentDescription="@string/notification_more_settings"
+ android:background="@drawable/ripple_drawable_20dp"
+ android:src="@drawable/ic_settings"
+ android:tint="@androidprv:color/materialColorPrimary"
+ android:layout_alignParentEnd="true" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/inline_controls"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingEnd="@dimen/notification_shade_content_margin_horizontal"
+ android:layout_marginTop="@dimen/notification_guts_option_vertical_padding"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:orientation="vertical">
+
+ <!-- Non configurable app/channel text. appears instead of @+id/interruptiveness_settings-->
+ <TextView
+ android:id="@+id/non_configurable_text"
+ android:text="@string/notification_unblockable_desc"
+ android:visibility="gone"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@*android:style/TextAppearance.DeviceDefault.Notification" />
+
+ <!-- Non configurable app/channel text. appears instead of @+id/interruptiveness_settings-->
+ <TextView
+ android:id="@+id/non_configurable_call_text"
+ android:text="@string/notification_unblockable_call_desc"
+ android:visibility="gone"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@*android:style/TextAppearance.DeviceDefault.Notification" />
+
+ <!-- Non configurable multichannel text. appears instead of @+id/interruptiveness_settings-->
+ <TextView
+ android:id="@+id/non_configurable_multichannel_text"
+ android:text="@string/notification_multichannel_desc"
+ android:visibility="gone"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@*android:style/TextAppearance.DeviceDefault.Notification" />
+
+ <LinearLayout
+ android:id="@+id/interruptiveness_settings"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="vertical">
+ <com.android.systemui.statusbar.notification.row.ButtonLinearLayout
+ android:id="@+id/automatic"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/notification_importance_button_separation"
+ android:padding="@dimen/notification_importance_button_padding"
+ android:clickable="true"
+ android:focusable="true"
+ android:background="@drawable/notification_guts_priority_button_bg"
+ android:orientation="vertical"
+ android:visibility="gone">
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center"
+ >
+ <ImageView
+ android:id="@+id/automatic_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@drawable/ic_notifications_automatic"
+ android:background="@android:color/transparent"
+ android:tint="@color/notification_guts_priority_contents"
+ android:clickable="false"
+ android:focusable="false"/>
+ <TextView
+ android:id="@+id/automatic_label"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/notification_importance_drawable_padding"
+ android:layout_weight="1"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:clickable="false"
+ android:focusable="false"
+ android:textAppearance="@style/TextAppearance.NotificationImportanceButton"
+ android:text="@string/notification_automatic_title"/>
+ </LinearLayout>
+ <TextView
+ android:id="@+id/automatic_summary"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/notification_importance_button_description_top_margin"
+ android:visibility="gone"
+ android:text="@string/notification_channel_summary_automatic"
+ android:clickable="false"
+ android:focusable="false"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:textAppearance="@style/TextAppearance.NotificationImportanceDetail"/>
+ </com.android.systemui.statusbar.notification.row.ButtonLinearLayout>
+
+ <com.android.systemui.statusbar.notification.row.ButtonLinearLayout
+ android:id="@+id/alert"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="@dimen/notification_importance_button_padding"
+ android:clickable="true"
+ android:focusable="true"
+ android:background="@drawable/notification_guts_priority_button_bg"
+ android:orientation="vertical">
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center"
+ >
+ <ImageView
+ android:id="@+id/alert_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@drawable/ic_notifications_alert"
+ android:background="@android:color/transparent"
+ android:tint="@color/notification_guts_priority_contents"
+ android:clickable="false"
+ android:focusable="false"/>
+ <TextView
+ android:id="@+id/alert_label"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/notification_importance_drawable_padding"
+ android:layout_weight="1"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:clickable="false"
+ android:focusable="false"
+ android:textAppearance="@style/TextAppearance.NotificationImportanceButton"
+ android:text="@string/notification_alert_title"/>
+ </LinearLayout>
+ <TextView
+ android:id="@+id/alert_summary"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/notification_importance_button_description_top_margin"
+ android:visibility="gone"
+ android:text="@string/notification_channel_summary_default"
+ android:clickable="false"
+ android:focusable="false"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:textAppearance="@style/TextAppearance.NotificationImportanceDetail"/>
+ </com.android.systemui.statusbar.notification.row.ButtonLinearLayout>
+
+ <com.android.systemui.statusbar.notification.row.ButtonLinearLayout
+ android:id="@+id/silence"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/notification_importance_button_separation"
+ android:padding="@dimen/notification_importance_button_padding"
+ android:clickable="true"
+ android:focusable="true"
+ android:background="@drawable/notification_guts_priority_button_bg"
+ android:orientation="vertical">
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center"
+ >
+ <ImageView
+ android:id="@+id/silence_icon"
+ android:src="@drawable/ic_notifications_silence"
+ android:background="@android:color/transparent"
+ android:tint="@color/notification_guts_priority_contents"
+ android:layout_gravity="center"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:clickable="false"
+ android:focusable="false"/>
+ <TextView
+ android:id="@+id/silence_label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:clickable="false"
+ android:focusable="false"
+ android:layout_toEndOf="@id/silence_icon"
+ android:layout_marginStart="@dimen/notification_importance_drawable_padding"
+ android:textAppearance="@style/TextAppearance.NotificationImportanceButton"
+ android:text="@string/notification_silence_title"/>
+ </LinearLayout>
+ <TextView
+ android:id="@+id/silence_summary"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/notification_importance_button_description_top_margin"
+ android:visibility="gone"
+ android:text="@string/notification_channel_summary_low"
+ android:clickable="false"
+ android:focusable="false"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:textAppearance="@style/TextAppearance.NotificationImportanceDetail"/>
+ </com.android.systemui.statusbar.notification.row.ButtonLinearLayout>
+
+ </LinearLayout>
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="60dp"
+ android:gravity="center_vertical"
+ android:paddingStart="4dp"
+ android:paddingEnd="4dp"
+ >
+ <TextView
+ android:id="@+id/promoted_demote"
+ android:text="@string/notification_inline_disable_promotion"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:gravity="start|center_vertical"
+ android:minWidth="@dimen/notification_importance_toggle_size"
+ android:minHeight="@dimen/notification_importance_toggle_size"
+ android:maxWidth="200dp"
+ style="@style/TextAppearance.NotificationInfo.Button"/>
+ <TextView
+ android:id="@+id/promoted_dismiss"
+ android:text="@string/notification_inline_dismiss"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:gravity="end|center_vertical"
+ android:minWidth="@dimen/notification_importance_toggle_size"
+ android:minHeight="@dimen/notification_importance_toggle_size"
+ android:maxWidth="125dp"
+ style="@style/TextAppearance.NotificationInfo.Button"/>
+ </RelativeLayout>
+
+ <RelativeLayout
+ android:id="@+id/bottom_buttons"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="60dp"
+ android:gravity="center_vertical"
+ android:paddingStart="4dp"
+ android:paddingEnd="4dp"
+ >
+ <TextView
+ android:id="@+id/turn_off_notifications"
+ android:text="@string/inline_turn_off_notifications"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:gravity="start|center_vertical"
+ android:minWidth="@dimen/notification_importance_toggle_size"
+ android:minHeight="@dimen/notification_importance_toggle_size"
+ android:maxWidth="200dp"
+ style="@style/TextAppearance.NotificationInfo.Button"/>
+ <TextView
+ android:id="@+id/done"
+ android:text="@string/inline_ok_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:gravity="end|center_vertical"
+ android:minWidth="@dimen/notification_importance_toggle_size"
+ android:minHeight="@dimen/notification_importance_toggle_size"
+ android:maxWidth="125dp"
+ style="@style/TextAppearance.NotificationInfo.Button"/>
+ </RelativeLayout>
+ </LinearLayout>
+</com.android.systemui.statusbar.notification.row.PromotedNotificationInfo>
diff --git a/packages/SystemUI/res/layout/volume_dialog_slider.xml b/packages/SystemUI/res/layout/volume_dialog_slider.xml
index c5f468e731f5..2628f4991b49 100644
--- a/packages/SystemUI/res/layout/volume_dialog_slider.xml
+++ b/packages/SystemUI/res/layout/volume_dialog_slider.xml
@@ -18,14 +18,10 @@
android:layout_height="match_parent"
android:maxHeight="@dimen/volume_dialog_slider_height">
- <com.google.android.material.slider.Slider
+ <androidx.compose.ui.platform.ComposeView
android:id="@+id/volume_dialog_slider"
- style="@style/SystemUI.Material3.Slider.Volume"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
- android:layout_marginTop="-20dp"
- android:layout_marginBottom="-20dp"
- android:orientation="vertical"
- android:theme="@style/Theme.Material3.DayNight" />
+ android:orientation="vertical" />
</FrameLayout> \ No newline at end of file
diff --git a/packages/SystemUI/res/values-xlarge-land/config.xml b/packages/SystemUI/res/values-xlarge-land/config.xml
index 5e4304e1c13a..6d8b64ade259 100644
--- a/packages/SystemUI/res/values-xlarge-land/config.xml
+++ b/packages/SystemUI/res/values-xlarge-land/config.xml
@@ -16,4 +16,5 @@
<resources>
<item name="shortcut_helper_screen_width_fraction" format="float" type="dimen">0.8</item>
+ <bool name="center_align_magic_portrait_shape">true</bool>
</resources>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 940e87d3d163..68e33f27aefa 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -1104,7 +1104,10 @@
-->
<bool name="config_userSwitchingMustGoThroughLoginScreen">false</bool>
-
<!-- The dream component used when the device is low light environment. -->
<string translatable="false" name="config_lowLightDreamComponent"/>
+
+ <!--Whether we should position magic portrait shape effects in the center of lockscreen
+ it's false by default, and only be true in tablet landscape -->
+ <bool name="center_align_magic_portrait_shape">false</bool>
</resources>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 8a0ffb90bf09..724a12c12490 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -266,6 +266,9 @@
<!-- Height of a large notification in the status bar -->
<dimen name="notification_max_height">358dp</dimen>
+ <!-- Height of a large promoted ongoing notification in the status bar -->
+ <dimen name="notification_max_height_for_promoted_ongoing">272dp</dimen>
+
<!-- Height of a heads up notification in the status bar for legacy custom views -->
<dimen name="notification_max_heads_up_height_legacy">128dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 866dfe4d8972..64367ef79856 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1351,12 +1351,6 @@
<string name="communal_widgets_disclaimer_text">To open an app using a widget, you\u2019ll need to verify it\u2019s you. Also, keep in mind that anyone can view them, even when your tablet\u2019s locked. Some widgets may not have been intended for your lock screen and may be unsafe to add here.</string>
<!-- Button for user to verify they understand the information presented. [CHAR LIMIT=50] -->
<string name="communal_widgets_disclaimer_button">Got it</string>
- <!-- Label for a lock screen affordance to show widgets on the lock screen. [CHAR LIMIT=20] -->
- <string name="glanceable_hub_lockscreen_affordance_label">Widgets</string>
- <!-- Text explaining why the lock screen affordance to show widgets on the lockscreen is disabled and how to enable the affordance in settings. [CHAR LIMIT=NONE] -->
- <string name="glanceable_hub_lockscreen_affordance_disabled_text">To add the \"Widgets\" shortcut, make sure \"Show widgets on lock screen\" is enabled in settings.</string>
- <!-- Label for a button used to open Settings in order to enable showing widgets on the lock screen. [CHAR LIMIT=NONE] -->
- <string name="glanceable_hub_lockscreen_affordance_action_button_label">Settings</string>
<!-- Content description for a "show screensaver" button on glanceable hub. [CHAR LIMIT=NONE] -->
<string name="accessibility_glanceable_hub_to_dream_button">Show screensaver button</string>
<!-- Title shown in hub onboarding bottom sheet. [CHAR LIMIT=50] -->
@@ -2089,6 +2083,12 @@
<!-- [CHAR LIMIT=80] Text shown in feedback button in notification guts for a bundled notification -->
<string name="notification_guts_bundle_feedback" translatable="false">Provide Bundle Feedback</string>
+ <!-- [CHAR LIMIT=30] Text shown in button used to dismiss this single notification. -->
+ <string name="notification_inline_dismiss">Dismiss</string>
+
+ <!-- [CHAR LIMIT=30] Text shown in button used to prevent app from showing Live Updates, for this notification and all future ones -->
+ <string name="notification_inline_disable_promotion">Don\'t show again</string>
+
<!-- Notification: Control panel: Label that displays when the app's notifications cannot be blocked. -->
<string name="notification_unblockable_desc">These notifications can\'t be modified.</string>
@@ -2316,9 +2316,6 @@
<string name="group_system_lock_screen">Lock screen</string>
<!-- User visible title for the keyboard shortcut that pulls up Notes app for quick memo. [CHAR LIMIT=70] -->
<string name="group_system_quick_memo">Take a note</string>
- <!-- TODO(b/383734125): make it translatable once string is finalized by UXW.-->
- <!-- User visible title for the keyboard shortcut that toggles Voice Access. [CHAR LIMIT=70] -->
- <string name="group_system_toggle_voice_access" translatable="false">Toggle Voice Access</string>
<!-- User visible title for the multitasking keyboard shortcuts list. [CHAR LIMIT=70] -->
<string name="keyboard_shortcut_group_system_multitasking">Multitasking</string>
@@ -2377,6 +2374,17 @@
<!-- User visible title for the keyboard shortcut that takes the user to the maps app. [CHAR LIMIT=70] -->
<string name="keyboard_shortcut_group_applications_maps">Maps</string>
+ <!-- User visible title for the keyboard shortcut that toggles bounce keys. [CHAR LIMIT=70]-->
+ <string name="group_accessibility_toggle_bounce_keys">Toggle bounce keys</string>
+ <!-- User visible title for the keyboard shortcut that toggles mouse keys. [CHAR LIMIT=70]-->
+ <string name="group_accessibility_toggle_mouse_keys">Toggle mouse keys</string>
+ <!-- User visible title for the keyboard shortcut that toggles sticky keys. [CHAR LIMIT=70]-->
+ <string name="group_accessibility_toggle_sticky_keys">Toggle sticky keys</string>
+ <!-- User visible title for the keyboard shortcut that toggles slow keys. [CHAR LIMIT=70]-->
+ <string name="group_accessibility_toggle_slow_keys">Toggle slow keys</string>
+ <!-- User visible title for the keyboard shortcut that toggles Voice Access. [CHAR LIMIT=70] -->
+ <string name="group_accessibility_toggle_voice_access">Toggle Voice Access</string>
+
<!-- SysUI Tuner: Label for screen about do not disturb settings [CHAR LIMIT=60] -->
<string name="volume_and_do_not_disturb">Do Not Disturb</string>
@@ -4011,13 +4019,13 @@
<!-- Touchpad switch apps gesture action name in tutorial [CHAR LIMIT=NONE] -->
<string name="touchpad_switch_apps_gesture_action_title">Switch apps</string>
<!-- Touchpad switch apps gesture guidance in gestures tutorial [CHAR LIMIT=NONE] -->
- <string name="touchpad_switch_apps_gesture_guidance">Swipe left using four fingers on your touchpad</string>
+ <string name="touchpad_switch_apps_gesture_guidance">Swipe right using four fingers on your touchpad</string>
<!-- Screen title after switch apps gesture was done successfully [CHAR LIMIT=NONE] -->
<string name="touchpad_switch_apps_gesture_success_title">Great job!</string>
<!-- Text shown to the user after they complete switch apps gesture tutorial [CHAR LIMIT=NONE] -->
<string name="touchpad_switch_apps_gesture_success_body">You completed the switch apps gesture.</string>
<!-- Text shown to the user after switch gesture was not done correctly [CHAR LIMIT=NONE] -->
- <string name="touchpad_switch_gesture_error_body">Swipe left using four fingers on your touchpad to switch apps</string>
+ <string name="touchpad_switch_gesture_error_body">Swipe right using four fingers on your touchpad to switch apps</string>
<!-- KEYBOARD TUTORIAL-->
<!-- Action key tutorial title [CHAR LIMIT=NONE] -->
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index b0d9bed05e27..c95c6dd89353 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -565,16 +565,6 @@
<item name="android:windowNoTitle">true</item>
</style>
- <style name="SystemUI.Material3.Slider.Volume">
- <item name="trackHeight">40dp</item>
- <item name="thumbHeight">52dp</item>
- <item name="trackCornerSize">12dp</item>
- <item name="trackInsideCornerSize">2dp</item>
- <item name="trackStopIndicatorSize">6dp</item>
- <item name="trackIconSize">20dp</item>
- <item name="labelBehavior">gone</item>
- </style>
-
<style name="SystemUI.Material3.Slider" parent="@style/Widget.Material3.Slider">
<item name="labelStyle">@style/Widget.Material3.Slider.Label</item>
<item name="thumbColor">@color/thumb_color</item>
diff --git a/packages/SystemUI/res/xml/gradient_color_wallpaper.xml b/packages/SystemUI/res/xml/gradient_color_wallpaper.xml
new file mode 100644
index 000000000000..f453a4450750
--- /dev/null
+++ b/packages/SystemUI/res/xml/gradient_color_wallpaper.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<wallpaper
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:showMetadataInPreview="false"
+ android:supportsMultipleDisplays="true" /> \ No newline at end of file
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
index d363e524a9f2..011458859db4 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
@@ -23,7 +23,7 @@ import android.os.IRemoteCallback;
import android.view.MotionEvent;
import com.android.systemui.shared.recents.ISystemUiProxy;
-// Next ID: 38
+// Next ID: 39
oneway interface IOverviewProxy {
void onActiveNavBarRegionChanges(in Region activeRegion) = 11;
@@ -154,4 +154,9 @@ oneway interface IOverviewProxy {
* Sent when {@link TaskbarDelegate#onDisplayRemoved} is called.
*/
void onDisplayRemoved(int displayId) = 37;
+
+ /**
+ * Sent when {@link TaskbarDelegate#onDisplayRemoveSystemDecorations} is called.
+ */
+ void onDisplayRemoveSystemDecorations(int displayId) = 38;
}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
index 8576a6ebac42..a518c57bdd16 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
@@ -72,6 +72,25 @@ public class Task {
@ViewDebug.ExportedProperty(category = "recents")
public final int displayId;
+ /**
+ * The component of the first activity in the task, can be considered the "application" of
+ * this task.
+ */
+ @Nullable
+ public ComponentName baseActivity;
+ /**
+ * The number of activities in this task (including running).
+ */
+ public int numActivities;
+ /**
+ * Whether the top activity is to be displayed. See {@link android.R.attr#windowNoDisplay}.
+ */
+ public boolean isTopActivityNoDisplay;
+ /**
+ * Whether fillsParent() is false for every activity in the tasks stack.
+ */
+ public boolean isActivityStackTransparent;
+
// The source component name which started this task
public final ComponentName sourceComponent;
@@ -90,6 +109,10 @@ public class Task {
this.userId = t.userId;
this.lastActiveTime = t.lastActiveTime;
this.displayId = t.displayId;
+ this.baseActivity = t.baseActivity;
+ this.numActivities = t.numActivities;
+ this.isTopActivityNoDisplay = t.isTopActivityNoDisplay;
+ this.isActivityStackTransparent = t.isActivityStackTransparent;
updateHashCode();
}
@@ -106,7 +129,9 @@ public class Task {
}
public TaskKey(int id, int windowingMode, @NonNull Intent intent,
- ComponentName sourceComponent, int userId, long lastActiveTime, int displayId) {
+ ComponentName sourceComponent, int userId, long lastActiveTime, int displayId,
+ @Nullable ComponentName baseActivity, int numActivities,
+ boolean isTopActivityNoDisplay, boolean isActivityStackTransparent) {
this.id = id;
this.windowingMode = windowingMode;
this.baseIntent = intent;
@@ -114,6 +139,10 @@ public class Task {
this.userId = userId;
this.lastActiveTime = lastActiveTime;
this.displayId = displayId;
+ this.baseActivity = baseActivity;
+ this.numActivities = numActivities;
+ this.isTopActivityNoDisplay = isTopActivityNoDisplay;
+ this.isActivityStackTransparent = isActivityStackTransparent;
updateHashCode();
}
@@ -185,6 +214,10 @@ public class Task {
parcel.writeLong(lastActiveTime);
parcel.writeInt(displayId);
parcel.writeTypedObject(sourceComponent, flags);
+ parcel.writeTypedObject(baseActivity, flags);
+ parcel.writeInt(numActivities);
+ parcel.writeBoolean(isTopActivityNoDisplay);
+ parcel.writeBoolean(isActivityStackTransparent);
}
private static TaskKey readFromParcel(Parcel parcel) {
@@ -195,9 +228,14 @@ public class Task {
long lastActiveTime = parcel.readLong();
int displayId = parcel.readInt();
ComponentName sourceComponent = parcel.readTypedObject(ComponentName.CREATOR);
+ ComponentName baseActivity = parcel.readTypedObject(ComponentName.CREATOR);
+ int numActivities = parcel.readInt();
+ boolean isTopActivityNoDisplay = parcel.readBoolean();
+ boolean isActivityStackTransparent = parcel.readBoolean();
return new TaskKey(id, windowingMode, baseIntent, sourceComponent, userId,
- lastActiveTime, displayId);
+ lastActiveTime, displayId, baseActivity, numActivities, isTopActivityNoDisplay,
+ isActivityStackTransparent);
}
@Override
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java
index 41ad4373455e..efe758ea0011 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java
@@ -16,11 +16,12 @@
package com.android.systemui.shared.recents.utilities;
-import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT;
-import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN;
-import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN;
+import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_DISMISS_IME;
+import static android.app.StatusBarManager.NAVIGATION_HINT_IME_VISIBLE;
+import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE;
import android.annotation.TargetApi;
+import android.app.StatusBarManager.NavigationHint;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Color;
@@ -103,35 +104,43 @@ public class Utilities {
}
/**
- * @return updated set of flags from InputMethodService based off {@param oldHints}
- * Leaves original hints unmodified
+ * Gets the updated navigation icon hints, based on the current ones and the given IME state.
+ *
+ * @param oldHints current navigation icon hints.
+ * @param backDisposition the IME back disposition mode. Only takes effect if
+ * {@code isImeVisible} is {@code true}.
+ * @param isImeVisible whether the IME is currently visible.
+ * @param showImeSwitcher whether the IME Switcher button should be shown. Only takes effect if
+ * {@code isImeVisible} is {@code true}.
*/
- public static int calculateBackDispositionHints(int oldHints,
- @BackDispositionMode int backDisposition, boolean imeShown, boolean showImeSwitcher) {
+ @NavigationHint
+ public static int calculateNavigationIconHints(@NavigationHint int oldHints,
+ @BackDispositionMode int backDisposition, boolean isImeVisible,
+ boolean showImeSwitcher) {
int hints = oldHints;
switch (backDisposition) {
case InputMethodService.BACK_DISPOSITION_DEFAULT:
case InputMethodService.BACK_DISPOSITION_WILL_NOT_DISMISS:
case InputMethodService.BACK_DISPOSITION_WILL_DISMISS:
- if (imeShown) {
- hints |= NAVIGATION_HINT_BACK_ALT;
+ if (isImeVisible) {
+ hints |= NAVIGATION_HINT_BACK_DISMISS_IME;
} else {
- hints &= ~NAVIGATION_HINT_BACK_ALT;
+ hints &= ~NAVIGATION_HINT_BACK_DISMISS_IME;
}
break;
case InputMethodService.BACK_DISPOSITION_ADJUST_NOTHING:
- hints &= ~NAVIGATION_HINT_BACK_ALT;
+ hints &= ~NAVIGATION_HINT_BACK_DISMISS_IME;
break;
}
- if (imeShown) {
- hints |= NAVIGATION_HINT_IME_SHOWN;
+ if (isImeVisible) {
+ hints |= NAVIGATION_HINT_IME_VISIBLE;
} else {
- hints &= ~NAVIGATION_HINT_IME_SHOWN;
+ hints &= ~NAVIGATION_HINT_IME_VISIBLE;
}
- if (showImeSwitcher) {
- hints |= NAVIGATION_HINT_IME_SWITCHER_SHOWN;
+ if (showImeSwitcher && isImeVisible) {
+ hints |= NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE;
} else {
- hints &= ~NAVIGATION_HINT_IME_SWITCHER_SHOWN;
+ hints &= ~NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE;
}
return hints;
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt
index bd20777c7102..e928525afc14 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt
@@ -16,6 +16,7 @@
package com.android.systemui.shared.shadow
import android.content.Context
+import android.content.res.TypedArray
import android.graphics.Canvas
import android.graphics.drawable.Drawable
import android.util.AttributeSet
@@ -31,19 +32,23 @@ constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
- defStyleRes: Int = 0
+ defStyleRes: Int = 0,
) : TextView(context, attrs, defStyleAttr, defStyleRes) {
- private val mKeyShadowInfo: ShadowInfo
- private val mAmbientShadowInfo: ShadowInfo
+ private lateinit var mKeyShadowInfo: ShadowInfo
+ private lateinit var mAmbientShadowInfo: ShadowInfo
init {
- val attributes =
+ updateShadowDrawables(
context.obtainStyledAttributes(
attrs,
R.styleable.DoubleShadowTextView,
defStyleAttr,
- defStyleRes
+ defStyleRes,
)
+ )
+ }
+
+ private fun updateShadowDrawables(attributes: TypedArray) {
val drawableSize: Int
val drawableInsetSize: Int
try {
@@ -70,17 +75,17 @@ constructor(
ambientShadowBlur,
ambientShadowOffsetX,
ambientShadowOffsetY,
- ambientShadowAlpha
+ ambientShadowAlpha,
)
drawableSize =
attributes.getDimensionPixelSize(
R.styleable.DoubleShadowTextView_drawableIconSize,
- 0
+ 0,
)
drawableInsetSize =
attributes.getDimensionPixelSize(
R.styleable.DoubleShadowTextView_drawableIconInsetSize,
- 0
+ 0,
)
} finally {
attributes.recycle()
@@ -95,12 +100,19 @@ constructor(
mAmbientShadowInfo,
drawable,
drawableSize,
- drawableInsetSize
+ drawableInsetSize,
)
}
setCompoundDrawablesRelative(drawables[0], drawables[1], drawables[2], drawables[3])
}
+ override fun setTextAppearance(resId: Int) {
+ super.setTextAppearance(resId)
+ updateShadowDrawables(
+ context.obtainStyledAttributes(resId, R.styleable.DoubleShadowTextView)
+ )
+ }
+
public override fun onDraw(canvas: Canvas) {
applyShadows(mKeyShadowInfo, mAmbientShadowInfo, this, canvas) { super.onDraw(canvas) }
}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
index 6f13d637d5c5..82ac78c6db15 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
@@ -98,12 +98,12 @@ public class QuickStepContract {
public static final long SYSUI_STATE_ONE_HANDED_ACTIVE = 1L << 16;
// Allow system gesture no matter the system bar(s) is visible or not
public static final long SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY = 1L << 17;
- // The IME is showing
- public static final long SYSUI_STATE_IME_SHOWING = 1L << 18;
+ // The IME is visible.
+ public static final long SYSUI_STATE_IME_VISIBLE = 1L << 18;
// The window magnification is overlapped with system gesture insets at the bottom.
public static final long SYSUI_STATE_MAGNIFICATION_OVERLAP = 1L << 19;
- // ImeSwitcher is showing
- public static final long SYSUI_STATE_IME_SWITCHER_SHOWING = 1L << 20;
+ // The IME Switcher button is visible.
+ public static final long SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE = 1L << 20;
// Device dozing/AOD state
public static final long SYSUI_STATE_DEVICE_DOZING = 1L << 21;
// The home feature is disabled (either by SUW/SysUI/device policy)
@@ -134,6 +134,10 @@ public class QuickStepContract {
public static final long SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING = 1L << 34;
// Communal hub is showing
public static final long SYSUI_STATE_COMMUNAL_HUB_SHOWING = 1L << 35;
+ // The back button is visually adjusted to indicate that it will dismiss the IME when pressed.
+ // This only takes effect while the IME is visible. By default, it is set while the IME is
+ // visible, but may be overridden by the backDispositionMode set by the IME.
+ public static final long SYSUI_STATE_BACK_DISMISS_IME = 1L << 36;
// Mask for SystemUiStateFlags to isolate SYSUI_STATE_AWAKE and
// SYSUI_STATE_WAKEFULNESS_TRANSITION, to match WAKEFULNESS_* constants
@@ -168,9 +172,9 @@ public class QuickStepContract {
SYSUI_STATE_DIALOG_SHOWING,
SYSUI_STATE_ONE_HANDED_ACTIVE,
SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY,
- SYSUI_STATE_IME_SHOWING,
+ SYSUI_STATE_IME_VISIBLE,
SYSUI_STATE_MAGNIFICATION_OVERLAP,
- SYSUI_STATE_IME_SWITCHER_SHOWING,
+ SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE,
SYSUI_STATE_DEVICE_DOZING,
SYSUI_STATE_BACK_DISABLED,
SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED,
@@ -185,6 +189,7 @@ public class QuickStepContract {
SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED,
SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING,
SYSUI_STATE_COMMUNAL_HUB_SHOWING,
+ SYSUI_STATE_BACK_DISMISS_IME,
})
public @interface SystemUiStateFlags {}
@@ -244,14 +249,14 @@ public class QuickStepContract {
if ((flags & SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) != 0) {
str.add("allow_gesture");
}
- if ((flags & SYSUI_STATE_IME_SHOWING) != 0) {
+ if ((flags & SYSUI_STATE_IME_VISIBLE) != 0) {
str.add("ime_visible");
}
if ((flags & SYSUI_STATE_MAGNIFICATION_OVERLAP) != 0) {
str.add("magnification_overlap");
}
- if ((flags & SYSUI_STATE_IME_SWITCHER_SHOWING) != 0) {
- str.add("ime_switcher_showing");
+ if ((flags & SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE) != 0) {
+ str.add("ime_switcher_button_visible");
}
if ((flags & SYSUI_STATE_DEVICE_DOZING) != 0) {
str.add("device_dozing");
@@ -295,6 +300,9 @@ public class QuickStepContract {
if ((flags & SYSUI_STATE_COMMUNAL_HUB_SHOWING) != 0) {
str.add("communal_hub_showing");
}
+ if ((flags & SYSUI_STATE_BACK_DISMISS_IME) != 0) {
+ str.add("back_dismiss_ime");
+ }
return str.toString();
}
diff --git a/packages/SystemUI/src/com/android/keyguard/CarrierText.java b/packages/SystemUI/src/com/android/keyguard/CarrierText.java
index ae282c7e14bd..e2ac6a494020 100644
--- a/packages/SystemUI/src/com/android/keyguard/CarrierText.java
+++ b/packages/SystemUI/src/com/android/keyguard/CarrierText.java
@@ -22,6 +22,7 @@ import android.text.TextUtils;
import android.text.method.SingleLineTransformationMethod;
import android.util.AttributeSet;
import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.TextView;
import com.android.systemui.res.R;
@@ -65,6 +66,14 @@ public class CarrierText extends TextView {
}
}
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ // Clear selected state set by CarrierTextController so "selected" not announced by
+ // accessibility but we can still marquee.
+ info.setSelected(false);
+ }
+
public boolean getShowAirplaneMode() {
return mShowAirplaneMode;
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardFingerprintListenModel.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardFingerprintListenModel.kt
index 953cf88feccb..943cbe87c8c2 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardFingerprintListenModel.kt
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardFingerprintListenModel.kt
@@ -49,6 +49,7 @@ data class KeyguardFingerprintListenModel(
var systemUser: Boolean = false,
var udfps: Boolean = false,
var userDoesNotHaveTrust: Boolean = false,
+ var communalShowing: Boolean = false,
) : KeyguardListenModel() {
/** List of [String] to be used as a [Row] with [DumpsysTableLogger]. */
@@ -81,6 +82,7 @@ data class KeyguardFingerprintListenModel(
systemUser.toString(),
udfps.toString(),
userDoesNotHaveTrust.toString(),
+ communalShowing.toString(),
)
}
@@ -122,6 +124,7 @@ data class KeyguardFingerprintListenModel(
systemUser = model.systemUser
udfps = model.udfps
userDoesNotHaveTrust = model.userDoesNotHaveTrust
+ communalShowing = model.communalShowing
}
}
@@ -170,6 +173,7 @@ data class KeyguardFingerprintListenModel(
"systemUser",
"underDisplayFingerprint",
"userDoesNotHaveTrust",
+ "communalShowing",
)
}
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
index 7f176de547bc..0e9d8fec9717 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
@@ -16,6 +16,8 @@
package com.android.keyguard;
+import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED;
+
import static com.android.systemui.Flags.pinInputFieldStyledFocusState;
import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
@@ -164,6 +166,8 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB
layoutParams.height = (int) getResources().getDimension(
R.dimen.keyguard_pin_field_height);
}
+
+ mPasswordEntry.sendAccessibilityEvent(TYPE_VIEW_FOCUSED);
}
private void setKeyboardBasedFocusOutline(boolean isAnyKeyboardConnected) {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 101fcdfce7e5..c266a5b47cff 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -43,6 +43,7 @@ import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STR
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW;
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT;
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN;
+import static com.android.systemui.Flags.glanceableHubV2;
import static com.android.systemui.Flags.simPinBouncerReset;
import static com.android.systemui.Flags.simPinUseSlotId;
import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED;
@@ -128,6 +129,7 @@ import com.android.systemui.biometrics.AuthController;
import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider;
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
@@ -294,6 +296,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
private final Provider<JavaAdapter> mJavaAdapter;
private final Provider<SceneInteractor> mSceneInteractor;
private final Provider<AlternateBouncerInteractor> mAlternateBouncerInteractor;
+ private final CommunalSceneInteractor mCommunalSceneInteractor;
private final AuthController mAuthController;
private final UiEventLogger mUiEventLogger;
private final Set<String> mAllowFingerprintOnOccludingActivitiesFromPackage;
@@ -404,6 +407,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
protected int mFingerprintRunningState = BIOMETRIC_STATE_STOPPED;
private boolean mFingerprintDetectRunning;
private boolean mIsDreaming;
+ private boolean mCommunalShowing;
private int mActiveMobileDataSubscription = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
private final FingerprintInteractiveToAuthProvider mFingerprintInteractiveToAuthProvider;
@@ -2205,7 +2209,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
IActivityTaskManager activityTaskManagerService,
Provider<AlternateBouncerInteractor> alternateBouncerInteractor,
Provider<JavaAdapter> javaAdapter,
- Provider<SceneInteractor> sceneInteractor) {
+ Provider<SceneInteractor> sceneInteractor,
+ CommunalSceneInteractor communalSceneInteractor) {
mContext = context;
mSubscriptionManager = subscriptionManager;
mUserTracker = userTracker;
@@ -2254,6 +2259,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
mAlternateBouncerInteractor = alternateBouncerInteractor;
mJavaAdapter = javaAdapter;
mSceneInteractor = sceneInteractor;
+ mCommunalSceneInteractor = communalSceneInteractor;
mHandler = new Handler(mainLooper) {
@Override
@@ -2535,6 +2541,13 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
);
}
+ if (glanceableHubV2()) {
+ mJavaAdapter.get().alwaysCollectFlow(
+ mCommunalSceneInteractor.isCommunalVisible(),
+ this::onCommunalShowingChanged
+ );
+ }
+
// start() can be invoked in the middle of user switching, so check for this state and issue
// the call manually as that important event was missed.
if (mUserTracker.isUserSwitching()) {
@@ -2837,6 +2850,15 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
}
/**
+ * Sets whether the communal hub is showing.
+ */
+ @VisibleForTesting
+ void onCommunalShowingChanged(boolean showing) {
+ mCommunalShowing = showing;
+ updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
+ }
+
+ /**
* Whether the alternate bouncer is showing.
*/
public void setAlternateBouncerShowing(boolean showing) {
@@ -2998,11 +3020,14 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
final boolean strongerAuthRequired = !isUnlockingWithFingerprintAllowed();
final boolean shouldListenBouncerState =
!strongerAuthRequired || !isPrimaryBouncerShowingOrWillBeShowing();
+ final boolean isUdfpsAuthRequiredOnCommunal =
+ !mCommunalShowing || isAlternateBouncerShowing();
final boolean shouldListenUdfpsState = !isUdfps
|| (!userCanSkipBouncer
&& !strongerAuthRequired
- && userDoesNotHaveTrust);
+ && userDoesNotHaveTrust
+ && (!glanceableHubV2() || isUdfpsAuthRequiredOnCommunal));
boolean shouldListen = shouldListenKeyguardState && shouldListenUserState
@@ -3033,7 +3058,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
mSwitchingUser,
mIsSystemUser,
isUdfps,
- userDoesNotHaveTrust));
+ userDoesNotHaveTrust,
+ mCommunalShowing));
return shouldListen;
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java
index e7470a34a065..102efcf7badd 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java
@@ -17,6 +17,7 @@
package com.android.systemui.accessibility.floatingmenu;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_UNRESTRICTED_GESTURE_EXCLUSION;
import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
import android.content.Context;
@@ -90,9 +91,11 @@ class MenuViewLayerController implements IAccessibilityFloatingMenu {
WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT);
+ params.setTitle("FloatingMenu");
params.receiveInsetsIgnoringZOrder = true;
params.privateFlags |=
- PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION | SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
+ PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION | SYSTEM_FLAG_SHOW_FOR_ALL_USERS
+ | PRIVATE_FLAG_UNRESTRICTED_GESTURE_EXCLUSION;
params.windowAnimations = android.R.style.Animation_Translucent;
// Insets are configured to allow the menu to display over navigation and system bars.
params.setFitInsetsTypes(0);
diff --git a/packages/SystemUI/src/com/android/systemui/grid/ui/compose/SpannedGrids.kt b/packages/SystemUI/src/com/android/systemui/grid/ui/compose/SpannedGrids.kt
index 62ab18bbb738..96ef03c996a9 100644
--- a/packages/SystemUI/src/com/android/systemui/grid/ui/compose/SpannedGrids.kt
+++ b/packages/SystemUI/src/com/android/systemui/grid/ui/compose/SpannedGrids.kt
@@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
@@ -63,6 +64,7 @@ fun HorizontalSpannedGrid(
rowSpacing: Dp,
spans: List<Int>,
modifier: Modifier = Modifier,
+ keys: (spanIndex: Int) -> Any = { it },
composables: @Composable BoxScope.(spanIndex: Int) -> Unit,
) {
SpannedGrid(
@@ -72,6 +74,7 @@ fun HorizontalSpannedGrid(
spans = spans,
isVertical = false,
modifier = modifier,
+ keys = keys,
composables = composables,
)
}
@@ -103,6 +106,7 @@ fun VerticalSpannedGrid(
rowSpacing: Dp,
spans: List<Int>,
modifier: Modifier = Modifier,
+ keys: (spanIndex: Int) -> Any = { it },
composables: @Composable BoxScope.(spanIndex: Int) -> Unit,
) {
SpannedGrid(
@@ -112,6 +116,7 @@ fun VerticalSpannedGrid(
spans = spans,
isVertical = true,
modifier = modifier,
+ keys = keys,
composables = composables,
)
}
@@ -124,6 +129,7 @@ private fun SpannedGrid(
spans: List<Int>,
isVertical: Boolean,
modifier: Modifier = Modifier,
+ keys: (spanIndex: Int) -> Any = { it },
composables: @Composable BoxScope.(spanIndex: Int) -> Unit,
) {
val crossAxisArrangement = Arrangement.spacedBy(crossAxisSpacing)
@@ -167,17 +173,19 @@ private fun SpannedGrid(
Layout(
{
(0 until spans.size).map { spanIndex ->
- Box(
- Modifier.semantics {
- collectionItemInfo =
- if (isVertical) {
- CollectionItemInfo(spanIndex, 1, 0, 1)
- } else {
- CollectionItemInfo(0, 1, spanIndex, 1)
- }
+ key(keys(spanIndex)) {
+ Box(
+ Modifier.semantics {
+ collectionItemInfo =
+ if (isVertical) {
+ CollectionItemInfo(spanIndex, 1, 0, 1)
+ } else {
+ CollectionItemInfo(0, 1, spanIndex, 1)
+ }
+ }
+ ) {
+ composables(spanIndex)
}
- ) {
- composables(spanIndex)
}
}
},
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModel.kt b/packages/SystemUI/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModel.kt
index 84c4bdf1621a..49625f8f9360 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModel.kt
@@ -17,6 +17,7 @@
package com.android.systemui.haptics.msdl.qs
import android.service.quicksettings.Tile
+import androidx.compose.runtime.Stable
import com.android.systemui.Flags
import com.android.systemui.animation.Expandable
import com.android.systemui.dagger.SysUISingleton
@@ -175,6 +176,7 @@ constructor(
}
@SysUISingleton
+@Stable
class TileHapticsViewModelFactoryProvider
@Inject
constructor(private val tileHapticsViewModelFactory: TileHapticsViewModel.Factory) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt
index 6a42cdc876ca..8e4c9349c604 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt
@@ -39,10 +39,14 @@ import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFO
import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT
import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT
import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS
import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS
import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_STICKY_KEYS
import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS
-import com.android.hardware.input.Flags.enableVoiceAccessKeyGestures
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.Accessibility
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.AppCategories
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System
@@ -65,7 +69,6 @@ class InputGestureMaps @Inject constructor(private val context: Context) {
KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to System,
KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to System,
KEY_GESTURE_TYPE_ALL_APPS to System,
- KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS to System,
// Multitasking Category
KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER to MultiTasking,
@@ -82,6 +85,13 @@ class InputGestureMaps @Inject constructor(private val context: Context) {
// App Category
KEY_GESTURE_TYPE_LAUNCH_APPLICATION to AppCategories,
+
+ // Accessibility Category
+ KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS to Accessibility,
+ KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS to Accessibility,
+ KEY_GESTURE_TYPE_TOGGLE_STICKY_KEYS to Accessibility,
+ KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS to Accessibility,
+ KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS to Accessibility,
)
val gestureToInternalKeyboardShortcutGroupLabelResIdMap =
@@ -103,7 +113,6 @@ class InputGestureMaps @Inject constructor(private val context: Context) {
KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to R.string.shortcut_helper_category_system_apps,
KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to
R.string.shortcut_helper_category_system_apps,
- KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS to R.string.shortcut_helper_category_system_apps,
// Multitasking Category
KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT to
@@ -128,6 +137,13 @@ class InputGestureMaps @Inject constructor(private val context: Context) {
// App Category
KEY_GESTURE_TYPE_LAUNCH_APPLICATION to R.string.keyboard_shortcut_group_applications,
+
+ // Accessibility Category
+ KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS to R.string.shortcutHelper_category_accessibility,
+ KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS to R.string.shortcutHelper_category_accessibility,
+ KEY_GESTURE_TYPE_TOGGLE_STICKY_KEYS to R.string.shortcutHelper_category_accessibility,
+ KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS to R.string.shortcutHelper_category_accessibility,
+ KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS to R.string.shortcutHelper_category_accessibility,
)
/**
@@ -152,7 +168,6 @@ class InputGestureMaps @Inject constructor(private val context: Context) {
KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to R.string.group_system_access_google_assistant,
KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to
R.string.group_system_access_google_assistant,
- KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS to R.string.group_system_toggle_voice_access,
// Multitasking Category
KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER to R.string.group_system_cycle_forward,
@@ -169,6 +184,14 @@ class InputGestureMaps @Inject constructor(private val context: Context) {
R.string.system_desktop_mode_toggle_maximize_window,
KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY to
R.string.system_multitasking_move_to_next_display,
+
+ // Accessibility Category
+ KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS to R.string.group_accessibility_toggle_bounce_keys,
+ KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS to R.string.group_accessibility_toggle_mouse_keys,
+ KEY_GESTURE_TYPE_TOGGLE_STICKY_KEYS to R.string.group_accessibility_toggle_sticky_keys,
+ KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS to R.string.group_accessibility_toggle_slow_keys,
+ KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS to
+ R.string.group_accessibility_toggle_voice_access,
)
val shortcutLabelToKeyGestureTypeMap: Map<String, Int>
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/AccessibilityShortcutsSource.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/AccessibilityShortcutsSource.kt
index d92c45591522..0c98f81e7cef 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/AccessibilityShortcutsSource.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/AccessibilityShortcutsSource.kt
@@ -17,9 +17,20 @@
package com.android.systemui.keyboard.shortcut.data.source
import android.content.res.Resources
+import android.hardware.input.InputSettings
+import android.view.KeyEvent.KEYCODE_3
+import android.view.KeyEvent.KEYCODE_4
+import android.view.KeyEvent.KEYCODE_5
+import android.view.KeyEvent.KEYCODE_6
+import android.view.KeyEvent.KEYCODE_V
+import android.view.KeyEvent.META_ALT_ON
+import android.view.KeyEvent.META_META_ON
import android.view.KeyboardShortcutGroup
import android.view.KeyboardShortcutInfo
+import com.android.hardware.input.Flags.enableVoiceAccessKeyGestures
+import com.android.hardware.input.Flags.keyboardA11yShortcutControl
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyboard.shortcut.data.model.shortcutInfo
import com.android.systemui.res.R
import javax.inject.Inject
@@ -33,5 +44,68 @@ class AccessibilityShortcutsSource @Inject constructor(@Main private val resourc
)
)
- private fun accessibilityShortcuts() = listOf<KeyboardShortcutInfo>()
+ private fun accessibilityShortcuts(): List<KeyboardShortcutInfo> {
+ val shortcuts = mutableListOf<KeyboardShortcutInfo>()
+
+ if (keyboardA11yShortcutControl()) {
+ if (InputSettings.isAccessibilityBounceKeysFeatureEnabled()) {
+ shortcuts.add(
+ // Toggle bounce keys:
+ // - Meta + Alt + 3
+ shortcutInfo(
+ resources.getString(R.string.group_accessibility_toggle_bounce_keys)
+ ) {
+ command(META_META_ON or META_ALT_ON, KEYCODE_3)
+ }
+ )
+ }
+ if (InputSettings.isAccessibilityMouseKeysFeatureFlagEnabled()) {
+ shortcuts.add(
+ // Toggle mouse keys:
+ // - Meta + Alt + 4
+ shortcutInfo(
+ resources.getString(R.string.group_accessibility_toggle_mouse_keys)
+ ) {
+ command(META_META_ON or META_ALT_ON, KEYCODE_4)
+ }
+ )
+ }
+ if (InputSettings.isAccessibilityStickyKeysFeatureEnabled()) {
+ shortcuts.add(
+ // Toggle sticky keys:
+ // - Meta + Alt + 5
+ shortcutInfo(
+ resources.getString(R.string.group_accessibility_toggle_sticky_keys)
+ ) {
+ command(META_META_ON or META_ALT_ON, KEYCODE_5)
+ }
+ )
+ }
+ if (InputSettings.isAccessibilitySlowKeysFeatureFlagEnabled()) {
+ shortcuts.add(
+ // Toggle slow keys:
+ // - Meta + Alt + 6
+ shortcutInfo(
+ resources.getString(R.string.group_accessibility_toggle_slow_keys)
+ ) {
+ command(META_META_ON or META_ALT_ON, KEYCODE_6)
+ }
+ )
+ }
+ }
+
+ if (enableVoiceAccessKeyGestures()) {
+ shortcuts.add(
+ // Toggle voice access:
+ // - Meta + Alt + V
+ shortcutInfo(
+ resources.getString(R.string.group_accessibility_toggle_voice_access)
+ ) {
+ command(META_META_ON or META_ALT_ON, KEYCODE_V)
+ }
+ )
+ }
+
+ return shortcuts
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/MultitaskingShortcutsSource.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/MultitaskingShortcutsSource.kt
index d785b5b5a7e7..464201f6ec12 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/MultitaskingShortcutsSource.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/MultitaskingShortcutsSource.kt
@@ -29,12 +29,12 @@ import android.view.KeyEvent.KEYCODE_RIGHT_BRACKET
import android.view.KeyEvent.META_CTRL_ON
import android.view.KeyEvent.META_META_ON
import android.view.KeyboardShortcutGroup
+import android.window.DesktopModeFlags
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyboard.shortcut.data.model.shortcutInfo
import com.android.systemui.res.R
import com.android.window.flags.Flags.enableMoveToNextDisplayShortcut
-import com.android.window.flags.Flags.enableTaskResizingKeyboardShortcuts
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import javax.inject.Inject
@@ -85,7 +85,8 @@ constructor(@Main private val resources: Resources, @Application private val con
)
}
if (
- DesktopModeStatus.canEnterDesktopMode(context) && enableTaskResizingKeyboardShortcuts()
+ DesktopModeStatus.canEnterDesktopMode(context) &&
+ DesktopModeFlags.ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS.isTrue
) {
// Snap a freeform window to the left
// - Meta + Left bracket
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt
index c3c9df97a682..5060abdda247 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt
@@ -32,14 +32,12 @@ import android.view.KeyEvent.KEYCODE_RECENT_APPS
import android.view.KeyEvent.KEYCODE_S
import android.view.KeyEvent.KEYCODE_SLASH
import android.view.KeyEvent.KEYCODE_TAB
-import android.view.KeyEvent.KEYCODE_V
import android.view.KeyEvent.META_ALT_ON
import android.view.KeyEvent.META_CTRL_ON
import android.view.KeyEvent.META_META_ON
import android.view.KeyEvent.META_SHIFT_ON
import android.view.KeyboardShortcutGroup
import android.view.KeyboardShortcutInfo
-import com.android.hardware.input.Flags.enableVoiceAccessKeyGestures
import com.android.systemui.Flags.shortcutHelperKeyGlyph
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyboard.shortcut.data.model.shortcutInfo
@@ -120,8 +118,8 @@ constructor(@Main private val resources: Resources, private val inputManager: In
return shortcuts
}
- private fun systemControlsShortcuts(): List<KeyboardShortcutInfo> {
- val shortcuts = mutableListOf(
+ private fun systemControlsShortcuts() =
+ listOf(
// Access list of all apps and search (i.e. Search/Launcher):
// - Meta
shortcutInfo(resources.getString(R.string.group_system_access_all_apps_search)) {
@@ -178,19 +176,6 @@ constructor(@Main private val resources: Resources, private val inputManager: In
},
)
- if (enableVoiceAccessKeyGestures()) {
- shortcuts.add(
- // Toggle voice access:
- // - Meta + Alt + V
- shortcutInfo(resources.getString(R.string.group_system_toggle_voice_access)) {
- command(META_META_ON or META_ALT_ON, KEYCODE_V)
- }
- )
- }
-
- return shortcuts
- }
-
private fun systemAppsShortcuts() =
listOf(
// Pull up Notes app for quick memo:
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt
index f1945e657d52..bd3d46d09f5e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt
@@ -108,22 +108,33 @@ constructor(
private fun setDialogProperties(dialog: SystemUIDialog, uiState: ShortcutCustomizationUiState) {
dialog.setOnDismissListener { viewModel.onDialogDismissed() }
- dialog.setTitle(
- resources.getString(
- when (uiState) {
- is AddShortcutDialog ->
- R.string.shortcut_customize_mode_add_shortcut_description
- is DeleteShortcutDialog ->
- R.string.shortcut_customize_mode_remove_shortcut_description
- else -> R.string.shortcut_customize_mode_reset_shortcut_description
- }
- )
- )
+ dialog.setTitle("${getDialogTitle(uiState)}. ${getDialogDescription(uiState)}")
// By default, apps cannot intercept action key. The system always handles it. This
// flag is needed to enable customisation dialog window to intercept action key
dialog.window?.addPrivateFlags(PRIVATE_FLAG_ALLOW_ACTION_KEY_EVENTS)
}
+ private fun getDialogTitle(uiState: ShortcutCustomizationUiState): String {
+ return when (uiState) {
+ is AddShortcutDialog -> uiState.shortcutLabel
+ is DeleteShortcutDialog ->
+ resources.getString(R.string.shortcut_customize_mode_remove_shortcut_dialog_title)
+ else ->
+ resources.getString(R.string.shortcut_customize_mode_reset_shortcut_dialog_title)
+ }
+ }
+
+ private fun getDialogDescription(uiState: ShortcutCustomizationUiState): String {
+ return resources.getString(
+ when (uiState) {
+ is AddShortcutDialog -> R.string.shortcut_customize_mode_add_shortcut_description
+ is DeleteShortcutDialog ->
+ R.string.shortcut_customize_mode_remove_shortcut_description
+ else -> R.string.shortcut_customize_mode_reset_shortcut_description
+ }
+ )
+ }
+
@AssistedFactory
interface Factory {
fun create(): ShortcutCustomizationDialogStarter
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt
index 550438aa220f..9d43c48ee274 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt
@@ -59,7 +59,12 @@ import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.LiveRegionMode
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.liveRegion
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.android.compose.ui.graphics.painter.rememberDrawablePainter
@@ -244,7 +249,10 @@ private fun ErrorMessageContainer(errorMessage: String) {
lineHeight = 20.sp,
fontWeight = FontWeight.W500,
color = MaterialTheme.colorScheme.error,
- modifier = Modifier.padding(start = 24.dp).width(252.dp),
+ modifier = Modifier.padding(start = 24.dp).width(252.dp).semantics {
+ contentDescription = errorMessage
+ liveRegion = LiveRegionMode.Polite
+ },
)
}
}
@@ -397,6 +405,7 @@ private fun Description(text: String) {
.width(316.dp)
.wrapContentSize(Alignment.Center),
color = MaterialTheme.colorScheme.onSurfaceVariant,
+ textAlign = TextAlign.Center
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index 5e0768a2fd24..f37e7685f21c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -27,6 +27,7 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
+import com.android.systemui.keyguard.domain.interactor.WallpaperFocalAreaInteractor
import com.android.systemui.keyguard.ui.binder.KeyguardBlueprintViewBinder
import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder
import com.android.systemui.keyguard.ui.binder.LightRevealScrimViewBinder
@@ -79,6 +80,7 @@ constructor(
private val keyguardClockViewModel: KeyguardClockViewModel,
private val smartspaceViewModel: KeyguardSmartspaceViewModel,
private val clockInteractor: KeyguardClockInteractor,
+ private val wallpaperFocalAreaInteractor: WallpaperFocalAreaInteractor,
private val keyguardViewMediator: KeyguardViewMediator,
private val deviceEntryUnlockTrackerViewBinder: Optional<DeviceEntryUnlockTrackerViewBinder>,
private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
@@ -139,6 +141,7 @@ constructor(
screenOffAnimationController,
shadeInteractor,
clockInteractor,
+ wallpaperFocalAreaInteractor,
keyguardClockViewModel,
interactionJankMonitor,
deviceEntryHapticsInteractor,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt
index a45204d41718..80675d373b8e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt
@@ -28,7 +28,6 @@ object BuiltInKeyguardQuickAffordanceKeys {
const val CREATE_NOTE = "create_note"
const val DO_NOT_DISTURB = "do_not_disturb"
const val FLASHLIGHT = "flashlight"
- const val GLANCEABLE_HUB = "glanceable_hub"
const val HOME_CONTROLS = "home"
const val MUTE = "mute"
const val QR_CODE_SCANNER = "qr_code_scanner"
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfig.kt
deleted file mode 100644
index 96b07cc84705..000000000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfig.kt
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.keyguard.data.quickaffordance
-
-import android.content.Context
-import android.content.Intent
-import android.provider.Settings
-import android.util.Log
-import com.android.systemui.animation.Expandable
-import com.android.systemui.common.shared.model.ContentDescription
-import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.communal.data.repository.CommunalSceneRepository
-import com.android.systemui.communal.domain.interactor.CommunalInteractor
-import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
-import com.android.systemui.communal.shared.model.CommunalScenes
-import com.android.systemui.communal.shared.model.CommunalTransitionKeys
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.res.R
-import com.android.systemui.scene.domain.interactor.SceneInteractor
-import com.android.systemui.scene.shared.flag.SceneContainerFlag
-import com.android.systemui.scene.shared.model.Scenes
-import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
-
-/** Lockscreen affordance that opens the glanceable hub. */
-@SysUISingleton
-class GlanceableHubQuickAffordanceConfig
-@Inject
-constructor(
- @Application private val context: Context,
- private val communalSceneRepository: CommunalSceneRepository,
- private val communalInteractor: CommunalInteractor,
- private val communalSettingsInteractor: CommunalSettingsInteractor,
- private val sceneInteractor: SceneInteractor,
-) : KeyguardQuickAffordanceConfig {
-
- private val pickerNameResourceId = R.string.glanceable_hub_lockscreen_affordance_label
-
- override val key: String = BuiltInKeyguardQuickAffordanceKeys.GLANCEABLE_HUB
-
- override fun pickerName(): String = context.getString(pickerNameResourceId)
-
- override val pickerIconResourceId: Int
- get() = R.drawable.ic_widgets
-
- override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState>
- get() =
- communalInteractor.isCommunalAvailable.map { available ->
- if (!communalSettingsInteractor.isV2FlagEnabled()) {
- Log.i(TAG, "Button hidden on lockscreen: flag not enabled.")
- KeyguardQuickAffordanceConfig.LockScreenState.Hidden
- } else if (!available) {
- Log.i(TAG, "Button hidden on lockscreen: hub not available.")
- KeyguardQuickAffordanceConfig.LockScreenState.Hidden
- } else {
- KeyguardQuickAffordanceConfig.LockScreenState.Visible(
- icon =
- Icon.Resource(
- pickerIconResourceId,
- ContentDescription.Resource(pickerNameResourceId),
- )
- )
- }
- }
-
- override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState {
- return if (!communalSettingsInteractor.isV2FlagEnabled()) {
- Log.i(TAG, "Button unavailable in picker: flag not enabled.")
- KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
- } else if (!communalInteractor.isCommunalEnabled.value) {
- Log.i(TAG, "Button disabled in picker: hub not enabled in settings.")
- KeyguardQuickAffordanceConfig.PickerScreenState.Disabled(
- explanation =
- context.getString(R.string.glanceable_hub_lockscreen_affordance_disabled_text),
- actionText =
- context.getString(
- R.string.glanceable_hub_lockscreen_affordance_action_button_label
- ),
- actionIntent = Intent(Settings.ACTION_LOCKSCREEN_SETTINGS),
- )
- } else {
- KeyguardQuickAffordanceConfig.PickerScreenState.Default()
- }
- }
-
- override fun onTriggered(
- expandable: Expandable?
- ): KeyguardQuickAffordanceConfig.OnTriggeredResult {
- if (SceneContainerFlag.isEnabled) {
- sceneInteractor.changeScene(Scenes.Communal, "lockscreen to communal from shortcut")
- } else {
- communalSceneRepository.changeScene(
- CommunalScenes.Communal,
- transitionKey = CommunalTransitionKeys.SimpleFade,
- )
- }
- return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(true)
- }
-
- companion object {
- private const val TAG = "GlanceableHubQuickAffordanceConfig"
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
index 8c6fdb989daf..787a9837b860 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
@@ -36,7 +36,6 @@ interface KeyguardDataQuickAffordanceModule {
camera: CameraQuickAffordanceConfig,
doNotDisturb: DoNotDisturbQuickAffordanceConfig,
flashlight: FlashlightQuickAffordanceConfig,
- glanceableHub: GlanceableHubQuickAffordanceConfig,
home: HomeControlsKeyguardQuickAffordanceConfig,
mute: MuteQuickAffordanceConfig,
quickAccessWallet: QuickAccessWalletKeyguardQuickAffordanceConfig,
@@ -47,7 +46,6 @@ interface KeyguardDataQuickAffordanceModule {
camera,
doNotDisturb,
flashlight,
- glanceableHub,
home,
mute,
quickAccessWallet,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index a39982dd31e7..11477fb6cad1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.data.repository
import android.graphics.Point
+import android.graphics.RectF
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.internal.widget.LockPatternUtils
import com.android.keyguard.KeyguardUpdateMonitor
@@ -258,6 +259,8 @@ interface KeyguardRepository {
val notificationStackAbsoluteBottom: StateFlow<Float>
+ val wallpaperFocalAreaBounds: StateFlow<RectF>
+
/**
* Returns `true` if the keyguard is showing; `false` otherwise.
*
@@ -329,6 +332,8 @@ interface KeyguardRepository {
* this value
*/
fun setNotificationStackAbsoluteBottom(bottom: Float)
+
+ fun setWallpaperFocalAreaBounds(bounds: RectF)
}
/** Encapsulates application state for the keyguard. */
@@ -380,7 +385,6 @@ constructor(
override val onCameraLaunchDetected = MutableStateFlow(CameraLaunchSourceModel())
override val panelAlpha: MutableStateFlow<Float> = MutableStateFlow(1f)
-
override val topClippingBounds = MutableStateFlow<Int?>(null)
override val isKeyguardShowing: MutableStateFlow<Boolean> =
@@ -622,6 +626,10 @@ constructor(
private val _notificationStackAbsoluteBottom = MutableStateFlow(0F)
override val notificationStackAbsoluteBottom = _notificationStackAbsoluteBottom.asStateFlow()
+ private val _wallpaperFocalAreaBounds = MutableStateFlow(RectF(0F, 0F, 0F, 0F))
+ override val wallpaperFocalAreaBounds: StateFlow<RectF> =
+ _wallpaperFocalAreaBounds.asStateFlow()
+
init {
val callback =
object : KeyguardStateController.Callback {
@@ -700,6 +708,10 @@ constructor(
_notificationStackAbsoluteBottom.value = bottom
}
+ override fun setWallpaperFocalAreaBounds(bounds: RectF) {
+ _wallpaperFocalAreaBounds.value = bounds
+ }
+
private fun dozeMachineStateToModel(state: DozeMachine.State): DozeStateModel {
return when (state) {
DozeMachine.State.UNINITIALIZED -> DozeStateModel.UNINITIALIZED
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WallpaperFocalAreaInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WallpaperFocalAreaInteractor.kt
new file mode 100644
index 000000000000..934afe248a36
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WallpaperFocalAreaInteractor.kt
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.RectF
+import android.util.TypedValue
+import com.android.app.animation.MathUtils.max
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardClockRepository
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import com.android.systemui.res.R
+import com.android.systemui.shade.data.repository.ShadeRepository
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
+import com.android.systemui.wallpapers.data.repository.WallpaperRepository
+import javax.inject.Inject
+import kotlin.math.min
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
+
+@SysUISingleton
+class WallpaperFocalAreaInteractor
+@Inject
+constructor(
+ @Application private val applicationScope: CoroutineScope,
+ context: Context,
+ private val keyguardRepository: KeyguardRepository,
+ shadeRepository: ShadeRepository,
+ activeNotificationsInteractor: ActiveNotificationsInteractor,
+ keyguardClockRepository: KeyguardClockRepository,
+ wallpaperRepository: WallpaperRepository,
+) {
+ // When there's notifications in splitshade, magic portrait shape effects should be left
+ // aligned in foldable
+ private val notificationInShadeWideLayout: Flow<Boolean> =
+ combine(
+ shadeRepository.isShadeLayoutWide,
+ activeNotificationsInteractor.areAnyNotificationsPresent,
+ ) { isShadeLayoutWide, areAnyNotificationsPresent: Boolean ->
+ when {
+ !isShadeLayoutWide -> false
+ !areAnyNotificationsPresent -> false
+ else -> true
+ }
+ }
+
+ val shouldSendFocalArea = wallpaperRepository.shouldSendFocalArea
+ val wallpaperFocalAreaBounds: StateFlow<RectF?> =
+ combine(
+ shadeRepository.isShadeLayoutWide,
+ notificationInShadeWideLayout,
+ keyguardRepository.notificationStackAbsoluteBottom,
+ keyguardRepository.shortcutAbsoluteTop,
+ keyguardClockRepository.notificationDefaultTop,
+ ) {
+ isShadeLayoutWide,
+ notificationInShadeWideLayout,
+ notificationStackAbsoluteBottom,
+ shortcutAbsoluteTop,
+ notificationDefaultTop ->
+ // Wallpaper will be zoomed in with config_wallpaperMaxScale in lockscreen
+ // so we need to give a bounds taking this scale in consideration
+ val wallpaperZoomedInScale = getSystemWallpaperMaximumScale(context)
+ val screenBounds =
+ RectF(
+ 0F,
+ 0F,
+ context.resources.displayMetrics.widthPixels.toFloat(),
+ context.resources.displayMetrics.heightPixels.toFloat(),
+ )
+ val scaledBounds =
+ RectF(
+ screenBounds.centerX() - screenBounds.width() / 2F / wallpaperZoomedInScale,
+ screenBounds.centerY() -
+ screenBounds.height() / 2F / wallpaperZoomedInScale,
+ screenBounds.centerX() + screenBounds.width() / 2F / wallpaperZoomedInScale,
+ screenBounds.centerY() + screenBounds.height() / 2F / wallpaperZoomedInScale,
+ )
+ val maxFocalAreaWidth =
+ TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ FOCAL_AREA_MAX_WIDTH_DP.toFloat(),
+ context.resources.displayMetrics,
+ )
+ val (left, right) =
+ // tablet landscape
+ if (context.resources.getBoolean(R.bool.center_align_magic_portrait_shape)) {
+ Pair(
+ scaledBounds.centerX() - maxFocalAreaWidth / 2F,
+ scaledBounds.centerX() + maxFocalAreaWidth / 2F,
+ )
+ // unfold foldable landscape
+ } else if (isShadeLayoutWide) {
+ if (notificationInShadeWideLayout) {
+ Pair(scaledBounds.left, scaledBounds.centerX())
+ } else {
+ Pair(scaledBounds.centerX(), scaledBounds.right)
+ }
+ // handheld / portrait
+ } else {
+ val focalAreaWidth = min(scaledBounds.width(), maxFocalAreaWidth)
+ Pair(
+ scaledBounds.centerX() - focalAreaWidth / 2F,
+ scaledBounds.centerX() + focalAreaWidth / 2F,
+ )
+ }
+ val scaledBottomMargin =
+ (context.resources.displayMetrics.heightPixels - shortcutAbsoluteTop) /
+ wallpaperZoomedInScale
+ val top =
+ // tablet landscape
+ if (context.resources.getBoolean(R.bool.center_align_magic_portrait_shape)) {
+ // no strict constraints for top, use bottom margin to make it symmetric
+ // vertically
+ scaledBounds.top + scaledBottomMargin
+ }
+ // unfold foldable landscape
+ else if (isShadeLayoutWide) {
+ // For all landscape, we should use bottom of smartspace to constrain
+ scaledBounds.top + notificationDefaultTop / wallpaperZoomedInScale
+ // handheld / portrait
+ } else {
+ scaledBounds.top +
+ max(notificationDefaultTop, notificationStackAbsoluteBottom) /
+ wallpaperZoomedInScale
+ }
+ val bottom = scaledBounds.bottom - scaledBottomMargin
+ RectF(left, top, right, bottom)
+ }
+ .stateIn(
+ applicationScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = null,
+ )
+
+ fun setWallpaperFocalAreaBounds(bounds: RectF) {
+ keyguardRepository.setWallpaperFocalAreaBounds(bounds)
+ }
+
+ companion object {
+ fun getSystemWallpaperMaximumScale(context: Context): Float {
+ return context.resources.getFloat(
+ Resources.getSystem()
+ .getIdentifier(
+ /* name= */ "config_wallpaperMaxScale",
+ /* defType= */ "dimen",
+ /* defPackage= */ "android",
+ )
+ )
+ }
+
+ // A max width for magic portrait shape effects bounds, to avoid it going too large
+ // in large screen portrait mode
+ const val FOCAL_AREA_MAX_WIDTH_DP = 500
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
index 92b49ed6156c..21c9b0b82b2d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
@@ -23,6 +23,7 @@ import android.widget.TextView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.app.tracing.coroutines.launchTraced as launch
+import com.android.systemui.Flags
import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.res.R
@@ -73,7 +74,6 @@ object KeyguardIndicationAreaBinder {
disposables +=
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
-
launch("$TAG#viewModel.indicationAreaTranslationX") {
viewModel.indicationAreaTranslationX.collect { translationX ->
view.translationX = translationX
@@ -119,6 +119,9 @@ object KeyguardIndicationAreaBinder {
launch("$TAG#viewModel.configurationChange") {
viewModel.configurationChange.collect {
configurationBasedDimensions.value = loadFromResources(view)
+ if (Flags.indicationTextA11yFix()) {
+ indicationController.onConfigurationChanged()
+ }
}
}
@@ -140,7 +143,7 @@ object KeyguardIndicationAreaBinder {
view.resources.getDimensionPixelOffset(R.dimen.keyguard_indication_area_padding),
indicationTextSizePx =
view.resources.getDimensionPixelSize(
- com.android.internal.R.dimen.text_size_small_material,
+ com.android.internal.R.dimen.text_size_small_material
),
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index 6d270b219c81..d8bd4452f2a6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -54,6 +54,7 @@ import com.android.systemui.customization.R as customR
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor
import com.android.systemui.keyguard.KeyguardViewMediator
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
+import com.android.systemui.keyguard.domain.interactor.WallpaperFocalAreaInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
@@ -87,6 +88,7 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
@@ -105,6 +107,7 @@ object KeyguardRootViewBinder {
screenOffAnimationController: ScreenOffAnimationController,
shadeInteractor: ShadeInteractor,
clockInteractor: KeyguardClockInteractor,
+ wallpaperFocalAreaInteractor: WallpaperFocalAreaInteractor,
clockViewModel: KeyguardClockViewModel,
interactionJankMonitor: InteractionJankMonitor?,
deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor?,
@@ -309,12 +312,15 @@ object KeyguardRootViewBinder {
.setTag(clockId)
jankMonitor.begin(builder)
}
+
TransitionState.CANCELED ->
jankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD)
+
TransitionState.FINISHED -> {
keyguardViewMediator?.maybeHandlePendingLock()
jankMonitor.end(CUJ_SCREEN_OFF_SHOW_AOD)
}
+
TransitionState.RUNNING -> Unit
}
}
@@ -378,6 +384,21 @@ object KeyguardRootViewBinder {
}
disposables +=
+ view.repeatWhenAttached {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ if (viewModel.shouldSendFocalArea.value) {
+ launch {
+ wallpaperFocalAreaInteractor.wallpaperFocalAreaBounds
+ .filterNotNull()
+ .collect {
+ wallpaperFocalAreaInteractor.setWallpaperFocalAreaBounds(it)
+ }
+ }
+ }
+ }
+ }
+
+ disposables +=
view.onLayoutChanged(
OnLayoutChange(
viewModel,
@@ -523,6 +544,7 @@ object KeyguardRootViewBinder {
View.INVISIBLE
}
}
+
else -> {
if (isVisible.value) {
CrossFadeHelper.fadeIn(this, animatorListener)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
index a2107871a585..7605bdd3d7b5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
@@ -55,6 +55,7 @@ import com.android.systemui.customization.R as customR
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.domain.interactor.WallpaperFocalAreaInteractor
import com.android.systemui.keyguard.shared.model.ClockSizeSetting
import com.android.systemui.keyguard.ui.binder.KeyguardPreviewClockViewBinder
import com.android.systemui.keyguard.ui.binder.KeyguardPreviewSmartspaceViewBinder
@@ -117,6 +118,7 @@ constructor(
private val secureSettings: SecureSettings,
private val defaultShortcutsSection: DefaultShortcutsSection,
private val keyguardQuickAffordanceViewBinder: KeyguardQuickAffordanceViewBinder,
+ private val wallpaperFocalAreaInteractor: WallpaperFocalAreaInteractor,
) {
val hostToken: IBinder? = bundle.getBinder(KEY_HOST_TOKEN)
private val width: Int = bundle.getInt(KEY_VIEW_WIDTH)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt
index 3eb8522e0338..542fb9b46bef 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt
@@ -23,10 +23,4 @@ data class BlurConfig(val minBlurRadiusPx: Float, val maxBlurRadiusPx: Float) {
// No-op config that will be used by dagger of other SysUI variants which don't blur the
// background surface.
@Inject constructor() : this(0.0f, 0.0f)
-
- companion object {
- // Blur the shade much lesser than the background surface so that the surface is
- // distinguishable from the background.
- @JvmStatic fun Float.maxBlurRadiusToNotificationPanelBlurRadius(): Float = this / 3.0f
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt
index 733d7d71061e..b531c7fa49ec 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt
@@ -24,7 +24,6 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCE
import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
import com.android.systemui.keyguard.ui.transitions.BlurConfig
-import com.android.systemui.keyguard.ui.transitions.BlurConfig.Companion.maxBlurRadiusToNotificationPanelBlurRadius
import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
import com.android.systemui.scene.shared.flag.SceneContainerFlag
@@ -89,9 +88,7 @@ constructor(
shadeDependentFlows.transitionFlow(
flowWhenShadeIsNotExpanded = emptyFlow(),
flowWhenShadeIsExpanded =
- transitionAnimation.immediatelyTransitionTo(
- blurConfig.maxBlurRadiusPx.maxBlurRadiusToNotificationPanelBlurRadius()
- ),
+ transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx),
)
} else {
emptyFlow<Float>()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModel.kt
index e68e465ed55a..ba03c48c65e9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModel.kt
@@ -16,10 +16,50 @@
package com.android.systemui.keyguard.ui.viewmodel
+import androidx.compose.runtime.getValue
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.lifecycle.Hydrator
import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
-import javax.inject.Inject
-import kotlinx.coroutines.flow.StateFlow
+import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.flow.flowOf
-class KeyguardMediaViewModel @Inject constructor(mediaCarouselInteractor: MediaCarouselInteractor) {
- val isMediaVisible: StateFlow<Boolean> = mediaCarouselInteractor.hasActiveMediaOrRecommendation
+class KeyguardMediaViewModel
+@AssistedInject
+constructor(
+ mediaCarouselInteractor: MediaCarouselInteractor,
+ keyguardInteractor: KeyguardInteractor,
+) : ExclusiveActivatable() {
+
+ private val hydrator = Hydrator("KeyguardMediaViewModel.hydrator")
+ /**
+ * Whether media carousel is visible on lockscreen. Media may be presented on lockscreen but
+ * still hidden on certain surfaces like AOD
+ */
+ val isMediaVisible: Boolean by
+ hydrator.hydratedStateOf(
+ traceName = "isMediaVisible",
+ source =
+ keyguardInteractor.isDozing.flatMapLatestConflated { isDozing ->
+ if (isDozing) {
+ flowOf(false)
+ } else {
+ mediaCarouselInteractor.hasActiveMediaOrRecommendation
+ }
+ },
+ initialValue =
+ !keyguardInteractor.isDozing.value &&
+ mediaCarouselInteractor.hasActiveMediaOrRecommendation.value,
+ )
+
+ override suspend fun onActivated(): Nothing {
+ hydrator.activate()
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(): KeyguardMediaViewModel
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index e51e05b8ab61..aa4293a201ac 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -28,6 +28,7 @@ import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.domain.interactor.PulseExpansionInteractor
+import com.android.systemui.keyguard.domain.interactor.WallpaperFocalAreaInteractor
import com.android.systemui.keyguard.shared.model.Edge
import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
@@ -133,6 +134,7 @@ constructor(
private val screenOffAnimationController: ScreenOffAnimationController,
private val aodBurnInViewModel: AodBurnInViewModel,
private val shadeInteractor: ShadeInteractor,
+ wallpaperFocalAreaInteractor: WallpaperFocalAreaInteractor,
) {
val burnInLayerVisibility: Flow<Int> =
keyguardTransitionInteractor.startedKeyguardTransitionStep
@@ -362,6 +364,8 @@ constructor(
initialValue = AnimatedValue.NotAnimating(false),
)
+ val shouldSendFocalArea = wallpaperFocalAreaInteractor.shouldSendFocalArea
+
fun onNotificationContainerBoundsChanged(top: Float, bottom: Float, animate: Boolean = false) {
keyguardInteractor.setNotificationContainerBounds(
NotificationContainerBounds(top = top, bottom = bottom, isAnimated = animate)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
index 44c4c8723dcb..89dcbf6aa52b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
@@ -24,7 +24,6 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
import com.android.systemui.keyguard.ui.transitions.BlurConfig
-import com.android.systemui.keyguard.ui.transitions.BlurConfig.Companion.maxBlurRadiusToNotificationPanelBlurRadius
import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
import com.android.systemui.scene.shared.flag.SceneContainerFlag
@@ -88,9 +87,7 @@ constructor(
shadeDependentFlows.transitionFlow(
flowWhenShadeIsNotExpanded = emptyFlow(),
flowWhenShadeIsExpanded =
- transitionAnimation.immediatelyTransitionTo(
- blurConfig.maxBlurRadiusPx.maxBlurRadiusToNotificationPanelBlurRadius()
- ),
+ transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx),
)
} else {
emptyFlow()
diff --git a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt
index 425e674ec804..fb69b793d975 100644
--- a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt
@@ -21,6 +21,7 @@ import com.android.systemui.dump.DumpManager
import com.android.systemui.log.LogBufferHelper.Companion.adjustMaxSize
import com.android.systemui.log.LogcatEchoTracker
import com.android.systemui.util.time.SystemClock
+import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
@SysUISingleton
@@ -31,7 +32,7 @@ constructor(
private val systemClock: SystemClock,
private val logcatEchoTracker: LogcatEchoTracker,
) {
- private val existingBuffers = mutableMapOf<String, TableLogBuffer>()
+ private val existingBuffers = ConcurrentHashMap<String, TableLogBuffer>()
/**
* Creates a new [TableLogBuffer]. This method should only be called from static contexts, where
@@ -42,17 +43,9 @@ constructor(
* @param maxSize the buffer max size. See [adjustMaxSize]
* @return a new [TableLogBuffer] registered with [DumpManager]
*/
- fun create(
- name: String,
- maxSize: Int,
- ): TableLogBuffer {
+ fun create(name: String, maxSize: Int): TableLogBuffer {
val tableBuffer =
- TableLogBuffer(
- adjustMaxSize(maxSize),
- name,
- systemClock,
- logcatEchoTracker,
- )
+ TableLogBuffer(adjustMaxSize(maxSize), name, systemClock, logcatEchoTracker)
dumpManager.registerTableLogBuffer(name, tableBuffer)
return tableBuffer
}
@@ -66,13 +59,12 @@ constructor(
*
* @return a [TableLogBuffer] suitable for reuse
*/
- fun getOrCreate(
- name: String,
- maxSize: Int,
- ): TableLogBuffer =
- existingBuffers.getOrElse(name) {
- val buffer = create(name, maxSize)
- existingBuffers[name] = buffer
- buffer
+ fun getOrCreate(name: String, maxSize: Int): TableLogBuffer =
+ synchronized(existingBuffers) {
+ existingBuffers.getOrElse(name) {
+ val buffer = create(name, maxSize)
+ existingBuffers[name] = buffer
+ buffer
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
index 173a964cc5d3..0de8c40bddaa 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
@@ -536,10 +536,12 @@ public final class NavBarHelper implements
}
/**
- * @return Whether the IME is shown on top of the screen given the {@code vis} flag of
- * {@link InputMethodService} and the keyguard states.
+ * Checks whether the IME is visible on top of the screen, based on the given IME window
+ * visibility flags, and the current keyguard state.
+ *
+ * @param vis the IME window visibility.
*/
- public boolean isImeShown(@ImeWindowVisibility int vis) {
+ public boolean isImeVisible(@ImeWindowVisibility int vis) {
View shadeWindowView = mNotificationShadeWindowController.getWindowRootView();
boolean isKeyguardShowing = mKeyguardStateController.isShowing();
boolean imeVisibleOnShade = shadeWindowView != null && shadeWindowView.isAttachedToWindow()
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
index 05d8bff2ceb6..de35dd7b52b6 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
@@ -17,8 +17,9 @@
package com.android.systemui.navigationbar;
import static android.app.ActivityManager.LOCK_TASK_MODE_PINNED;
-import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN;
-import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN;
+import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_DISMISS_IME;
+import static android.app.StatusBarManager.NAVIGATION_HINT_IME_VISIBLE;
+import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE;
import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
@@ -30,13 +31,15 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BACK_DISABLED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BACK_DISMISS_IME;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_VISIBLE;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
import android.app.StatusBarManager;
+import android.app.StatusBarManager.NavigationHint;
import android.app.StatusBarManager.WindowVisibleState;
import android.content.Context;
import android.graphics.Rect;
@@ -111,6 +114,7 @@ public class TaskbarDelegate implements CommandQueue.Callbacks,
private TaskStackChangeListeners mTaskStackChangeListeners;
private Optional<Pip> mPipOptional;
private int mDefaultDisplayId;
+ @NavigationHint
private int mNavigationIconHints;
private final NavBarHelper.NavbarTaskbarStateUpdater mNavbarTaskbarStateUpdater =
new NavBarHelper.NavbarTaskbarStateUpdater() {
@@ -261,6 +265,20 @@ public class TaskbarDelegate implements CommandQueue.Callbacks,
}
}
+ @Override
+ public void onDisplayRemoveSystemDecorations(int displayId) {
+ CommandQueue.Callbacks.super.onDisplayRemoveSystemDecorations(displayId);
+ if (mOverviewProxyService.getProxy() == null) {
+ return;
+ }
+
+ try {
+ mOverviewProxyService.getProxy().onDisplayRemoveSystemDecorations(displayId);
+ } catch (RemoteException e) {
+ Log.e(TAG, "onDisplaySystemDecorationsRemoved() failed", e);
+ }
+ }
+
// Separated into a method to keep setDependencies() clean/readable.
private LightBarTransitionsController createLightBarTransitionsController() {
@@ -360,10 +378,12 @@ public class TaskbarDelegate implements CommandQueue.Callbacks,
mSysUiState.setFlag(SYSUI_STATE_A11Y_BUTTON_CLICKABLE, clickable)
.setFlag(SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE, longClickable)
- .setFlag(SYSUI_STATE_IME_SHOWING,
- (mNavigationIconHints & NAVIGATION_HINT_IME_SHOWN) != 0)
- .setFlag(SYSUI_STATE_IME_SWITCHER_SHOWING,
- (mNavigationIconHints & NAVIGATION_HINT_IME_SWITCHER_SHOWN) != 0)
+ .setFlag(SYSUI_STATE_IME_VISIBLE,
+ (mNavigationIconHints & NAVIGATION_HINT_IME_VISIBLE) != 0)
+ .setFlag(SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE,
+ (mNavigationIconHints & NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE) != 0)
+ .setFlag(SYSUI_STATE_BACK_DISMISS_IME,
+ (mNavigationIconHints & NAVIGATION_HINT_BACK_DISMISS_IME) != 0)
.setFlag(SYSUI_STATE_OVERVIEW_DISABLED,
(mDisabledFlags & View.STATUS_BAR_DISABLE_RECENT) != 0)
.setFlag(SYSUI_STATE_HOME_DISABLED,
@@ -483,18 +503,17 @@ public class TaskbarDelegate implements CommandQueue.Callbacks,
@Override
public void setImeWindowStatus(int displayId, @ImeWindowVisibility int vis,
@BackDispositionMode int backDisposition, boolean showImeSwitcher) {
- boolean imeShown = mNavBarHelper.isImeShown(vis);
- if (!imeShown) {
- // Count imperceptible changes as visible so we transition taskbar out quickly.
- imeShown = (vis & InputMethodService.IME_VISIBLE_IMPERCEPTIBLE) != 0;
- }
- showImeSwitcher = imeShown && showImeSwitcher;
- int hints = Utilities.calculateBackDispositionHints(mNavigationIconHints, backDisposition,
- imeShown, showImeSwitcher);
- if (hints != mNavigationIconHints) {
- mNavigationIconHints = hints;
- updateSysuiFlags();
+ // Count imperceptible changes as visible so we transition taskbar out quickly.
+ final boolean isImeVisible = mNavBarHelper.isImeVisible(vis)
+ || (vis & InputMethodService.IME_VISIBLE_IMPERCEPTIBLE) != 0;
+ final int hints = Utilities.calculateNavigationIconHints(mNavigationIconHints,
+ backDisposition, isImeVisible, showImeSwitcher);
+ if (hints == mNavigationIconHints) {
+ return;
}
+
+ mNavigationIconHints = hints;
+ updateSysuiFlags();
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java
index c78750718cf0..f1fe2802286c 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java
@@ -17,12 +17,14 @@
package com.android.systemui.navigationbar.views;
import static android.app.ActivityManager.LOCK_TASK_MODE_PINNED;
-import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN;
-import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN;
+import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_DISMISS_IME;
+import static android.app.StatusBarManager.NAVIGATION_HINT_IME_VISIBLE;
+import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE;
import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN;
import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
import static android.app.StatusBarManager.WindowType;
import static android.app.StatusBarManager.WindowVisibleState;
+import static android.app.StatusBarManager.navigationHintsToString;
import static android.app.StatusBarManager.windowStateToString;
import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
import static android.view.InsetsSource.FLAG_SUPPRESS_SCRIM;
@@ -42,8 +44,9 @@ import static com.android.systemui.shared.statusbar.phone.BarTransitions.Transit
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BACK_DISMISS_IME;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_VISIBLE;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
import static com.android.systemui.shared.system.QuickStepContract.isGesturalMode;
@@ -56,6 +59,7 @@ import android.annotation.NonNull;
import android.app.ActivityTaskManager;
import android.app.IActivityTaskManager;
import android.app.StatusBarManager;
+import android.app.StatusBarManager.NavigationHint;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Insets;
@@ -233,6 +237,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements
private @WindowVisibleState int mNavigationBarWindowState = WINDOW_STATE_SHOWING;
+ @NavigationHint
private int mNavigationIconHints = 0;
private @TransitionMode int mTransitionMode;
private boolean mLongPressHomeEnabled;
@@ -649,18 +654,18 @@ public class NavigationBar extends ViewController<NavigationBarView> implements
if (!mEdgeBackGestureHandler.isHandlingGestures()) {
// We're in 2/3 button mode OR back button force-shown in SUW
if (!mImeVisible) {
- // IME not showing, take all touches
+ // IME is not visible, take all touches
info.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_FRAME);
return;
}
if (!mView.isImeRenderingNavButtons()) {
- // IME showing but not drawing any buttons, take all touches
+ // IME is visible but not drawing any buttons, take all touches
info.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_FRAME);
return;
}
}
- // When in gestural and the IME is showing, don't use the nearest region since it will
+ // When in gestural and the IME is visible, don't use the nearest region since it will
// take gesture space away from the IME
info.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
info.touchableRegion.set(
@@ -817,7 +822,6 @@ public class NavigationBar extends ViewController<NavigationBarView> implements
if (mSavedState != null) {
getBarTransitions().getLightTransitionsController().restoreState(mSavedState);
}
- setNavigationIconHints(mNavigationIconHints);
setWindowVisible(isNavBarWindowVisible());
mView.setBehavior(mBehavior);
setNavBarMode(mNavBarMode);
@@ -1111,6 +1115,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements
pw.println(" mLongPressHomeEnabled=" + mLongPressHomeEnabled);
pw.println(" mNavigationBarWindowState="
+ windowStateToString(mNavigationBarWindowState));
+ pw.println(" mNavigationIconHints=" + navigationHintsToString(mNavigationIconHints));
pw.println(" mTransitionMode="
+ BarTransitions.modeToString(mTransitionMode));
pw.println(" mTransientShown=" + mTransientShown);
@@ -1135,11 +1140,12 @@ public class NavigationBar extends ViewController<NavigationBarView> implements
if (displayId != mDisplayId) {
return;
}
- boolean imeShown = mNavBarHelper.isImeShown(vis);
- showImeSwitcher = imeShown && showImeSwitcher;
- int hints = Utilities.calculateBackDispositionHints(mNavigationIconHints, backDisposition,
- imeShown, showImeSwitcher);
- if (hints == mNavigationIconHints) return;
+ final boolean isImeVisible = mNavBarHelper.isImeVisible(vis);
+ final int hints = Utilities.calculateNavigationIconHints(mNavigationIconHints,
+ backDisposition, isImeVisible, showImeSwitcher);
+ if (hints == mNavigationIconHints) {
+ return;
+ }
setNavigationIconHints(hints);
checkBarModes();
@@ -1680,10 +1686,12 @@ public class NavigationBar extends ViewController<NavigationBarView> implements
mSysUiFlagsContainer.setFlag(SYSUI_STATE_A11Y_BUTTON_CLICKABLE, clickable)
.setFlag(SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE, longClickable)
.setFlag(SYSUI_STATE_NAV_BAR_HIDDEN, !isNavBarWindowVisible())
- .setFlag(SYSUI_STATE_IME_SHOWING,
- (mNavigationIconHints & NAVIGATION_HINT_IME_SHOWN) != 0)
- .setFlag(SYSUI_STATE_IME_SWITCHER_SHOWING,
- (mNavigationIconHints & NAVIGATION_HINT_IME_SWITCHER_SHOWN) != 0)
+ .setFlag(SYSUI_STATE_IME_VISIBLE,
+ (mNavigationIconHints & NAVIGATION_HINT_IME_VISIBLE) != 0)
+ .setFlag(SYSUI_STATE_IME_SWITCHER_BUTTON_VISIBLE,
+ (mNavigationIconHints & NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE) != 0)
+ .setFlag(SYSUI_STATE_BACK_DISMISS_IME,
+ (mNavigationIconHints & NAVIGATION_HINT_BACK_DISMISS_IME) != 0)
.setFlag(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY,
allowSystemGestureIgnoringBarVisibility())
.commitUpdate(mDisplayId);
@@ -1926,28 +1934,36 @@ public class NavigationBar extends ViewController<NavigationBarView> implements
};
@VisibleForTesting
+ @NavigationHint
int getNavigationIconHints() {
return mNavigationIconHints;
}
- private void setNavigationIconHints(int hints) {
- if (hints == mNavigationIconHints) return;
+ /**
+ * Updates the navigation icons based on {@code hints}.
+ *
+ * @param hints bit flags defined in {@link StatusBarManager}.
+ */
+ private void setNavigationIconHints(@NavigationHint int hints) {
+ if (hints == mNavigationIconHints) {
+ return;
+ }
if (!isLargeScreen(mContext)) {
// All IME functions handled by launcher via Sysui flags for large screen
- final boolean newBackAlt = (hints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0;
- final boolean oldBackAlt =
- (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0;
- if (newBackAlt != oldBackAlt) {
- mView.onBackAltChanged(newBackAlt);
+ final boolean backDismissIme =
+ (hints & StatusBarManager.NAVIGATION_HINT_BACK_DISMISS_IME) != 0;
+ final boolean oldBackDismissIme =
+ (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_DISMISS_IME) != 0;
+ if (backDismissIme != oldBackDismissIme) {
+ mView.onBackDismissImeChanged(backDismissIme);
}
- mImeVisible = (hints & NAVIGATION_HINT_IME_SHOWN) != 0;
+ mImeVisible = (hints & NAVIGATION_HINT_IME_VISIBLE) != 0;
mView.setNavigationIconHints(hints);
}
if (DEBUG) {
- android.widget.Toast.makeText(mContext,
- "Navigation icon hints = " + hints,
- 500).show();
+ android.widget.Toast.makeText(mContext, "Navigation icon hints = " + hints, 500)
+ .show();
}
mNavigationIconHints = hints;
}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java
index d5ae72165c4a..38f2d42c8869 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java
@@ -16,6 +16,9 @@
package com.android.systemui.navigationbar.views;
+import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_DISMISS_IME;
+import static android.app.StatusBarManager.NAVIGATION_HINT_IME_VISIBLE;
+import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE;
import static android.inputmethodservice.InputMethodService.canImeRenderGesturalNavButtons;
import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL;
@@ -31,7 +34,7 @@ import android.animation.PropertyValuesHolder;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.annotation.DrawableRes;
-import android.app.StatusBarManager;
+import android.app.StatusBarManager.NavigationHint;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Canvas;
@@ -113,6 +116,7 @@ public class NavigationBarView extends FrameLayout {
boolean mLongClickableAccessibilityButton;
int mDisabledFlags = 0;
+ @NavigationHint
int mNavigationIconHints = 0;
private int mNavBarMode;
private boolean mImeDrawsImeNavBar;
@@ -210,7 +214,11 @@ public class NavigationBarView extends FrameLayout {
}
}
- public void onBackAltCleared() {
+ /**
+ * Called when the back button is no longer visually adjusted to indicate that it will
+ * dismiss the IME when pressed.
+ */
+ public void onBackDismissImeCleared() {
ButtonDispatcher backButton = getBackButton();
// When dismissing ime during unlock, force the back button to run the same appearance
@@ -499,10 +507,10 @@ public class NavigationBarView extends FrameLayout {
}
private void orientBackButton(KeyButtonDrawable drawable) {
- final boolean useAltBack =
- (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0;
+ final boolean isBackDismissIme =
+ (mNavigationIconHints & NAVIGATION_HINT_BACK_DISMISS_IME) != 0;
final boolean isRtl = mConfiguration.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
- float degrees = useAltBack ? (isRtl ? 90 : -90) : 0;
+ float degrees = isBackDismissIme ? (isRtl ? 90 : -90) : 0;
if (drawable.getRotation() == degrees) {
return;
}
@@ -514,7 +522,7 @@ public class NavigationBarView extends FrameLayout {
// Animate the back button's rotation to the new degrees and only in portrait move up the
// back button to line up with the other buttons
- float targetY = !mShowSwipeUpUi && !mIsVertical && useAltBack
+ float targetY = !mShowSwipeUpUi && !mIsVertical && isBackDismissIme
? - getResources().getDimension(R.dimen.navbar_back_button_ime_offset)
: 0;
ObjectAnimator navBarAnimator = ObjectAnimator.ofPropertyValuesHolder(drawable,
@@ -555,22 +563,25 @@ public class NavigationBarView extends FrameLayout {
super.setLayoutDirection(layoutDirection);
}
- void setNavigationIconHints(int hints) {
- if (hints == mNavigationIconHints) return;
+ void setNavigationIconHints(@NavigationHint int hints) {
+ if (hints == mNavigationIconHints) {
+ return;
+ }
mNavigationIconHints = hints;
updateNavButtonIcons();
}
/**
- * Called when the boolean value of whether to adjust the back button for the IME changed.
+ * Called when the state of the back button being visually adjusted to indicate that it will
+ * dismiss the IME when pressed has changed.
*
- * @param useBackAlt whether to adjust the back button for the IME.
+ * @param isBackDismissIme whether the back button is adjusted for IME dismissal.
*
* @see android.inputmethodservice.InputMethodService.BackDispositionMode
*/
- void onBackAltChanged(boolean useBackAlt) {
- if (!useBackAlt) {
- mTransitionListener.onBackAltCleared();
+ void onBackDismissImeChanged(boolean isBackDismissIme) {
+ if (!isBackDismissIme) {
+ mTransitionListener.onBackDismissImeCleared();
}
}
@@ -594,8 +605,8 @@ public class NavigationBarView extends FrameLayout {
// We have to replace or restore the back and home button icons when exiting or entering
// carmode, respectively. Recents are not available in CarMode in nav bar so change
// to recent icon is not required.
- final boolean useAltBack =
- (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0;
+ final boolean isBackDismissIme =
+ (mNavigationIconHints & NAVIGATION_HINT_BACK_DISMISS_IME) != 0;
KeyButtonDrawable backIcon = mBackIcon;
orientBackButton(backIcon);
KeyButtonDrawable homeIcon = mHomeDefaultIcon;
@@ -607,11 +618,12 @@ public class NavigationBarView extends FrameLayout {
updateRecentsIcon();
- // Update IME button visibility, a11y and rotate button always overrides the appearance
- boolean disableImeSwitcher =
- (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN) == 0
- || isImeRenderingNavButtons();
- mContextualButtonGroup.setButtonVisibility(R.id.ime_switcher, !disableImeSwitcher);
+ // Update IME switcher button visibility, a11y and rotate button always overrides
+ // the appearance.
+ final boolean isImeSwitcherButtonVisible =
+ (mNavigationIconHints & NAVIGATION_HINT_IME_SWITCHER_BUTTON_VISIBLE) != 0
+ && !isImeRenderingNavButtons();
+ mContextualButtonGroup.setButtonVisibility(R.id.ime_switcher, isImeSwitcherButtonVisible);
mBarTransitions.reapplyDarkIntensity();
@@ -625,7 +637,7 @@ public class NavigationBarView extends FrameLayout {
boolean disableHomeHandle = disableRecent
&& ((mDisabledFlags & View.STATUS_BAR_DISABLE_HOME) != 0);
- boolean disableBack = !useAltBack && (mEdgeBackGestureHandler.isHandlingGestures()
+ boolean disableBack = !isBackDismissIme && (mEdgeBackGestureHandler.isHandlingGestures()
|| ((mDisabledFlags & View.STATUS_BAR_DISABLE_BACK) != 0))
|| isImeRenderingNavButtons();
@@ -665,7 +677,7 @@ public class NavigationBarView extends FrameLayout {
boolean isImeRenderingNavButtons() {
return mImeDrawsImeNavBar
&& mImeCanRenderGesturalNavButtons
- && (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_IME_SHOWN) != 0;
+ && (mNavigationIconHints & NAVIGATION_HINT_IME_VISIBLE) != 0;
}
@VisibleForTesting
diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt
deleted file mode 100644
index 398ace4b67f4..000000000000
--- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.notifications.ui.viewmodel
-
-import com.android.compose.animation.scene.Back
-import com.android.compose.animation.scene.Swipe
-import com.android.compose.animation.scene.UserAction
-import com.android.compose.animation.scene.UserActionResult
-import com.android.compose.animation.scene.UserActionResult.ReplaceByOverlay
-import com.android.systemui.scene.shared.model.Overlays
-import com.android.systemui.scene.shared.model.SceneFamilies
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
-import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
-import dagger.assisted.AssistedFactory
-import dagger.assisted.AssistedInject
-
-/**
- * Models the UI state for the user actions that the user can perform to navigate to other scenes.
- */
-class NotificationsShadeUserActionsViewModel @AssistedInject constructor() :
- UserActionsViewModel() {
-
- override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) {
- setActions(
- mapOf(
- Back to SceneFamilies.Home,
- Swipe.Up to SceneFamilies.Home,
- Swipe.Down(fromSource = SceneContainerEdge.TopRight) to
- ReplaceByOverlay(Overlays.QuickSettingsShade),
- )
- )
- }
-
- @AssistedFactory
- interface Factory {
- fun create(): NotificationsShadeUserActionsViewModel
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/BounceableInfo.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/BounceableInfo.kt
index c9d767e6d152..302242ca11dd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/BounceableInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/BounceableInfo.kt
@@ -16,7 +16,7 @@
package com.android.systemui.qs.panels.ui.compose
-import android.processor.immutability.Immutable
+import androidx.compose.runtime.Stable
import com.android.compose.animation.Bounceable
import com.android.systemui.qs.panels.shared.model.SizedTile
import com.android.systemui.qs.panels.ui.model.GridCell
@@ -24,7 +24,7 @@ import com.android.systemui.qs.panels.ui.model.TileGridCell
import com.android.systemui.qs.panels.ui.viewmodel.BounceableTileViewModel
import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
-@Immutable
+@Stable
data class BounceableInfo(
val bounceable: BounceableTileViewModel,
val previousTile: Bounceable?,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt
index 5cb30b999e13..b084f79a5bba 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt
@@ -19,8 +19,8 @@ package com.android.systemui.qs.panels.ui.compose
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
@@ -49,6 +49,8 @@ fun ContentScope.QuickQuickSettings(
val squishiness by viewModel.squishinessViewModel.squishiness.collectAsStateWithLifecycle()
val scope = rememberCoroutineScope()
+ val spans by remember(sizedTiles) { derivedStateOf { sizedTiles.fastMap { it.width } } }
+
DisposableEffect(tiles) {
val token = Any()
tiles.forEach { it.startListening(token) }
@@ -62,26 +64,24 @@ fun ContentScope.QuickQuickSettings(
columns = columns,
columnSpacing = dimensionResource(R.dimen.qs_tile_margin_horizontal),
rowSpacing = dimensionResource(R.dimen.qs_tile_margin_vertical),
- spans = sizedTiles.fastMap { it.width },
+ spans = spans,
modifier = Modifier.sysuiResTag("qqs_tile_layout"),
+ keys = { sizedTiles[it].tile.spec },
) { spanIndex ->
val it = sizedTiles[spanIndex]
val column = cellIndex % columns
cellIndex += it.width
- key(it.tile.spec) {
- Tile(
- tile = it.tile,
- iconOnly = it.isIcon,
- modifier = Modifier.element(it.tile.spec.toElementKey(spanIndex)),
- squishiness = { squishiness },
- coroutineScope = scope,
- bounceableInfo = bounceables.bounceableInfo(it, spanIndex, column, columns),
- tileHapticsViewModelFactoryProvider =
- viewModel.tileHapticsViewModelFactoryProvider,
- // There should be no QuickQuickSettings when the details view is enabled.
- detailsViewModel = null,
- )
- }
+ Tile(
+ tile = it.tile,
+ iconOnly = it.isIcon,
+ modifier = Modifier.element(it.tile.spec.toElementKey(spanIndex)),
+ squishiness = { squishiness },
+ coroutineScope = scope,
+ bounceableInfo = bounceables.bounceableInfo(it, spanIndex, column, columns),
+ tileHapticsViewModelFactoryProvider = viewModel.tileHapticsViewModelFactoryProvider,
+ // There should be no QuickQuickSettings when the details view is enabled.
+ detailsViewModel = null,
+ )
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
index d2ee126ace91..2cccaddc02a8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
@@ -498,11 +498,9 @@ fun gridHeight(rows: Int, tileHeight: Dp, tilePadding: Dp, gridPadding: Dp): Dp
return ((tileHeight + tilePadding) * rows) + gridPadding * 2
}
-private fun GridCell.key(index: Int, dragAndDropState: DragAndDropState): Any {
+private fun GridCell.key(index: Int): Any {
return when (this) {
- is TileGridCell -> {
- if (dragAndDropState.isMoving(tile.tileSpec)) index else key
- }
+ is TileGridCell -> key
is SpacerGridCell -> index
}
}
@@ -510,10 +508,13 @@ private fun GridCell.key(index: Int, dragAndDropState: DragAndDropState): Any {
/**
* Adds a list of [GridCell] to the lazy grid
*
- * @param cells the pairs of [GridCell] to [AnimatableTileViewModel]
+ * @param cells the pairs of [GridCell] to [BounceableTileViewModel]
+ * @param columns the number of columns of this tile grid
* @param dragAndDropState the [DragAndDropState] for this grid
* @param selectionState the [MutableSelectionState] for this grid
- * @param onToggleSize the callback when a tile's size is toggled
+ * @param coroutineScope the [CoroutineScope] to be used for the tiles
+ * @param largeTilesSpan the width used for large tiles
+ * @param onResize the callback when a tile has a new [ResizeOperation]
*/
fun LazyGridScope.EditTiles(
cells: List<Pair<GridCell, BounceableTileViewModel>>,
@@ -526,7 +527,7 @@ fun LazyGridScope.EditTiles(
) {
items(
count = cells.size,
- key = { cells[it].first.key(it, dragAndDropState) },
+ key = { cells[it].first.key(it) },
span = { cells[it].first.span },
contentType = { TileType },
) { index ->
@@ -536,13 +537,12 @@ fun LazyGridScope.EditTiles(
// If the tile is being moved, replace it with a visible spacer
SpacerGridCell(
Modifier.background(
- color =
- MaterialTheme.colorScheme.secondary.copy(
- alpha = EditModeTileDefaults.PLACEHOLDER_ALPHA
- ),
- shape = RoundedCornerShape(InactiveCornerRadius),
- )
- .animateItem()
+ color =
+ MaterialTheme.colorScheme.secondary.copy(
+ alpha = EditModeTileDefaults.PLACEHOLDER_ALPHA
+ ),
+ shape = RoundedCornerShape(InactiveCornerRadius),
+ )
)
} else {
TileGridCell(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
index 4432d336237f..cc4c3af1dc63 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
@@ -18,8 +18,8 @@ package com.android.systemui.qs.panels.ui.compose.infinitegrid
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
@@ -86,27 +86,28 @@ constructor(
val scope = rememberCoroutineScope()
var cellIndex = 0
+ val spans by remember(sizedTiles) { derivedStateOf { sizedTiles.fastMap { it.width } } }
+
VerticalSpannedGrid(
columns = columns,
columnSpacing = dimensionResource(R.dimen.qs_tile_margin_horizontal),
rowSpacing = dimensionResource(R.dimen.qs_tile_margin_vertical),
- spans = sizedTiles.fastMap { it.width },
+ spans = spans,
+ keys = { sizedTiles[it].tile.spec },
) { spanIndex ->
val it = sizedTiles[spanIndex]
val column = cellIndex % columns
cellIndex += it.width
- key(it.tile.spec) {
- Tile(
- tile = it.tile,
- iconOnly = iconTilesViewModel.isIconTile(it.tile.spec),
- modifier = Modifier.element(it.tile.spec.toElementKey(spanIndex)),
- squishiness = { squishiness },
- tileHapticsViewModelFactoryProvider = tileHapticsViewModelFactoryProvider,
- coroutineScope = scope,
- bounceableInfo = bounceables.bounceableInfo(it, spanIndex, column, columns),
- detailsViewModel = detailsViewModel,
- )
- }
+ Tile(
+ tile = it.tile,
+ iconOnly = iconTilesViewModel.isIconTile(it.tile.spec),
+ modifier = Modifier.element(it.tile.spec.toElementKey(spanIndex)),
+ squishiness = { squishiness },
+ tileHapticsViewModelFactoryProvider = tileHapticsViewModelFactoryProvider,
+ coroutineScope = scope,
+ bounceableInfo = bounceables.bounceableInfo(it, spanIndex, column, columns),
+ detailsViewModel = detailsViewModel,
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt
index 16c27223a471..8a627c452081 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt
@@ -18,6 +18,7 @@ package com.android.systemui.qs.pipeline.shared
import android.content.ComponentName
import android.text.TextUtils
+import androidx.compose.runtime.Stable
import com.android.systemui.qs.external.CustomTile
/**
@@ -34,6 +35,7 @@ sealed class TileSpec private constructor(open val spec: String) {
data object Invalid : TileSpec("")
/** Container for the spec of a tile provided by SystemUI. */
+ @Stable
data class PlatformTileSpec internal constructor(override val spec: String) : TileSpec(spec) {
override fun toString(): String {
return "P($spec)"
@@ -45,6 +47,7 @@ sealed class TileSpec private constructor(open val spec: String) {
*
* [componentName] indicates the associated `TileService`.
*/
+ @Stable
data class CustomTileSpec
internal constructor(override val spec: String, val componentName: ComponentName) :
TileSpec(spec) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt
index 0051bf5de7f2..ad5dd27f07c2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt
@@ -35,12 +35,14 @@ import com.android.systemui.modes.shared.ModesUiIcons
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.plugins.qs.TileDetailsViewModel
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.qs.QSHost
import com.android.systemui.qs.QsEventLogger
import com.android.systemui.qs.asQSTileIcon
import com.android.systemui.qs.logging.QSLogger
import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.qs.tiles.dialog.ModesDetailsViewModel
import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesTileDataInteractor
import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesTileUserActionInteractor
import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel
@@ -48,6 +50,7 @@ import com.android.systemui.qs.tiles.impl.modes.ui.ModesTileMapper
import com.android.systemui.qs.tiles.viewmodel.QSTileConfigProvider
import com.android.systemui.qs.tiles.viewmodel.QSTileState
import com.android.systemui.res.R
+import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel
import javax.inject.Inject
import kotlinx.coroutines.runBlocking
@@ -67,6 +70,7 @@ constructor(
private val dataInteractor: ModesTileDataInteractor,
private val tileMapper: ModesTileMapper,
private val userActionInteractor: ModesTileUserActionInteractor,
+ private val modesDialogViewModel: ModesDialogViewModel,
) :
QSTileImpl<QSTile.State>(
host,
@@ -114,6 +118,13 @@ constructor(
userActionInteractor.handleToggleClick(model)
}
+ override fun getDetailsViewModel(): TileDetailsViewModel {
+ return ModesDetailsViewModel(
+ onSettingsClick = { userActionInteractor.handleLongClick(null) },
+ viewModel = modesDialogViewModel,
+ )
+ }
+
override fun getLongClickIntent(): Intent = userActionInteractor.longClickIntent
@VisibleForTesting
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt
new file mode 100644
index 000000000000..511597d05d37
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.dialog
+
+import androidx.compose.runtime.Composable
+import com.android.systemui.plugins.qs.TileDetailsViewModel
+import com.android.systemui.statusbar.policy.ui.dialog.composable.ModeTileGrid
+import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel
+
+/** The view model used for the modes details view in the Quick Settings */
+class ModesDetailsViewModel(
+ private val onSettingsClick: () -> Unit,
+ private val viewModel: ModesDialogViewModel,
+) : TileDetailsViewModel() {
+ @Composable
+ override fun GetContentView() {
+ // TODO(b/378513940): Finish implementing this function.
+ ModeTileGrid(viewModel = viewModel)
+ }
+
+ override fun clickOnSettingsButton() {
+ onSettingsClick()
+ }
+
+ override fun getTitle(): String {
+ // TODO(b/388321032): Replace this string with a string in a translatable xml file.
+ return "Modes"
+ }
+
+ override fun getSubTitle(): String {
+ // TODO(b/388321032): Replace this string with a string in a translatable xml file.
+ return "Silences interruptions from people and apps in different circumstances"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt
index 5ce7f0d039c8..b5da044b886a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt
@@ -54,7 +54,7 @@ constructor(
handleToggleClick(input.data)
}
is QSTileUserAction.LongClick -> {
- qsTileIntentUserInputHandler.handle(action.expandable, longClickIntent)
+ handleLongClick(action.expandable)
}
}
}
@@ -95,6 +95,10 @@ constructor(
}
}
+ fun handleLongClick(expandable: Expandable?) {
+ qsTileIntentUserInputHandler.handle(expandable, longClickIntent)
+ }
+
companion object {
const val TAG = "ModesTileUserActionInteractor"
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index ecd002705c60..63c10c9b971a 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -945,10 +945,6 @@ constructor(
override fun onTransitionAnimationEnd() {
sceneInteractor.onTransitionAnimationEnd()
}
-
- override fun onTransitionAnimationCancelled() {
- sceneInteractor.onTransitionAnimationCancelled()
- }
}
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt
index 08214c456897..f5c605211520 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt
@@ -35,7 +35,6 @@ import android.util.Log
import android.view.Display
import android.view.ScrollCaptureResponse
import android.view.ViewRootImpl.ActivityConfigCallback
-import android.view.WindowManager
import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
import android.widget.Toast
import android.window.WindowContext
@@ -218,9 +217,7 @@ internal constructor(
window.setFocusable(true)
viewProxy.requestFocus()
- if (screenshot.type != WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE) {
- enqueueScrollCaptureRequest(requestId, screenshot.userHandle)
- }
+ enqueueScrollCaptureRequest(requestId, screenshot.userHandle)
window.attachWindow()
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotCrossProfileService.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotCrossProfileService.kt
index 2e6c7567259f..d82a8bd0ddcd 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotCrossProfileService.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotCrossProfileService.kt
@@ -31,7 +31,7 @@ class ScreenshotCrossProfileService : Service() {
private val mBinder: IBinder =
object : ICrossProfileService.Stub() {
- override fun launchIntent(intent: Intent, bundle: Bundle) {
+ override fun launchIntent(intent: Intent, bundle: Bundle?) {
startActivity(intent, bundle)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java b/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java
index 49f3cfc4ceaf..917869a66ca4 100644
--- a/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java
+++ b/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java
@@ -27,6 +27,8 @@ import android.graphics.PorterDuff;
import android.graphics.PorterDuff.Mode;
import android.graphics.PorterDuffColorFilter;
import android.graphics.Rect;
+import android.graphics.RenderEffect;
+import android.graphics.Shader;
import android.graphics.drawable.Drawable;
import android.os.Looper;
import android.util.AttributeSet;
@@ -373,4 +375,18 @@ public class ScrimView extends View {
((ScrimDrawable) mDrawable).setRoundedCorners(radius);
}
}
+
+ /**
+ * Blur the view with the specific blur radius or clear any blurs if the radius is 0
+ */
+ public void setBlurRadius(float blurRadius) {
+ if (blurRadius > 0) {
+ setRenderEffect(RenderEffect.createBlurEffect(
+ blurRadius,
+ blurRadius,
+ Shader.TileMode.CLAMP));
+ } else {
+ setRenderEffect(null);
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 19bf4c0bab81..a1b4def09ba9 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -109,7 +109,6 @@ import com.android.systemui.keyguard.shared.model.Edge;
import com.android.systemui.keyguard.shared.model.TransitionState;
import com.android.systemui.keyguard.shared.model.TransitionStep;
import com.android.systemui.keyguard.ui.binder.KeyguardLongPressViewBinder;
-import com.android.systemui.keyguard.ui.transitions.BlurConfig;
import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.KeyguardTouchHandlingViewModel;
import com.android.systemui.media.controls.domain.pipeline.MediaDataManager;
@@ -915,8 +914,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
if (!com.android.systemui.Flags.bouncerUiRevamp()) return;
if (isBouncerShowing && isExpanded()) {
- float shadeBlurEffect = BlurConfig.maxBlurRadiusToNotificationPanelBlurRadius(
- mDepthController.getMaxBlurRadiusPx());
+ float shadeBlurEffect = mDepthController.getMaxBlurRadiusPx();
mView.setRenderEffect(RenderEffect.createBlurEffect(
shadeBlurEffect,
shadeBlurEffect,
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
index 48bbb0407ee3..48e374746bf5 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
@@ -17,6 +17,7 @@
package com.android.systemui.shade;
import static android.os.Trace.TRACE_TAG_APP;
+import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED;
import static com.android.systemui.Flags.enableViewCaptureTracing;
import static com.android.systemui.statusbar.phone.CentralSurfaces.DEBUG;
@@ -49,10 +50,12 @@ import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.WindowInsetsController;
+import android.view.accessibility.AccessibilityEvent;
import com.android.app.viewcapture.ViewCaptureFactory;
import com.android.internal.view.FloatingActionMode;
import com.android.internal.widget.floatingtoolbar.FloatingToolbar;
+import com.android.systemui.Flags;
import com.android.systemui.scene.ui.view.WindowRootView;
import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround;
import com.android.systemui.statusbar.phone.ConfigurationForwarder;
@@ -77,6 +80,8 @@ public class NotificationShadeWindowView extends WindowRootView {
private SafeCloseable mViewCaptureCloseable;
+ private boolean mAnimatingContentLaunch = false;
+
public NotificationShadeWindowView(Context context, AttributeSet attrs) {
super(context, attrs);
setMotionEventSplittingEnabled(false);
@@ -188,6 +193,22 @@ public class NotificationShadeWindowView extends WindowRootView {
}
}
+ @Override
+ public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event) {
+ if (Flags.shadeLaunchAccessibility() && mAnimatingContentLaunch
+ && event.getEventType() == TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
+ // Block accessibility focus events during launch animations to avoid stray TalkBack
+ // announcements.
+ return false;
+ }
+
+ return super.requestSendAccessibilityEvent(child, event);
+ }
+
+ public void setAnimatingContentLaunch(boolean animating) {
+ mAnimatingContentLaunch = animating;
+ }
+
public void setConfigurationForwarder(ConfigurationForwarder configurationForwarder) {
ShadeWindowGoesAround.isUnexpectedlyInLegacyMode();
mConfigurationForwarder = configurationForwarder;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index e5dcd2338b9d..ffec8f284c48 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -16,10 +16,12 @@
package com.android.systemui.shade;
+import static com.android.systemui.Flags.shadeLaunchAccessibility;
import static com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING;
import static com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN;
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
+import static com.android.systemui.util.kotlin.JavaAdapterKt.combineFlows;
import android.app.StatusBarManager;
import android.util.Log;
@@ -59,6 +61,7 @@ import com.android.systemui.res.R;
import com.android.systemui.scene.shared.flag.SceneContainerFlag;
import com.android.systemui.settings.brightness.domain.interactor.BrightnessMirrorShowingInteractor;
import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor;
+import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractor;
import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround;
import com.android.systemui.shared.animation.DisableSubpixelTextTransitionListener;
import com.android.systemui.statusbar.BlurUtils;
@@ -85,6 +88,7 @@ import com.android.systemui.window.ui.WindowRootViewBinder;
import com.android.systemui.window.ui.viewmodel.WindowRootViewModel;
import kotlinx.coroutines.ExperimentalCoroutinesApi;
+import kotlinx.coroutines.flow.Flow;
import java.io.PrintWriter;
import java.util.Optional;
@@ -174,6 +178,7 @@ public class NotificationShadeWindowViewController implements Dumpable {
NotificationShadeDepthController depthController,
NotificationShadeWindowView notificationShadeWindowView,
ShadeViewController shadeViewController,
+ ShadeAnimationInteractor shadeAnimationInteractor,
PanelExpansionInteractor panelExpansionInteractor,
ShadeExpansionStateManager shadeExpansionStateManager,
NotificationStackScrollLayoutController notificationStackScrollLayoutController,
@@ -238,9 +243,17 @@ public class NotificationShadeWindowViewController implements Dumpable {
collectFlow(mView, keyguardTransitionInteractor.transition(
Edge.create(LOCKSCREEN, DREAMING)),
mLockscreenToDreamingTransition);
+ Flow<Boolean> isLaunchAnimationRunning =
+ shadeLaunchAccessibility()
+ ? combineFlows(
+ notificationLaunchAnimationInteractor.isLaunchAnimationRunning(),
+ shadeAnimationInteractor.isLaunchingActivity(),
+ (notificationLaunching, shadeLaunching) ->
+ notificationLaunching || shadeLaunching)
+ : notificationLaunchAnimationInteractor.isLaunchAnimationRunning();
collectFlow(
mView,
- notificationLaunchAnimationInteractor.isLaunchAnimationRunning(),
+ isLaunchAnimationRunning,
this::setExpandAnimationRunning);
if (QSComposeFragment.isEnabled()) {
collectFlow(mView,
@@ -726,9 +739,17 @@ public class NotificationShadeWindowViewController implements Dumpable {
if (ActivityTransitionAnimator.DEBUG_TRANSITION_ANIMATION) {
Log.d(TAG, "Setting mExpandAnimationRunning=" + running);
}
+
if (running) {
mLaunchAnimationTimeout = mClock.uptimeMillis() + 5000;
}
+
+ if (shadeLaunchAccessibility()) {
+ // The view needs to know when an animation is ongoing so it can intercept
+ // unnecessary accessibility events.
+ mView.setAnimatingContentLaunch(running);
+ }
+
mExpandAnimationRunning = running;
mNotificationShadeWindowController.setLaunchingActivity(mExpandAnimationRunning);
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QsBatteryModeController.kt b/packages/SystemUI/src/com/android/systemui/shade/QsBatteryModeController.kt
index 7a70966c2b12..b15615a83698 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QsBatteryModeController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/QsBatteryModeController.kt
@@ -5,6 +5,7 @@ import android.view.DisplayCutout
import com.android.systemui.battery.BatteryMeterView
import com.android.systemui.res.R
import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
import javax.inject.Inject
/**
@@ -15,11 +16,9 @@ class QsBatteryModeController
@Inject
constructor(
@ShadeDisplayAware private val context: Context,
- insetsProviderStore: StatusBarContentInsetsProviderStore,
+ private val insetsProviderStore: StatusBarContentInsetsProviderStore,
) {
- private val insetsProvider = insetsProviderStore.defaultDisplay
-
private companion object {
// MotionLayout frames are in [0, 100]. Where 0 and 100 are reserved for start and end
// frames.
@@ -43,17 +42,19 @@ constructor(
* animation.
*/
@BatteryMeterView.BatteryPercentMode
- fun getBatteryMode(cutout: DisplayCutout?, qsExpandedFraction: Float): Int? =
- when {
+ fun getBatteryMode(cutout: DisplayCutout?, qsExpandedFraction: Float): Int? {
+ val insetsProvider = insetsProviderStore.forDisplay(context.displayId)
+ return when {
qsExpandedFraction > fadeInStartFraction -> BatteryMeterView.MODE_ESTIMATE
- qsExpandedFraction < fadeOutCompleteFraction ->
- if (hasCenterCutout(cutout)) {
+ insetsProvider != null && qsExpandedFraction < fadeOutCompleteFraction ->
+ if (hasCenterCutout(cutout, insetsProvider)) {
BatteryMeterView.MODE_ON
} else {
BatteryMeterView.MODE_ESTIMATE
}
else -> null
}
+ }
fun updateResources() {
fadeInStartFraction =
@@ -64,7 +65,10 @@ constructor(
MOTION_LAYOUT_MAX_FRAME.toFloat()
}
- private fun hasCenterCutout(cutout: DisplayCutout?): Boolean =
+ private fun hasCenterCutout(
+ cutout: DisplayCutout?,
+ insetsProvider: StatusBarContentInsetsProvider,
+ ): Boolean =
cutout?.let {
!insetsProvider.currentRotationHasCornerCutout() && !it.boundingRectTop.isEmpty
} ?: false
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
index 747642097327..f926d39760fe 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
@@ -36,7 +36,6 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.res.R
import com.android.systemui.scene.ui.view.WindowRootView
-import com.android.systemui.shade.data.repository.MutableShadeDisplaysRepository
import com.android.systemui.shade.data.repository.ShadeDisplaysRepository
import com.android.systemui.shade.data.repository.ShadeDisplaysRepositoryImpl
import com.android.systemui.shade.display.ShadeDisplayPolicyModule
@@ -211,15 +210,6 @@ object ShadeDisplayAwareModule {
return impl
}
- @SysUISingleton
- @Provides
- fun provideMutableShadePositionRepository(
- impl: ShadeDisplaysRepositoryImpl
- ): MutableShadeDisplaysRepository {
- ShadeWindowGoesAround.isUnexpectedlyInLegacyMode()
- return impl
- }
-
@Provides
@SysUISingleton
fun provideShadeDialogContextInteractor(
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
index e8a792c30aa2..d82f8e722744 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
@@ -57,6 +57,7 @@ import com.android.systemui.shade.ShadeHeaderController.Companion.QS_HEADER_CONS
import com.android.systemui.shade.ShadeViewProviderModule.Companion.SHADE_HEADER
import com.android.systemui.shade.carrier.ShadeCarrierGroup
import com.android.systemui.shade.carrier.ShadeCarrierGroupController
+import com.android.systemui.shade.data.repository.ShadeDisplaysRepository
import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore
import com.android.systemui.statusbar.phone.StatusBarLocation
import com.android.systemui.statusbar.phone.StatusIconContainer
@@ -90,8 +91,9 @@ constructor(
private val statusBarIconController: StatusBarIconController,
private val tintedIconManagerFactory: TintedIconManager.Factory,
private val privacyIconsController: HeaderPrivacyIconsController,
- private val insetsProviderStore: StatusBarContentInsetsProviderStore,
+ private val statusBarContentInsetsProviderStore: StatusBarContentInsetsProviderStore,
@ShadeDisplayAware private val configurationController: ConfigurationController,
+ private val shadeDisplaysRepository: ShadeDisplaysRepository,
private val variableDateViewControllerFactory: VariableDateViewController.Factory,
@Named(SHADE_HEADER) private val batteryMeterViewController: BatteryMeterViewController,
private val dumpManager: DumpManager,
@@ -104,7 +106,9 @@ constructor(
private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory,
) : ViewController<View>(header), Dumpable {
- private val insetsProvider = insetsProviderStore.defaultDisplay
+ private val statusBarContentInsetsProvider
+ get() =
+ statusBarContentInsetsProviderStore.forDisplay(shadeDisplaysRepository.displayId.value)
companion object {
/** IDs for transitions and constraints for the [MotionLayout]. */
@@ -222,10 +226,14 @@ constructor(
private val insetListener =
View.OnApplyWindowInsetsListener { view, insets ->
- updateConstraintsForInsets(view as MotionLayout, insets)
- lastInsets = WindowInsets(insets)
-
- view.onApplyWindowInsets(insets)
+ val windowInsets = WindowInsets(insets)
+ if (windowInsets != lastInsets) {
+ updateConstraintsForInsets(view as MotionLayout, insets)
+ lastInsets = windowInsets
+ view.onApplyWindowInsets(insets)
+ } else {
+ insets
+ }
}
private var singleCarrier = false
@@ -414,6 +422,7 @@ constructor(
}
private fun updateConstraintsForInsets(view: MotionLayout, insets: WindowInsets) {
+ val insetsProvider = statusBarContentInsetsProvider ?: return
val cutout = insets.displayCutout.also { this.cutout = it }
val sbInsets: Insets = insetsProvider.getStatusBarContentInsetsForCurrentRotation()
@@ -508,6 +517,9 @@ constructor(
systemIconsHoverContainer.setOnClickListener(null)
systemIconsHoverContainer.isClickable = false
}
+
+ lastInsets?.let { updateConstraintsForInsets(header, it) }
+
header.jumpToState(header.startState)
updatePosition()
updateScrollY()
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt
index 7bfe40c3d811..173da336c62f 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt
@@ -20,7 +20,7 @@ import android.provider.Settings.Global.DEVELOPMENT_SHADE_DISPLAY_AWARENESS
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.display.data.repository.DisplayRepository
-import com.android.systemui.shade.data.repository.MutableShadeDisplaysRepository
+import com.android.systemui.shade.data.repository.ShadeDisplaysRepository
import com.android.systemui.shade.display.ShadeDisplayPolicy
import com.android.systemui.statusbar.commandline.Command
import com.android.systemui.statusbar.commandline.CommandRegistry
@@ -35,7 +35,7 @@ constructor(
private val globalSettings: GlobalSettings,
private val commandRegistry: CommandRegistry,
private val displaysRepository: DisplayRepository,
- private val positionRepository: MutableShadeDisplaysRepository,
+ private val positionRepository: ShadeDisplaysRepository,
private val policies: Set<@JvmSuppressWildcards ShadeDisplayPolicy>,
private val defaultPolicy: ShadeDisplayPolicy,
) : Command, CoreStartable {
@@ -103,7 +103,7 @@ constructor(
}
private fun printPolicies() {
- val currentPolicyName = positionRepository.policy.value.name
+ val currentPolicyName = positionRepository.currentPolicy.name
pw.println("Available policies: ")
policies.forEach {
pw.print(" - ${it.name}")
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/FakeShadeDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/FakeShadeDisplayRepository.kt
index 732d4d1500e7..3513334f2a5c 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/FakeShadeDisplayRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/FakeShadeDisplayRepository.kt
@@ -17,6 +17,8 @@
package com.android.systemui.shade.data.repository
import android.view.Display
+import com.android.systemui.shade.display.FakeShadeDisplayPolicy
+import com.android.systemui.shade.display.ShadeDisplayPolicy
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -30,7 +32,6 @@ class FakeShadeDisplayRepository : ShadeDisplaysRepository {
override val displayId: StateFlow<Int>
get() = _displayId
- fun resetDisplayId() {
- _displayId.value = Display.DEFAULT_DISPLAY
- }
+ override val currentPolicy: ShadeDisplayPolicy
+ get() = FakeShadeDisplayPolicy
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt
index af48231e0a99..f959f7fe0c31 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt
@@ -20,14 +20,18 @@ import android.provider.Settings.Global.DEVELOPMENT_SHADE_DISPLAY_AWARENESS
import android.view.Display
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import com.android.systemui.shade.ShadeOnDefaultDisplayWhenLocked
import com.android.systemui.shade.display.ShadeDisplayPolicy
import com.android.systemui.util.settings.GlobalSettings
import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
@@ -38,12 +42,8 @@ import kotlinx.coroutines.flow.stateIn
interface ShadeDisplaysRepository {
/** ID of the display which currently hosts the shade */
val displayId: StateFlow<Int>
-}
-
-/** Allows to change the policy that determines in which display the Shade window is visible. */
-interface MutableShadeDisplaysRepository : ShadeDisplaysRepository {
- /** Updates the policy to select where the shade is visible. */
- val policy: StateFlow<ShadeDisplayPolicy>
+ /** The current policy set. */
+ val currentPolicy: ShadeDisplayPolicy
}
/** Keeps the policy and propagates the display id for the shade from it. */
@@ -56,9 +56,11 @@ constructor(
defaultPolicy: ShadeDisplayPolicy,
@Background bgScope: CoroutineScope,
policies: Set<@JvmSuppressWildcards ShadeDisplayPolicy>,
-) : MutableShadeDisplaysRepository {
+ @ShadeOnDefaultDisplayWhenLocked private val shadeOnDefaultDisplayWhenLocked: Boolean,
+ keyguardRepository: KeyguardRepository,
+) : ShadeDisplaysRepository {
- override val policy: StateFlow<ShadeDisplayPolicy> =
+ private val policy: StateFlow<ShadeDisplayPolicy> =
globalSettings
.observerFlow(DEVELOPMENT_SHADE_DISPLAY_AWARENESS)
.onStart { emit(Unit) }
@@ -71,10 +73,32 @@ constructor(
return@map defaultPolicy
}
.distinctUntilChanged()
- .stateIn(bgScope, SharingStarted.WhileSubscribed(), defaultPolicy)
+ .stateIn(bgScope, SharingStarted.Eagerly, defaultPolicy)
+
+ private val displayIdFromPolicy: Flow<Int> = policy.flatMapLatest { it.displayId }
+
+ private val keyguardAwareDisplayPolicy: Flow<Int> =
+ if (!shadeOnDefaultDisplayWhenLocked) {
+ displayIdFromPolicy
+ } else {
+ keyguardRepository.isKeyguardShowing.combine(displayIdFromPolicy) {
+ isKeyguardShowing,
+ currentDisplayId ->
+ if (isKeyguardShowing) {
+ Display.DEFAULT_DISPLAY
+ } else {
+ currentDisplayId
+ }
+ }
+ }
+
+ override val currentPolicy: ShadeDisplayPolicy
+ get() = policy.value
override val displayId: StateFlow<Int> =
- policy
- .flatMapLatest { it.displayId }
- .stateIn(bgScope, SharingStarted.WhileSubscribed(), Display.DEFAULT_DISPLAY)
+ keyguardAwareDisplayPolicy.stateIn(
+ bgScope,
+ SharingStarted.WhileSubscribed(),
+ Display.DEFAULT_DISPLAY,
+ )
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/display/FakeShadeDisplayPolicy.kt b/packages/SystemUI/src/com/android/systemui/shade/display/FakeShadeDisplayPolicy.kt
new file mode 100644
index 000000000000..e010bd6f9880
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/display/FakeShadeDisplayPolicy.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade.display
+
+import android.view.Display
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+/** Used only for testing. */
+object FakeShadeDisplayPolicy : ShadeDisplayPolicy {
+ override val name: String
+ get() = "fake_shade_policy"
+
+ override val displayId: StateFlow<Int>
+ get() = _displayId
+
+ private val _displayId = MutableStateFlow(Display.DEFAULT_DISPLAY)
+
+ fun setDisplayId(displayId: Int) {
+ _displayId.value = displayId
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/display/FocusShadeDisplayPolicy.kt b/packages/SystemUI/src/com/android/systemui/shade/display/FocusShadeDisplayPolicy.kt
new file mode 100644
index 000000000000..7d8f7c59ad66
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/display/FocusShadeDisplayPolicy.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade.display
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.display.data.repository.FocusedDisplayRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.StateFlow
+
+/** Policy that just emits the [FocusedDisplayRepository] display id. */
+@SysUISingleton
+class FocusShadeDisplayPolicy
+@Inject
+constructor(private val focusedDisplayRepository: FocusedDisplayRepository) : ShadeDisplayPolicy {
+ override val name: String
+ get() = "focused_display"
+
+ override val displayId: StateFlow<Int>
+ get() = focusedDisplayRepository.focusedDisplayId
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/display/ShadeDisplayPolicy.kt b/packages/SystemUI/src/com/android/systemui/shade/display/ShadeDisplayPolicy.kt
index bf5deff5cff5..677e41a47afe 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/display/ShadeDisplayPolicy.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/display/ShadeDisplayPolicy.kt
@@ -19,7 +19,8 @@ package com.android.systemui.shade.display
import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractor.ShadeElement
import dagger.Binds
import dagger.Module
-import dagger.multibindings.IntoSet
+import dagger.Provides
+import dagger.multibindings.ElementsIntoSet
import kotlinx.coroutines.flow.StateFlow
/** Describes the display the shade should be shown in. */
@@ -53,27 +54,25 @@ interface ShadeExpansionIntent {
fun consumeExpansionIntent(): ShadeElement?
}
-@Module
+@Module(includes = [AllShadeDisplayPoliciesModule::class])
interface ShadeDisplayPolicyModule {
@Binds fun provideDefaultPolicy(impl: DefaultDisplayShadePolicy): ShadeDisplayPolicy
@Binds
fun provideShadeExpansionIntent(impl: StatusBarTouchShadeDisplayPolicy): ShadeExpansionIntent
+}
- @IntoSet
- @Binds
- fun provideDefaultDisplayPolicyToSet(impl: DefaultDisplayShadePolicy): ShadeDisplayPolicy
-
- @IntoSet
- @Binds
- fun provideAnyExternalShadeDisplayPolicyToSet(
- impl: AnyExternalShadeDisplayPolicy
- ): ShadeDisplayPolicy
-
- @Binds
- @IntoSet
- fun provideStatusBarTouchShadeDisplayPolicy(
- impl: StatusBarTouchShadeDisplayPolicy
- ): ShadeDisplayPolicy
+@Module
+internal object AllShadeDisplayPoliciesModule {
+ @Provides
+ @ElementsIntoSet
+ fun provideShadeDisplayPolicies(
+ defaultPolicy: DefaultDisplayShadePolicy,
+ externalPolicy: AnyExternalShadeDisplayPolicy,
+ statusBarPolicy: StatusBarTouchShadeDisplayPolicy,
+ focusPolicy: FocusShadeDisplayPolicy,
+ ): Set<ShadeDisplayPolicy> {
+ return setOf(defaultPolicy, externalPolicy, statusBarPolicy, focusPolicy)
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt b/packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt
index 91020aa7bdb0..b155ada87efd 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt
@@ -23,8 +23,6 @@ import com.android.app.tracing.coroutines.launchTraced
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.display.data.repository.DisplayRepository
-import com.android.systemui.keyguard.data.repository.KeyguardRepository
-import com.android.systemui.shade.ShadeOnDefaultDisplayWhenLocked
import com.android.systemui.shade.domain.interactor.NotificationShadeElement
import com.android.systemui.shade.domain.interactor.QSShadeElement
import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractor.ShadeElement
@@ -38,13 +36,10 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
/**
* Moves the shade on the last display that received a status bar touch.
@@ -57,9 +52,7 @@ class StatusBarTouchShadeDisplayPolicy
@Inject
constructor(
displayRepository: DisplayRepository,
- keyguardRepository: KeyguardRepository,
@Background private val backgroundScope: CoroutineScope,
- @ShadeOnDefaultDisplayWhenLocked private val shadeOnDefaultDisplayWhenLocked: Boolean,
private val shadeInteractor: Lazy<ShadeInteractor>,
private val qsShadeElement: Lazy<QSShadeElement>,
private val notificationElement: Lazy<NotificationShadeElement>,
@@ -72,20 +65,7 @@ constructor(
private var latestIntent = AtomicReference<ShadeElement?>()
private var timeoutJob: Job? = null
- override val displayId: StateFlow<Int> =
- if (shadeOnDefaultDisplayWhenLocked) {
- keyguardRepository.isKeyguardShowing
- .combine(currentDisplayId) { isKeyguardShowing, currentDisplayId ->
- if (isKeyguardShowing) {
- Display.DEFAULT_DISPLAY
- } else {
- currentDisplayId
- }
- }
- .stateIn(backgroundScope, SharingStarted.WhileSubscribed(), currentDisplayId.value)
- } else {
- currentDisplayId
- }
+ override val displayId: StateFlow<Int> = currentDisplayId
private var removalListener: Job? = null
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt
index edf503d03f3e..59d812403777 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt
@@ -16,17 +16,20 @@
package com.android.systemui.shade.domain.interactor
+import android.provider.Settings
import androidx.annotation.FloatRange
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.shade.data.repository.ShadeRepository
import com.android.systemui.shade.shared.flag.DualShade
import com.android.systemui.shade.shared.model.ShadeMode
+import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
/**
@@ -76,29 +79,53 @@ interface ShadeModeInteractor {
class ShadeModeInteractorImpl
@Inject
-constructor(@Application applicationScope: CoroutineScope, repository: ShadeRepository) :
- ShadeModeInteractor {
+constructor(
+ @Application applicationScope: CoroutineScope,
+ repository: ShadeRepository,
+ secureSettingsRepository: SecureSettingsRepository,
+) : ShadeModeInteractor {
+
+ private val isDualShadeEnabled: Flow<Boolean> =
+ secureSettingsRepository.boolSetting(
+ Settings.Secure.DUAL_SHADE,
+ defaultValue = DUAL_SHADE_ENABLED_DEFAULT,
+ )
override val isShadeLayoutWide: StateFlow<Boolean> = repository.isShadeLayoutWide
override val shadeMode: StateFlow<ShadeMode> =
- isShadeLayoutWide
- .map(this::determineShadeMode)
+ combine(isDualShadeEnabled, repository.isShadeLayoutWide, ::determineShadeMode)
.stateIn(
applicationScope,
SharingStarted.Eagerly,
- initialValue = determineShadeMode(isShadeLayoutWide.value),
+ initialValue =
+ determineShadeMode(
+ isDualShadeEnabled = DUAL_SHADE_ENABLED_DEFAULT,
+ isShadeLayoutWide = repository.isShadeLayoutWide.value,
+ ),
)
@FloatRange(from = 0.0, to = 1.0) override fun getTopEdgeSplitFraction(): Float = 0.5f
- private fun determineShadeMode(isShadeLayoutWide: Boolean): ShadeMode {
+ private fun determineShadeMode(
+ isDualShadeEnabled: Boolean,
+ isShadeLayoutWide: Boolean,
+ ): ShadeMode {
return when {
- DualShade.isEnabled -> ShadeMode.Dual
+ isDualShadeEnabled ||
+ // TODO(b/388793191): This ensures that the dual_shade aconfig flag can also enable
+ // Dual Shade, to avoid breaking unit tests. Remove this once all references to the
+ // flag are removed.
+ DualShade.isEnabled -> ShadeMode.Dual
isShadeLayoutWide -> ShadeMode.Split
else -> ShadeMode.Single
}
}
+
+ companion object {
+ /* Whether the Dual Shade setting is enabled by default. */
+ private const val DUAL_SHADE_ENABLED_DEFAULT = false
+ }
}
class ShadeModeInteractorEmptyImpl @Inject constructor() : ShadeModeInteractor {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 7fc1510f1136..dcea8d85e10d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -273,12 +273,12 @@ public class CommandQueue extends IStatusBar.Stub implements
default void toggleQuickSettingsPanel() { }
/**
- * Called to notify IME window status changes.
+ * Sets the new IME window status.
*
- * @param displayId The id of the display to notify.
- * @param vis IME visibility.
- * @param backDisposition Disposition mode of back button.
- * @param showImeSwitcher {@code true} to show IME switch button.
+ * @param displayId The id of the display to which the IME is bound.
+ * @param vis The IME window visibility.
+ * @param backDisposition The IME back disposition mode.
+ * @param showImeSwitcher Whether the IME Switcher button should be shown.
*/
default void setImeWindowStatus(int displayId, @ImeWindowVisibility int vis,
@BackDispositionMode int backDisposition, boolean showImeSwitcher) { }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index a5595edcbb95..4269f60e1c2a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -88,6 +88,7 @@ import com.android.keyguard.TrustGrantFlags;
import com.android.keyguard.logging.KeyguardLogger;
import com.android.settingslib.Utils;
import com.android.settingslib.fuelgauge.BatteryStatus;
+import com.android.systemui.Flags;
import com.android.systemui.biometrics.AuthController;
import com.android.systemui.biometrics.FaceHelpMessageDeferral;
import com.android.systemui.biometrics.FaceHelpMessageDeferralFactory;
@@ -199,7 +200,7 @@ public class KeyguardIndicationController {
private CharSequence mBiometricMessage;
private CharSequence mBiometricMessageFollowUp;
private BiometricSourceType mBiometricMessageSource;
- protected ColorStateList mInitialTextColorState;
+ private ColorStateList mInitialTextColorState;
private boolean mVisible;
private boolean mOrganizationOwnedDevice;
@@ -393,13 +394,27 @@ public class KeyguardIndicationController {
return mIndicationArea;
}
+ /**
+ * Notify controller about configuration changes.
+ */
+ public void onConfigurationChanged() {
+ // Get new text color in case theme has changed
+ if (Flags.indicationTextA11yFix()) {
+ setIndicationColorToThemeColor();
+ }
+ }
+
public void setIndicationArea(ViewGroup indicationArea) {
mIndicationArea = indicationArea;
mTopIndicationView = indicationArea.findViewById(R.id.keyguard_indication_text);
mLockScreenIndicationView = indicationArea.findViewById(
R.id.keyguard_indication_text_bottom);
- mInitialTextColorState = mTopIndicationView != null
- ? mTopIndicationView.getTextColors() : ColorStateList.valueOf(Color.WHITE);
+ if (Flags.indicationTextA11yFix()) {
+ setIndicationColorToThemeColor();
+ } else {
+ setIndicationTextColor(mTopIndicationView != null
+ ? mTopIndicationView.getTextColors() : ColorStateList.valueOf(Color.WHITE));
+ }
if (mRotateTextViewController != null) {
mRotateTextViewController.destroy();
}
@@ -436,6 +451,12 @@ public class KeyguardIndicationController {
mIsLogoutEnabledCallback);
}
+ @NonNull
+ private ColorStateList wallpaperTextColor() {
+ return ColorStateList.valueOf(
+ Utils.getColorAttrDefaultColor(mContext, R.attr.wallpaperTextColor));
+ }
+
/**
* Cleanup
*/
@@ -513,7 +534,7 @@ public class KeyguardIndicationController {
.setMessage(mContext.getResources().getString(
com.android.systemui.res.R.string.dismissible_keyguard_swipe)
)
- .setTextColor(mInitialTextColorState)
+ .setTextColor(getInitialTextColorState())
.build(),
/* updateImmediately */ true);
} else {
@@ -533,7 +554,7 @@ public class KeyguardIndicationController {
INDICATION_TYPE_DISCLOSURE,
new KeyguardIndication.Builder()
.setMessage(disclosure)
- .setTextColor(mInitialTextColorState)
+ .setTextColor(getInitialTextColorState())
.build(),
/* updateImmediately */ false);
}
@@ -602,7 +623,7 @@ public class KeyguardIndicationController {
INDICATION_TYPE_OWNER_INFO,
new KeyguardIndication.Builder()
.setMessage(finalInfo)
- .setTextColor(mInitialTextColorState)
+ .setTextColor(getInitialTextColorState())
.build(),
false);
} else {
@@ -624,7 +645,7 @@ public class KeyguardIndicationController {
INDICATION_TYPE_BATTERY,
new KeyguardIndication.Builder()
.setMessage(powerIndication)
- .setTextColor(mInitialTextColorState)
+ .setTextColor(getInitialTextColorState())
.build(),
animate);
} else {
@@ -645,7 +666,7 @@ public class KeyguardIndicationController {
new KeyguardIndication.Builder()
.setMessage(mContext.getResources().getText(
com.android.internal.R.string.lockscreen_storage_locked))
- .setTextColor(mInitialTextColorState)
+ .setTextColor(getInitialTextColorState())
.build(),
false);
} else {
@@ -666,7 +687,7 @@ public class KeyguardIndicationController {
.setMessage(mBiometricMessage)
.setForceAccessibilityLiveRegionAssertive()
.setMinVisibilityMillis(IMPORTANT_MSG_MIN_DURATION)
- .setTextColor(mInitialTextColorState)
+ .setTextColor(getInitialTextColorState())
.build(),
true
);
@@ -680,7 +701,7 @@ public class KeyguardIndicationController {
new KeyguardIndication.Builder()
.setMessage(mBiometricMessageFollowUp)
.setMinVisibilityMillis(IMPORTANT_MSG_MIN_DURATION)
- .setTextColor(mInitialTextColorState)
+ .setTextColor(getInitialTextColorState())
.build(),
true
);
@@ -711,7 +732,7 @@ public class KeyguardIndicationController {
INDICATION_TYPE_TRUST,
new KeyguardIndication.Builder()
.setMessage(trustGrantedIndication)
- .setTextColor(mInitialTextColorState)
+ .setTextColor(getInitialTextColorState())
.build(),
true);
hideBiometricMessage();
@@ -722,7 +743,7 @@ public class KeyguardIndicationController {
INDICATION_TYPE_TRUST,
new KeyguardIndication.Builder()
.setMessage(trustManagedIndication)
- .setTextColor(mInitialTextColorState)
+ .setTextColor(getInitialTextColorState())
.build(),
false);
} else {
@@ -751,7 +772,7 @@ public class KeyguardIndicationController {
INDICATION_TYPE_PERSISTENT_UNLOCK_MESSAGE,
new KeyguardIndication.Builder()
.setMessage(mPersistentUnlockMessage)
- .setTextColor(mInitialTextColorState)
+ .setTextColor(getInitialTextColorState())
.build(),
true);
} else {
@@ -792,7 +813,7 @@ public class KeyguardIndicationController {
new KeyguardIndication.Builder()
.setMessage(mContext.getString(
R.string.keyguard_indication_after_adaptive_auth_lock))
- .setTextColor(mInitialTextColorState)
+ .setTextColor(getInitialTextColorState())
.build(),
true);
} else {
@@ -1179,7 +1200,8 @@ public class KeyguardIndicationController {
} else {
message = mContext.getString(R.string.keyguard_retry);
}
- mStatusBarKeyguardViewManager.setKeyguardMessage(message, mInitialTextColorState,
+ mStatusBarKeyguardViewManager.setKeyguardMessage(message,
+ getInitialTextColorState(),
null);
}
} else {
@@ -1232,7 +1254,7 @@ public class KeyguardIndicationController {
public void dump(PrintWriter pw, String[] args) {
pw.println("KeyguardIndicationController:");
- pw.println(" mInitialTextColorState: " + mInitialTextColorState);
+ pw.println(" mInitialTextColorState: " + getInitialTextColorState());
pw.println(" mPowerPluggedInWired: " + mPowerPluggedInWired);
pw.println(" mPowerPluggedIn: " + mPowerPluggedIn);
pw.println(" mPowerCharged: " + mPowerCharged);
@@ -1253,6 +1275,22 @@ public class KeyguardIndicationController {
mRotateTextViewController.dump(pw, args);
}
+ protected ColorStateList getInitialTextColorState() {
+ return mInitialTextColorState;
+ }
+
+ private void setIndicationColorToThemeColor() {
+ mInitialTextColorState = wallpaperTextColor();
+ }
+
+ /**
+ * @deprecated Use {@link #setIndicationColorToThemeColor}
+ */
+ @Deprecated
+ private void setIndicationTextColor(ColorStateList color) {
+ mInitialTextColorState = color;
+ }
+
protected class BaseKeyguardCallback extends KeyguardUpdateMonitorCallback {
@Override
public void onTimeChanged() {
@@ -1358,7 +1396,7 @@ public class KeyguardIndicationController {
mBouncerMessageInteractor.setFaceAcquisitionMessage(helpString);
}
mStatusBarKeyguardViewManager.setKeyguardMessage(helpString,
- mInitialTextColorState, biometricSourceType);
+ getInitialTextColorState(), biometricSourceType);
} else if (mScreenLifecycle.getScreenState() == SCREEN_ON) {
if (isCoExFaceAcquisitionMessage && msgId == FACE_ACQUIRED_TOO_DARK) {
showBiometricMessage(
@@ -1655,7 +1693,7 @@ public class KeyguardIndicationController {
private void showErrorMessageNowOrLater(String errString, @Nullable String followUpMsg,
BiometricSourceType biometricSourceType) {
if (mStatusBarKeyguardViewManager.isBouncerShowing()) {
- mStatusBarKeyguardViewManager.setKeyguardMessage(errString, mInitialTextColorState,
+ mStatusBarKeyguardViewManager.setKeyguardMessage(errString, getInitialTextColorState(),
biometricSourceType);
} else if (mScreenLifecycle.getScreenState() == SCREEN_ON) {
showBiometricMessage(errString, followUpMsg, biometricSourceType);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java
index ccea254defaa..4c6fa4839e5c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java
@@ -284,7 +284,8 @@ public class NotificationListener extends NotificationListenerWithPlugins implem
/* rankingAdjustment= */ 0,
/* isBubble= */ false,
/* proposedImportance= */ 0,
- /* sensitiveContent= */ false
+ /* sensitiveContent= */ false,
+ /* summarization = */ null
);
}
return ranking;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/headsup/shared/StatusBarNoHunBehavior.kt b/packages/SystemUI/src/com/android/systemui/statusbar/headsup/shared/StatusBarNoHunBehavior.kt
index 2ae54d7c6c83..c9024d934068 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/headsup/shared/StatusBarNoHunBehavior.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/headsup/shared/StatusBarNoHunBehavior.kt
@@ -33,7 +33,7 @@ object StatusBarNoHunBehavior {
/** Is the refactor enabled */
@JvmStatic
inline val isEnabled
- get() = Flags.statusBarNoHunBehavior() && android.app.Flags.notificationsRedesignAppIcons()
+ get() = Flags.statusBarNoHunBehavior()
/**
* Called to ensure code is only run when the flag is enabled. This protects users from the
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt
index 3825c098ca5d..b6ef95893036 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.notification
import android.app.Notification
+import android.app.Notification.EXTRA_SUMMARIZED_CONTENT
import android.content.Context
import android.content.pm.LauncherApps
import android.graphics.drawable.AnimatedImageDrawable
@@ -66,6 +67,12 @@ constructor(
messagingStyle.shortcutIcon = launcherApps.getShortcutIcon(shortcutInfo)
shortcutInfo.label?.let { label -> messagingStyle.conversationTitle = label }
}
+ if (NmSummarizationUiFlag.isEnabled) {
+ entry.sbn.notification.extras.putCharSequence(
+ EXTRA_SUMMARIZED_CONTENT, entry.ranking.summarization
+ )
+ }
+
messagingStyle.unreadMessageCount =
conversationNotificationManager.getUnreadCount(entry, recoveredBuilder)
return messagingStyle
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NmSummarizationUiFlag.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NmSummarizationUiFlag.kt
new file mode 100644
index 000000000000..feac0a514828
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NmSummarizationUiFlag.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification
+
+import android.app.Flags;
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/**
+ * Helper for android.app.nm_summarization and android.nm_summarization_ui. The new functionality
+ * should be enabled if either flag is enabled.
+ */
+@Suppress("NOTHING_TO_INLINE")
+object NmSummarizationUiFlag {
+ const val FLAG_DESC = "android.app.nm_summarization(_ui)"
+
+ @JvmStatic
+ inline val isEnabled
+ get() = Flags.nmSummarizationUi() || Flags.nmSummarization()
+
+ /**
+ * Called to ensure code is only run when the flag is enabled. This protects users from the
+ * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+ * build to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun isUnexpectedlyInLegacyMode() =
+ RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_DESC)
+
+ /**
+ * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+ * the flag is enabled to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun assertInLegacyMode() =
+ RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_DESC)
+} \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index 417e57d2205f..5cc79df9130a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -836,6 +836,14 @@ public final class NotificationEntry extends ListEntry {
}
/**
+ * Returns whether the NotificationEntry is promoted ongoing.
+ */
+ @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
+ public boolean isOngoingPromoted() {
+ return mSbn.getNotification().isPromotedOngoing();
+ }
+
+ /**
* Returns whether this row is considered blockable (i.e. it's not a system notif
* or is not in an allowList).
*/
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt
index 331ef1c01596..aa5008b8416e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt
@@ -40,6 +40,7 @@ internal constructor(
@RedactionType val redactionType: Int,
val isChildInGroup: Boolean,
val isGroupSummary: Boolean,
+ val summarization: String?,
) {
companion object {
@JvmStatic
@@ -61,6 +62,7 @@ internal constructor(
AsyncGroupHeaderViewInflation.isEnabled &&
!oldAdjustment.isGroupSummary &&
newAdjustment.isGroupSummary -> true
+ oldAdjustment.summarization != newAdjustment.summarization -> true
else -> false
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt
index 97e55c19d2f4..465bc288cbc1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt
@@ -152,5 +152,6 @@ constructor(
},
isChildInGroup = entry.hasEverBeenGroupChild(),
isGroupSummary = entry.hasEverBeenGroupSummary(),
+ summarization = entry.ranking.summarization
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
index d401283aa84e..96192b1ea315 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
@@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.footer.ui.view;
import static android.graphics.PorterDuff.Mode.SRC_ATOP;
import static com.android.systemui.Flags.notificationFooterBackgroundTintOptimization;
+import static com.android.systemui.Flags.notificationShadeBlur;
import static com.android.systemui.util.ColorUtilKt.hexColorString;
import android.annotation.ColorInt;
@@ -29,6 +30,7 @@ import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.content.res.Resources;
+import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
@@ -39,6 +41,8 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
+import com.android.internal.graphics.ColorUtils;
+import com.android.systemui.common.shared.colors.SurfaceEffectColors;
import com.android.systemui.res.R;
import com.android.systemui.scene.shared.flag.SceneContainerFlag;
import com.android.systemui.statusbar.notification.ColorUpdateLogger;
@@ -383,9 +387,23 @@ public class FooterView extends StackScrollerDecorView {
final Drawable historyBg = NotifRedesignFooter.isEnabled()
? theme.getDrawable(R.drawable.notif_footer_btn_background) : null;
final @ColorInt int scHigh;
+
if (!notificationFooterBackgroundTintOptimization()) {
- scHigh = mContext.getColor(
- com.android.internal.R.color.materialColorSurfaceContainerHigh);
+ if (notificationShadeBlur()) {
+ Color backgroundColor = Color.valueOf(
+ SurfaceEffectColors.surfaceEffect0(getResources()));
+ scHigh = ColorUtils.setAlphaComponent(backgroundColor.toArgb(), 0xFF);
+ // Apply alpha on background drawables.
+ int backgroundAlpha = (int) (backgroundColor.alpha() * 0xFF);
+ clearAllBg.setAlpha(backgroundAlpha);
+ settingsBg.setAlpha(backgroundAlpha);
+ if (historyBg != null) {
+ historyBg.setAlpha(backgroundAlpha);
+ }
+ } else {
+ scHigh = mContext.getColor(
+ com.android.internal.R.color.materialColorSurfaceContainerHigh);
+ }
if (scHigh != 0) {
final ColorFilter bgColorFilter = new PorterDuffColorFilter(scHigh, SRC_ATOP);
clearAllBg.setColorFilter(bgColorFilter);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java
index 0171fb72e158..be61dc95fe20 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java
@@ -706,7 +706,7 @@ public class HeadsUpManagerImpl
}
private void updateHeadsUpFlow() {
- mHeadsUpNotificationRows.setValue(new HashSet<>(getHeadsUpEntryPhoneMap().values()));
+ mHeadsUpNotificationRows.setValue(new HashSet<>(mHeadsUpEntryMap.values()));
}
@Override
@@ -732,11 +732,6 @@ public class HeadsUpManagerImpl
return mHeadsUpAnimatingAway.getValue();
}
- @NonNull
- private ArrayMap<String, HeadsUpEntry> getHeadsUpEntryPhoneMap() {
- return mHeadsUpEntryMap;
- }
-
/**
* Called to notify the listeners that the HUN animating away animation has ended.
*/
@@ -1014,7 +1009,7 @@ public class HeadsUpManagerImpl
@Override
public void setRemoteInputActive(
@NonNull NotificationEntry entry, boolean remoteInputActive) {
- HeadsUpEntry headsUpEntry = getHeadsUpEntryPhone(entry.getKey());
+ HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(entry.getKey());
if (headsUpEntry != null && headsUpEntry.mRemoteInputActive != remoteInputActive) {
headsUpEntry.mRemoteInputActive = remoteInputActive;
if (ExpandHeadsUpOnInlineReply.isEnabled() && remoteInputActive) {
@@ -1029,11 +1024,6 @@ public class HeadsUpManagerImpl
}
}
- @Nullable
- private HeadsUpEntry getHeadsUpEntryPhone(@NonNull String key) {
- return mHeadsUpEntryMap.get(key);
- }
-
@Override
public void setGutsShown(@NonNull NotificationEntry entry, boolean gutsShown) {
HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey());
@@ -1125,7 +1115,7 @@ public class HeadsUpManagerImpl
return true;
}
- HeadsUpEntry headsUpEntry = getHeadsUpEntryPhone(key);
+ HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key);
HeadsUpEntry topEntry = getTopHeadsUpEntryPhone();
if (headsUpEntry == null || headsUpEntry != topEntry) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BundleNotificationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BundleNotificationInfo.java
index aad618d50067..9c6e41c482b6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BundleNotificationInfo.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BundleNotificationInfo.java
@@ -64,11 +64,11 @@ public class BundleNotificationInfo extends NotificationInfo {
boolean isNonblockable,
boolean wasShownHighPriority,
AssistantFeedbackController assistantFeedbackController,
- MetricsLogger metricsLogger) throws RemoteException {
+ MetricsLogger metricsLogger, OnClickListener onCloseClick) throws RemoteException {
super.bindNotification(pm, iNotificationManager, onUserInteractionCallback,
channelEditorDialogController, pkg, notificationChannel, entry, onSettingsClick,
onAppSettingsClick, uiEventLogger, isDeviceProvisioned, isNonblockable,
- wasShownHighPriority, assistantFeedbackController, metricsLogger);
+ wasShownHighPriority, assistantFeedbackController, metricsLogger, onCloseClick);
// Additionally, bind the feedback button.
ComponentName assistant = iNotificationManager.getAllowedNotificationAssistant();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 5d7b8e6e8a84..d986aaebc0f8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -21,6 +21,7 @@ import static android.app.Notification.Action.SEMANTIC_ACTION_MARK_CONVERSATION_
import static android.service.notification.NotificationListenerService.REASON_CANCEL;
import static com.android.systemui.flags.Flags.ENABLE_NOTIFICATIONS_SIMULATE_SLOW_MEASURE;
+import static com.android.systemui.Flags.notificationsPinnedHunInShade;
import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.PARENT_DISMISSED;
import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP;
import static com.android.systemui.statusbar.policy.RemoteInputView.FOCUS_ANIMATION_MIN_SCALE;
@@ -106,6 +107,7 @@ import com.android.systemui.statusbar.notification.headsup.HeadsUpManager;
import com.android.systemui.statusbar.notification.headsup.PinnedStatus;
import com.android.systemui.statusbar.notification.logging.NotificationCounters;
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
+import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded;
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag;
import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation;
import com.android.systemui.statusbar.notification.row.shared.LockscreenOtpRedaction;
@@ -163,6 +165,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
private boolean mShowSnooze = false;
private boolean mIsFaded;
+ private boolean mIsPromotedOngoing = false;
+
/**
* Listener for when {@link ExpandableNotificationRow} is laid out.
*/
@@ -196,6 +200,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
private int mMaxSmallHeightBeforeS;
private int mMaxSmallHeight;
private int mMaxExpandedHeight;
+ private int mMaxExpandedHeightForPromotedOngoing;
private int mNotificationLaunchHeight;
private boolean mMustStayOnScreen;
@@ -331,6 +336,15 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
private boolean mSaveSpaceOnLockscreen;
/**
+ * It is added for unit testing purpose.
+ * Please do not use it for other purposes.
+ */
+ @VisibleForTesting
+ public void setIgnoreLockscreenConstraints(boolean ignoreLockscreenConstraints) {
+ mIgnoreLockscreenConstraints = ignoreLockscreenConstraints;
+ }
+
+ /**
* True if we use intrinsic height regardless of vertical space available on lockscreen.
*/
private boolean mIgnoreLockscreenConstraints;
@@ -804,6 +818,13 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
private void updateLimitsForView(NotificationContentView layout) {
+ final int maxExpandedHeight;
+ if (isPromotedOngoing()) {
+ maxExpandedHeight = mMaxExpandedHeightForPromotedOngoing;
+ } else {
+ maxExpandedHeight = mMaxExpandedHeight;
+ }
+
View contractedView = layout.getContractedChild();
boolean customView = contractedView != null
&& contractedView.getId()
@@ -824,7 +845,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
smallHeight = mMaxSmallHeightBeforeS;
}
} else if (isCallLayout) {
- smallHeight = mMaxExpandedHeight;
+ smallHeight = maxExpandedHeight;
} else {
smallHeight = mMaxSmallHeight;
}
@@ -848,7 +869,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
if (headsUpWrapper != null) {
headsUpHeight = Math.max(headsUpHeight, headsUpWrapper.getMinLayoutHeight());
}
- layout.setHeights(smallHeight, headsUpHeight, mMaxExpandedHeight);
+
+ layout.setHeights(smallHeight, headsUpHeight, maxExpandedHeight);
}
@NonNull
@@ -1258,6 +1280,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
if (mIsSummaryWithChildren) {
return mChildrenContainer.getIntrinsicHeight();
}
+ if (isPromotedOngoing()) {
+ return getMaxExpandHeight();
+ }
if (mExpandedWhenPinned) {
return Math.max(getMaxExpandHeight(), getHeadsUpHeight());
} else if (android.app.Flags.compactHeadsUpNotification()
@@ -2077,6 +2102,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
mMaxExpandedHeight = NotificationUtils.getFontScaledHeight(mContext,
R.dimen.notification_max_height);
+ mMaxExpandedHeightForPromotedOngoing = NotificationUtils.getFontScaledHeight(mContext,
+ R.dimen.notification_max_height_for_promoted_ongoing);
mMaxHeadsUpHeightBeforeN = NotificationUtils.getFontScaledHeight(mContext,
R.dimen.notification_max_heads_up_height_legacy);
mMaxHeadsUpHeightBeforeP = NotificationUtils.getFontScaledHeight(mContext,
@@ -2762,6 +2789,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
if (mIsSummaryWithChildren && !shouldShowPublic()) {
return !mChildrenExpanded;
}
+ if (isPromotedOngoing()) {
+ return false;
+ }
return mEnableNonGroupedNotificationExpand && mExpandable;
}
@@ -2770,6 +2800,18 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
mPrivateLayout.updateExpandButtons(isExpandable());
}
+ /**
+ * Set this notification to be promoted ongoing
+ */
+ public void setPromotedOngoing(boolean promotedOngoing) {
+ if (PromotedNotificationUiForceExpanded.isUnexpectedlyInLegacyMode()) {
+ return;
+ }
+
+ mIsPromotedOngoing = promotedOngoing;
+ setExpandable(!mIsPromotedOngoing);
+ }
+
@Override
public void setClipToActualHeight(boolean clipToActualHeight) {
super.setClipToActualHeight(clipToActualHeight || isUserLocked());
@@ -2839,6 +2881,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
public void setUserLocked(boolean userLocked) {
+ if (isPromotedOngoing()) return;
+
mUserLocked = userLocked;
mPrivateLayout.setUserExpanding(userLocked);
// This is intentionally not guarded with mIsSummaryWithChildren since we might have had
@@ -3000,6 +3044,35 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
}
+ public boolean isPromotedOngoing() {
+ return PromotedNotificationUiForceExpanded.isEnabled() && mIsPromotedOngoing;
+ }
+
+ private boolean isPromotedNotificationExpanded(boolean allowOnKeyguard) {
+ // public view in non group notifications is always collapsed.
+ if (shouldShowPublic()) {
+ return false;
+ }
+ // RON will always be expanded when it is not on keyguard.
+ if (!mOnKeyguard) {
+ return true;
+ }
+ // RON will always be expanded when it is allowed on keyguard.
+ // allowOnKeyguard is used for getting the maximum height by NotificationContentView and
+ // NotificationChildrenContainer.
+ if (allowOnKeyguard) {
+ return true;
+ }
+
+ // RON will be expanded when it needs to ignore lockscreen constraints.
+ if (mIgnoreLockscreenConstraints) {
+ return true;
+ }
+
+ // RON will need be collapsed when it needs to save space on the lock screen.
+ return !mSaveSpaceOnLockscreen;
+ }
+
/**
* Check whether the view state is currently expanded. This is given by the system in {@link
* #setSystemExpanded(boolean)} and can be overridden by user expansion or
@@ -3013,6 +3086,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
public boolean isExpanded(boolean allowOnKeyguard) {
+ if (isPromotedOngoing()) {
+ return isPromotedNotificationExpanded(allowOnKeyguard);
+ }
+
return (!shouldShowPublic()) && (!mOnKeyguard || allowOnKeyguard)
&& (!hasUserChangedExpansion()
&& (isSystemExpanded() || isSystemChildExpanded())
@@ -3184,7 +3261,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
@Override
public boolean mustStayOnScreen() {
- return mIsHeadsUp && mMustStayOnScreen;
+ // Must stay on screen in the open shade regardless how much the stack is scrolled if:
+ // 1. Is HUN and not marked as seen yet (isHeadsUp && mustStayOnScreen)
+ // 2. Is an FSI HUN (isPinned)
+ return mIsHeadsUp && mMustStayOnScreen || notificationsPinnedHunInShade() && isPinned();
}
/**
@@ -4011,6 +4091,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
+ (!shouldShowPublic() && mIsSummaryWithChildren));
pw.print(", mShowNoBackground: " + mShowNoBackground);
pw.print(", clipBounds: " + getClipBounds());
+ if (PromotedNotificationUiForceExpanded.isEnabled()) {
+ pw.print(", isPromotedOngoing: " + isPromotedOngoing());
+ pw.print(", isExpandable: " + isExpandable());
+ pw.print(", mExpandable: " + mExpandable);
+ }
pw.println();
if (NotificationContentView.INCLUDE_HEIGHTS_TO_DUMP) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java
index 1ff0d9262476..92c10abff735 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java
@@ -288,14 +288,21 @@ public class HybridConversationNotificationView extends HybridNotificationView {
public void setText(
CharSequence titleText,
CharSequence contentText,
- CharSequence conversationSenderName
+ CharSequence conversationSenderName,
+ @Nullable String summarization
) {
if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) return;
- if (conversationSenderName == null) {
+ if (summarization != null) {
mConversationSenderName.setVisibility(GONE);
+ titleText = null;
+ contentText = summarization;
} else {
- mConversationSenderName.setVisibility(VISIBLE);
- mConversationSenderName.setText(conversationSenderName);
+ if (conversationSenderName == null) {
+ mConversationSenderName.setVisibility(GONE);
+ } else {
+ mConversationSenderName.setVisibility(VISIBLE);
+ mConversationSenderName.setText(conversationSenderName);
+ }
}
// TODO (b/217799515): super.bind() doesn't use contentView, remove the contentView
// argument when the flag is removed
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt
index 793b3b8b1e42..6aa5e405f29c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.notification.row
+import android.animation.ValueAnimator
import android.content.Context
import android.graphics.BlendMode
import android.graphics.Canvas
@@ -38,8 +39,8 @@ import androidx.compose.ui.viewinterop.AndroidView
import com.android.internal.graphics.ColorUtils
import com.android.systemui.res.R
import com.android.systemui.surfaceeffects.shaderutil.ShaderUtilLibrary
-import kotlin.math.max
-import kotlin.math.roundToInt
+import com.android.wm.shell.shared.animation.Interpolators
+import kotlin.math.min
/**
* A background style for smarter-smart-actions. The style is composed by a simplex3d noise,
@@ -48,7 +49,7 @@ import kotlin.math.roundToInt
class MagicActionBackgroundDrawable(
context: Context,
primaryContainer: Int? = null,
- private val seed: Float = 0f,
+ seed: Float = 0f,
) : Drawable() {
private val pixelDensity = context.resources.displayMetrics.density
@@ -60,17 +61,15 @@ class MagicActionBackgroundDrawable(
.toFloat()
private val buttonShape = Path()
private val paddingVertical =
- context.resources
- .getDimensionPixelSize(R.dimen.smart_reply_button_padding_vertical)
- .toFloat()
+ context.resources.getDimensionPixelSize(R.dimen.smart_action_button_icon_padding).toFloat()
/** The color of the button background. */
private val mainColor =
primaryContainer
?: context.getColor(com.android.internal.R.color.materialColorPrimaryContainer)
- /** Slightly dimmed down version of [mainColor] used on the simplex noise. */
- private val dimColor: Int
+ /** Slightly brighter version of [mainColor] used on the simplex noise. */
+ private val effectColor: Int
get() {
val labColor = arrayOf(0.0, 0.0, 0.0).toDoubleArray()
ColorUtils.colorToLAB(mainColor, labColor)
@@ -78,56 +77,97 @@ class MagicActionBackgroundDrawable(
return ColorUtils.CAMToColor(
camColor.hue,
camColor.chroma,
- max(0f, (labColor[0] - 20).toFloat()),
+ min(100f, (labColor[0] + 10).toFloat()),
)
}
private val bgShader = MagicActionBackgroundShader()
private val bgPaint = Paint()
private val outlinePaint = Paint()
+ private val gradientAnimator =
+ ValueAnimator.ofFloat(0f, 1f).apply {
+ duration = 2500
+ interpolator = Interpolators.LINEAR
+ addUpdateListener { invalidateSelf() }
+ }
+ private val turbulenceAnimator =
+ ValueAnimator.ofFloat(seed, seed + TURBULENCE_MOVEMENT).apply {
+ duration = ANIMATION_DURATION
+ interpolator = Interpolators.LINEAR
+ addUpdateListener { invalidateSelf() }
+ start()
+ }
+ private val effectFadeAnimation =
+ ValueAnimator.ofFloat(0f, 1f).apply {
+ duration = 1000
+ startDelay = ANIMATION_DURATION - 1000L
+ interpolator = Interpolators.STANDARD_DECELERATE
+ addUpdateListener { invalidateSelf() }
+ }
init {
bgShader.setColorUniform("in_color", mainColor)
- bgShader.setColorUniform("in_dimColor", dimColor)
+ bgShader.setColorUniform("in_effectColor", effectColor)
bgPaint.shader = bgShader
outlinePaint.style = Paint.Style.STROKE
// Stroke is doubled in width and then clipped, to avoid anti-aliasing artifacts at the edge
// of the rectangle.
outlinePaint.strokeWidth = outlineStrokeWidth * 2
outlinePaint.blendMode = BlendMode.SCREEN
- outlinePaint.alpha = (255 * 0.32f).roundToInt()
+ outlinePaint.alpha = OUTLINE_ALPHA
+
+ animate()
+ }
+
+ private fun animate() {
+ turbulenceAnimator.start()
+ gradientAnimator.start()
+ effectFadeAnimation.start()
}
override fun draw(canvas: Canvas) {
+ updateShaders()
+
// We clip instead of drawing 2 rounded rects, otherwise there will be artifacts where
// around the button background and the outline.
+ canvas.save()
canvas.clipPath(buttonShape)
+ canvas.drawPath(buttonShape, bgPaint)
+ canvas.drawPath(buttonShape, outlinePaint)
+ canvas.restore()
+ }
- canvas.drawRect(bounds, bgPaint)
- canvas.drawRoundRect(
- bounds.left.toFloat(),
- bounds.top + paddingVertical,
- bounds.right.toFloat(),
- bounds.bottom - paddingVertical,
- cornerRadius,
- cornerRadius,
- outlinePaint,
- )
+ private fun updateShaders() {
+ val effectAlpha = 1f - effectFadeAnimation.animatedValue as Float
+ val turbulenceZ = turbulenceAnimator.animatedValue as Float
+ bgShader.setFloatUniform("in_sparkleMove", turbulenceZ * 1000)
+ bgShader.setFloatUniform("in_noiseMove", 0f, 0f, turbulenceZ)
+ bgShader.setFloatUniform("in_turbulenceAlpha", effectAlpha)
+ bgShader.setFloatUniform("in_spkarkleAlpha", SPARKLE_ALPHA * effectAlpha)
+ val gradientOffset = gradientAnimator.animatedValue as Float * bounds.width()
+ val outlineGradient =
+ LinearGradient(
+ gradientOffset + bounds.left.toFloat(),
+ 0f,
+ gradientOffset + bounds.right.toFloat(),
+ 0f,
+ mainColor,
+ ColorUtils.setAlphaComponent(mainColor, 0),
+ Shader.TileMode.MIRROR,
+ )
+ outlinePaint.shader = outlineGradient
}
override fun onBoundsChange(bounds: Rect) {
super.onBoundsChange(bounds)
val width = bounds.width().toFloat()
- val height = bounds.height() - paddingVertical * 2
+ val height = bounds.height().toFloat()
if (width == 0f || height == 0f) return
bgShader.setFloatUniform("in_gridNum", NOISE_SIZE)
- bgShader.setFloatUniform("in_spkarkleAlpha", SPARKLE_ALPHA)
- bgShader.setFloatUniform("in_noiseMove", 0f, 0f, 0f)
bgShader.setFloatUniform("in_size", width, height)
bgShader.setFloatUniform("in_aspectRatio", width / height)
- bgShader.setFloatUniform("in_time", seed)
bgShader.setFloatUniform("in_pixelDensity", pixelDensity)
buttonShape.reset()
@@ -140,18 +180,6 @@ class MagicActionBackgroundDrawable(
cornerRadius,
Path.Direction.CW,
)
-
- val outlineGradient =
- LinearGradient(
- bounds.left.toFloat(),
- 0f,
- bounds.right.toFloat(),
- 0f,
- mainColor,
- ColorUtils.setAlphaComponent(mainColor, 0),
- Shader.TileMode.CLAMP,
- )
- outlinePaint.shader = outlineGradient
}
override fun setAlpha(alpha: Int) {
@@ -168,10 +196,15 @@ class MagicActionBackgroundDrawable(
companion object {
/** Smoothness of the turbulence. Larger numbers yield more detail. */
- private const val NOISE_SIZE = 0.7f
-
+ private const val NOISE_SIZE = 0.57f
/** Strength of the sparkles overlaid on the turbulence. */
private const val SPARKLE_ALPHA = 0.15f
+ /** Alpha (0..255) of the button outline */
+ private const val OUTLINE_ALPHA = 82
+ /** Turbulence grid size */
+ private const val TURBULENCE_MOVEMENT = 4.3f
+ /** Total animation duration in millis */
+ private const val ANIMATION_DURATION = 5000L
}
}
@@ -183,24 +216,25 @@ private class MagicActionBackgroundShader : RuntimeShader(SHADER) {
"""
uniform float in_gridNum;
uniform vec3 in_noiseMove;
+ uniform half in_sparkleMove;
uniform vec2 in_size;
uniform float in_aspectRatio;
- uniform half in_time;
uniform half in_pixelDensity;
+ uniform float in_turbulenceAlpha;
uniform float in_spkarkleAlpha;
layout(color) uniform vec4 in_color;
- layout(color) uniform vec4 in_dimColor;
+ layout(color) uniform vec4 in_effectColor;
"""
private const val MAIN_SHADER =
"""vec4 main(vec2 p) {
vec2 uv = p / in_size.xy;
uv.x *= in_aspectRatio;
vec3 noiseP = vec3(uv + in_noiseMove.xy, in_noiseMove.z) * in_gridNum;
- half luma = 1.0 - getLuminosity(half3(simplex3d(noiseP)));
- half4 turbulenceColor = mix(in_color, in_dimColor, luma);
- float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_time);
+ half luma = getLuminosity(half3(simplex3d(noiseP)));
+ half4 turbulenceColor = mix(in_color, in_effectColor, luma * in_turbulenceAlpha);
+ float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_sparkleMove);
sparkle = min(sparkle * in_spkarkleAlpha, in_spkarkleAlpha);
- return turbulenceColor + half4(half3(sparkle), 1.0);
+ return saturate(turbulenceColor + half4(sparkle));
}
"""
private const val SHADER = UNIFORMS + ShaderUtilLibrary.SHADER_LIB + MAIN_SHADER
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
index 57fe24f40acb..13ed6c449797 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
@@ -57,6 +57,7 @@ import com.android.systemui.statusbar.notification.ConversationNotificationProce
import com.android.systemui.statusbar.notification.InflationException;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor;
+import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded;
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel;
import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation;
import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation;
@@ -216,7 +217,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder
messagingStyle,
builder,
row.getContext(),
- false
+ false,
+ entry.getRanking().getSummarization()
);
// If the messagingStyle is null, we want to inflate the normal view
isConversation = viewModel.isConversation();
@@ -238,7 +240,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder
messagingStyle,
builder,
row.getContext(),
- true);
+ true,
+ entry.getRanking().getSummarization());
} else {
result.mPublicInflatedSingleLineViewModel =
SingleLineViewInflater.inflateRedactedSingleLineViewModel(
@@ -1111,6 +1114,10 @@ public class NotificationContentInflater implements NotificationRowContentBinder
entry.setHeadsUpStatusBarText(result.headsUpStatusBarText);
entry.setHeadsUpStatusBarTextPublic(result.headsUpStatusBarTextPublic);
+ if (PromotedNotificationUiForceExpanded.isEnabled()) {
+ row.setPromotedOngoing(entry.isOngoingPromoted());
+ }
+
Trace.endAsyncSection(APPLY_TRACE_METHOD, System.identityHashCode(row));
if (endListener != null) {
endListener.onAsyncInflationFinished(entry);
@@ -1313,7 +1320,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder
messagingStyle,
recoveredBuilder,
mContext,
- false
+ false,
+ mEntry.getRanking().getSummarization()
);
result.mInflatedSingleLineView =
SingleLineViewInflater.inflatePrivateSingleLineView(
@@ -1333,7 +1341,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder
messagingStyle,
recoveredBuilder,
mContext,
- true
+ true,
+ null
);
} else {
result.mPublicInflatedSingleLineViewModel =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
index 9712db8a1812..b1e5b22f9b1a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
@@ -70,10 +70,10 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider;
import com.android.systemui.statusbar.notification.collection.render.NotifGutsViewListener;
import com.android.systemui.statusbar.notification.collection.render.NotifGutsViewManager;
+import com.android.systemui.statusbar.notification.headsup.HeadsUpManager;
import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
import com.android.systemui.statusbar.phone.CentralSurfaces;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
-import com.android.systemui.statusbar.notification.headsup.HeadsUpManager;
import com.android.systemui.util.kotlin.JavaAdapter;
import com.android.systemui.wmshell.BubblesManager;
@@ -422,7 +422,8 @@ public class NotificationGutsManager implements NotifGutsViewManager, CoreStarta
row.getIsNonblockable(),
mHighPriorityProvider.isHighPriority(row.getEntry()),
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ row.getCloseButtonOnClickListener(row));
}
/**
@@ -476,7 +477,8 @@ public class NotificationGutsManager implements NotifGutsViewManager, CoreStarta
row.getIsNonblockable(),
mHighPriorityProvider.isHighPriority(row.getEntry()),
mAssistantFeedbackController,
- mMetricsLogger);
+ mMetricsLogger,
+ row.getCloseButtonOnClickListener(row));
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java
index 20120991b5ac..8d26f94ced21 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java
@@ -202,7 +202,7 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
boolean isNonblockable,
boolean wasShownHighPriority,
AssistantFeedbackController assistantFeedbackController,
- MetricsLogger metricsLogger)
+ MetricsLogger metricsLogger, OnClickListener onCloseClick)
throws RemoteException {
mINotificationManager = iNotificationManager;
mMetricsLogger = metricsLogger;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
index 6e8ec9576f80..bf738aa1128f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
@@ -42,6 +42,7 @@ import android.widget.FrameLayout.LayoutParams;
import com.android.app.animation.Interpolators;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.Flags;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.AlphaOptimizedImageView;
@@ -270,6 +271,10 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl
mInfoItem = createPartialConversationItem(mContext);
} else if (personNotifType >= PeopleNotificationIdentifier.TYPE_FULL_PERSON) {
mInfoItem = createConversationItem(mContext);
+ } else if (android.app.Flags.uiRichOngoing()
+ && Flags.permissionHelperUiRichOngoing()
+ && entry.getSbn().getNotification().isPromotedOngoing()) {
+ mInfoItem = createPromotedItem(mContext);
} else {
mInfoItem = createInfoItem(mContext);
}
@@ -682,6 +687,16 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl
R.drawable.ic_settings);
}
+ static NotificationMenuItem createPromotedItem(Context context) {
+ Resources res = context.getResources();
+ String infoDescription = res.getString(R.string.notification_menu_gear_description);
+ PromotedNotificationInfo infoContent =
+ (PromotedNotificationInfo) LayoutInflater.from(context).inflate(
+ R.layout.promoted_notification_info, null, false);
+ return new NotificationMenuItem(context, infoDescription, infoContent,
+ R.drawable.ic_settings);
+ }
+
static NotificationMenuItem createBundleItem(Context context) {
Resources res = context.getResources();
String infoDescription = res.getString(R.string.notification_menu_gear_description);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
index 0b299d965b09..f4aae6e288a7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
@@ -719,6 +719,7 @@ constructor(
builder = builder,
systemUiContext = systemUiContext,
redactText = false,
+ summarization = entry.ranking.summarization
)
} else null
@@ -735,6 +736,7 @@ constructor(
builder = builder,
systemUiContext = systemUiContext,
redactText = true,
+ summarization = null
)
} else {
SingleLineViewInflater.inflateRedactedSingleLineViewModel(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java
new file mode 100644
index 000000000000..e4a0fa5b534e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row;
+
+import android.app.INotificationManager;
+import android.app.NotificationChannel;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.RemoteException;
+import android.service.notification.StatusBarNotification;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.UiEventLogger;
+import com.android.systemui.res.R;
+import com.android.systemui.statusbar.notification.AssistantFeedbackController;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+
+/**
+ * The guts of a notification revealed when performing a long press, specifically
+ * for notifications that are shown as promoted. Contains extra controls to allow user to revoke
+ * app permissions for sending promoted notifications.
+ */
+public class PromotedNotificationInfo extends NotificationInfo {
+ private static final String TAG = "PromotedNotifInfoGuts";
+ private INotificationManager mNotificationManager;
+
+ public PromotedNotificationInfo(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void bindNotification(
+ PackageManager pm,
+ INotificationManager iNotificationManager,
+ OnUserInteractionCallback onUserInteractionCallback,
+ ChannelEditorDialogController channelEditorDialogController,
+ String pkg,
+ NotificationChannel notificationChannel,
+ NotificationEntry entry,
+ OnSettingsClickListener onSettingsClick,
+ OnAppSettingsClickListener onAppSettingsClick,
+ UiEventLogger uiEventLogger,
+ boolean isDeviceProvisioned,
+ boolean isNonblockable,
+ boolean wasShownHighPriority,
+ AssistantFeedbackController assistantFeedbackController,
+ MetricsLogger metricsLogger, OnClickListener onCloseClick) throws RemoteException {
+ super.bindNotification(pm, iNotificationManager, onUserInteractionCallback,
+ channelEditorDialogController, pkg, notificationChannel, entry, onSettingsClick,
+ onAppSettingsClick, uiEventLogger, isDeviceProvisioned, isNonblockable,
+ wasShownHighPriority, assistantFeedbackController, metricsLogger, onCloseClick);
+
+ mNotificationManager = iNotificationManager;
+
+ bindDismiss(entry.getSbn(), onCloseClick);
+ bindDemote(entry.getSbn(), pkg);
+ }
+
+
+ protected void bindDismiss(StatusBarNotification sbn,
+ View.OnClickListener onCloseClick) {
+ View dismissButton = findViewById(R.id.promoted_dismiss);
+
+ dismissButton.setOnClickListener(onCloseClick);
+ dismissButton.setVisibility(!sbn.isNonDismissable()
+ && dismissButton.hasOnClickListeners() ? VISIBLE : GONE);
+
+ }
+
+ protected void bindDemote(StatusBarNotification sbn, String packageName) {
+ View demoteButton = findViewById(R.id.promoted_demote);
+ demoteButton.setOnClickListener(getDemoteClickListener(sbn, packageName));
+ demoteButton.setVisibility(demoteButton.hasOnClickListeners() ? VISIBLE : GONE);
+ }
+
+ private OnClickListener getDemoteClickListener(StatusBarNotification sbn, String packageName) {
+ return ((View unusedView) -> {
+ try {
+ // TODO(b/391661009): Signal AutomaticPromotionCoordinator here
+ mNotificationManager.setCanBePromoted(packageName, sbn.getUid(), false, true);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Couldn't revoke live update permission", e);
+ }
+ });
+ }
+}
+
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt
index fe2803bfc5d6..c051513ef3b4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt
@@ -61,6 +61,7 @@ internal object SingleLineViewInflater {
builder: Notification.Builder,
systemUiContext: Context,
redactText: Boolean,
+ summarization: String?
): SingleLineViewModel {
if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) {
return SingleLineViewModel(null, null, null)
@@ -108,6 +109,7 @@ internal object SingleLineViewInflater {
conversationSenderName =
if (isGroupConversation) conversationTextData?.senderName else null,
avatar = conversationAvatar,
+ summarization = summarization
)
return SingleLineViewModel(
@@ -132,6 +134,7 @@ internal object SingleLineViewInflater {
.ic_redacted_notification_single_line_icon
)
),
+ null
)
} else {
null
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineViewBinder.kt
index a17197c1f8ea..a50fc4c7986a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineViewBinder.kt
@@ -32,6 +32,7 @@ object SingleLineViewBinder {
viewModel?.titleText,
viewModel?.contentText,
viewModel?.conversationData?.conversationSenderName,
+ viewModel?.conversationData?.summarization
)
} else {
// bind the title and content text views
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/SingleLineViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/SingleLineViewModel.kt
index d583fa5d97ed..32ded25f18a1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/SingleLineViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/SingleLineViewModel.kt
@@ -46,6 +46,7 @@ data class SingleLineViewModel(
data class ConversationData(
val conversationSenderName: CharSequence?,
val avatar: ConversationAvatar,
+ val summarization: String?
)
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
index a96d972af2c4..08bc8f5d5bb9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
@@ -29,6 +29,7 @@ import com.android.systemui.statusbar.LockscreenShadeTransitionController
import com.android.systemui.statusbar.StatusBarState.KEYGUARD
import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
+import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.ExpandableView
import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
@@ -463,7 +464,12 @@ constructor(
var size =
if (onLockscreen) {
- if (view is ExpandableNotificationRow && view.entry.isStickyAndNotDemoted) {
+ if (
+ view is ExpandableNotificationRow &&
+ (view.entry.isStickyAndNotDemoted ||
+ (PromotedNotificationUiForceExpanded.isEnabled &&
+ view.isPromotedOngoing))
+ ) {
height
} else {
view.getMinHeight(/* ignoreTemporaryStates= */ true).toFloat()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
index 1965b9538df0..f7401440cfcb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
@@ -36,6 +36,8 @@ import com.android.systemui.util.kotlin.DisposableHandles
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
/** Binds the shared notification container to its view-model. */
@SysUISingleton
@@ -143,21 +145,25 @@ constructor(
if (!SceneContainerFlag.isEnabled) {
if (Flags.magicPortraitWallpapers()) {
launch {
- viewModel
- .getNotificationStackAbsoluteBottom(
- calculateMaxNotifications = calculateMaxNotifications,
- calculateHeight = { maxNotifications ->
- notificationStackSizeCalculator.computeHeight(
- maxNotifs = maxNotifications,
- shelfHeight = controller.getShelfHeight().toFloat(),
- stack = controller.view,
- )
- },
- controller.getShelfHeight().toFloat(),
+ combine(
+ viewModel.getNotificationStackAbsoluteBottom(
+ calculateMaxNotifications = calculateMaxNotifications,
+ calculateHeight = { maxNotifications ->
+ notificationStackSizeCalculator.computeHeight(
+ maxNotifs = maxNotifications,
+ shelfHeight =
+ controller.getShelfHeight().toFloat(),
+ stack = controller.view,
+ )
+ },
+ controller.getShelfHeight().toFloat(),
+ ),
+ viewModel.configurationBasedDimensions.map { it.marginTop },
+ ::Pair,
)
- .collect { bottom ->
+ .collect { (bottom: Float, marginTop: Int) ->
keyguardInteractor.setNotificationStackAbsoluteBottom(
- bottom
+ marginTop + bottom
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java
index 16e9c717935c..a2f1ded042f4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java
@@ -26,24 +26,31 @@ import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
-import android.widget.TextView;
import androidx.annotation.StyleRes;
+import androidx.core.graphics.ColorUtils;
import com.android.app.animation.Interpolators;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.Flags;
import com.android.systemui.keyguard.KeyguardIndication;
import com.android.systemui.res.R;
+import com.android.systemui.shared.shadow.DoubleShadowTextView;
/**
* A view to show hints on Keyguard ("Swipe up to unlock", "Tap again to open").
*/
-public class KeyguardIndicationTextView extends TextView {
+public class KeyguardIndicationTextView extends DoubleShadowTextView {
+ // Minimum luminance for texts to receive shadows.
+ private static final float MIN_TEXT_SHADOW_LUMINANCE = 0.5f;
public static final long Y_IN_DURATION = 600L;
@StyleRes
private static int sStyleId = R.style.TextAppearance_Keyguard_BottomArea;
@StyleRes
+ private static int sStyleWithDoubleShadowTextId =
+ R.style.TextAppearance_Keyguard_BottomArea_DoubleShadow;
+ @StyleRes
private static int sButtonStyleId = R.style.TextAppearance_Keyguard_BottomArea_Button;
private boolean mAnimationsEnabled = true;
@@ -226,7 +233,14 @@ public class KeyguardIndicationTextView extends TextView {
if (mKeyguardIndicationInfo.getBackground() != null) {
setTextAppearance(sButtonStyleId);
} else {
- setTextAppearance(sStyleId);
+ // If text is transparent or dark color, don't draw any shadow
+ if (Flags.indicationTextA11yFix() && ColorUtils.calculateLuminance(
+ mKeyguardIndicationInfo.getTextColor().getDefaultColor())
+ > MIN_TEXT_SHADOW_LUMINANCE) {
+ setTextAppearance(sStyleWithDoubleShadowTextId);
+ } else {
+ setTextAppearance(sStyleId);
+ }
}
setBackground(mKeyguardIndicationInfo.getBackground());
setTextColor(mKeyguardIndicationInfo.getTextColor());
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
index 4c2bfe5ca257..40245aef4f67 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
@@ -23,6 +23,7 @@ import static com.android.systemui.Flags.updateUserSwitcherBackground;
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
+import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.ContentObserver;
@@ -56,6 +57,7 @@ import com.android.systemui.log.core.LogLevel;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.res.R;
import com.android.systemui.scene.shared.flag.SceneContainerFlag;
+import com.android.systemui.shade.ShadeDisplayAware;
import com.android.systemui.shade.ShadeViewStateProvider;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.StatusBarState;
@@ -114,6 +116,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
R.id.keyguard_hun_animator_start_tag);
private final CoroutineDispatcher mCoroutineDispatcher;
+ private final Context mContext;
private final CarrierTextController mCarrierTextController;
private final ConfigurationController mConfigurationController;
private final SystemStatusAnimationScheduler mAnimationScheduler;
@@ -129,7 +132,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
private final KeyguardStatusBarViewModel mKeyguardStatusBarViewModel;
private final BiometricUnlockController mBiometricUnlockController;
private final SysuiStatusBarStateController mStatusBarStateController;
- private final StatusBarContentInsetsProvider mInsetsProvider;
+ private final StatusBarContentInsetsProviderStore mInsetsProviderStore;
private final UserManager mUserManager;
private final StatusBarUserChipViewModel mStatusBarUserChipViewModel;
private final SecureSettings mSecureSettings;
@@ -314,6 +317,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
@Inject
public KeyguardStatusBarViewController(
@Main CoroutineDispatcher dispatcher,
+ @ShadeDisplayAware Context context,
KeyguardStatusBarView view,
CarrierTextController carrierTextController,
ConfigurationController configurationController,
@@ -347,6 +351,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
) {
super(view);
mCoroutineDispatcher = dispatcher;
+ mContext = context;
mCarrierTextController = carrierTextController;
mConfigurationController = configurationController;
mAnimationScheduler = animationScheduler;
@@ -362,7 +367,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
mKeyguardStatusBarViewModel = keyguardStatusBarViewModel;
mBiometricUnlockController = biometricUnlockController;
mStatusBarStateController = statusBarStateController;
- mInsetsProvider = statusBarContentInsetsProviderStore.getDefaultDisplay();
+ mInsetsProviderStore = statusBarContentInsetsProviderStore;
mUserManager = userManager;
mStatusBarUserChipViewModel = userChipViewModel;
mSecureSettings = secureSettings;
@@ -404,6 +409,10 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
mStatusOverlayHoverListenerFactory = statusOverlayHoverListenerFactory;
}
+ private StatusBarContentInsetsProvider insetsProvider() {
+ return mInsetsProviderStore.forDisplay(mContext.getDisplayId());
+ }
+
@Override
protected void onInit() {
super.onInit();
@@ -446,7 +455,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
.createDarkAwareListener(mSystemIconsContainer, mView.darkChangeFlow());
mSystemIconsContainer.setOnHoverListener(hoverListener);
mView.setOnApplyWindowInsetsListener(
- (view, windowInsets) -> mView.updateWindowInsets(windowInsets, mInsetsProvider));
+ (view, windowInsets) -> mView.updateWindowInsets(windowInsets, insetsProvider()));
mSecureSettings.registerContentObserverForUserSync(
Settings.Secure.STATUS_BAR_SHOW_VIBRATE_ICON,
false,
@@ -645,7 +654,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
* {@code OnApplyWindowInsetsListener}s.
*/
public void setDisplayCutout(@Nullable DisplayCutout displayCutout) {
- mView.setDisplayCutout(displayCutout, mInsetsProvider);
+ mView.setDisplayCutout(displayCutout, insetsProvider());
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
index 3f44f7bdef90..caf8a43b2aaf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
@@ -46,7 +46,6 @@ import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore
-import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
import com.android.systemui.statusbar.policy.Clock
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.window.StatusBarWindowStateController
@@ -84,7 +83,7 @@ private constructor(
private val configurationController: ConfigurationController,
private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory,
private val darkIconDispatcher: DarkIconDispatcher,
- private val statusBarContentInsetsProvider: StatusBarContentInsetsProvider,
+ private val statusBarContentInsetsProviderStore: StatusBarContentInsetsProviderStore,
private val lazyStatusBarShadeDisplayPolicy: Lazy<StatusBarTouchShadeDisplayPolicy>,
) : ViewController<PhoneStatusBarView>(view) {
@@ -92,6 +91,8 @@ private constructor(
private lateinit var clock: Clock
private lateinit var startSideContainer: View
private lateinit var endSideContainer: View
+ private val statusBarContentInsetsProvider
+ get() = statusBarContentInsetsProviderStore.forDisplay(context.displayId)
private val iconsOnTouchListener =
object : View.OnTouchListener {
@@ -189,11 +190,9 @@ private constructor(
init {
// These should likely be done in `onInit`, not `init`.
mView.setTouchEventHandler(PhoneStatusBarViewTouchHandler())
- mView.setHasCornerCutoutFetcher {
- statusBarContentInsetsProvider.currentRotationHasCornerCutout()
- }
- mView.setInsetsFetcher {
- statusBarContentInsetsProvider.getStatusBarContentInsetsForCurrentRotation()
+ statusBarContentInsetsProvider?.let {
+ mView.setHasCornerCutoutFetcher { it.currentRotationHasCornerCutout() }
+ mView.setInsetsFetcher { it.getStatusBarContentInsetsForCurrentRotation() }
}
mView.init(userChipViewModel)
}
@@ -393,7 +392,7 @@ private constructor(
configurationController,
statusOverlayHoverListenerFactory,
darkIconDispatcher,
- statusBarContentInsetsProviderStore.defaultDisplay,
+ statusBarContentInsetsProviderStore,
lazyStatusBarShadeDisplayPolicy,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index 0f6c3069609e..be48c3d928f4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -66,6 +66,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState;
import com.android.systemui.keyguard.shared.model.ScrimAlpha;
import com.android.systemui.keyguard.shared.model.TransitionState;
import com.android.systemui.keyguard.shared.model.TransitionStep;
+import com.android.systemui.keyguard.ui.transitions.BlurConfig;
import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel;
import com.android.systemui.res.R;
@@ -258,6 +259,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump
private int mScrimsVisibility;
private final TriConsumer<ScrimState, Float, GradientColors> mScrimStateListener;
private final LargeScreenShadeInterpolator mLargeScreenShadeInterpolator;
+ private final BlurConfig mBlurConfig;
private Consumer<Integer> mScrimVisibleListener;
private boolean mBlankScreen;
private boolean mScreenBlankingCallbackCalled;
@@ -339,9 +341,11 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump
KeyguardTransitionInteractor keyguardTransitionInteractor,
KeyguardInteractor keyguardInteractor,
@Main CoroutineDispatcher mainDispatcher,
- LargeScreenShadeInterpolator largeScreenShadeInterpolator) {
+ LargeScreenShadeInterpolator largeScreenShadeInterpolator,
+ BlurConfig blurConfig) {
mScrimStateListener = lightBarController::setScrimState;
mLargeScreenShadeInterpolator = largeScreenShadeInterpolator;
+ mBlurConfig = blurConfig;
// All scrims default alpha need to match bouncer background alpha to make sure the
// transitions involving the bouncer are smooth and don't overshoot the bouncer alpha.
mDefaultScrimAlpha =
@@ -406,7 +410,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump
final ScrimState[] states = ScrimState.values();
for (int i = 0; i < states.length; i++) {
- states[i].init(mScrimInFront, mScrimBehind, mDozeParameters, mDockManager);
+ states[i].init(mScrimInFront, mScrimBehind, mDozeParameters, mDockManager, mBlurConfig);
states[i].setScrimBehindAlphaKeyguard(mScrimBehindAlphaKeyguard);
states[i].setDefaultScrimAlpha(mDefaultScrimAlpha);
}
@@ -868,7 +872,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump
* bounds instead.
*/
public void setClipsQsScrim(boolean clipScrim) {
- if (Flags.notificationShadeBlur()) {
+ if (Flags.notificationShadeBlur() || Flags.bouncerUiRevamp()) {
// Never clip scrims when blur is enabled, colors of UI elements are supposed to "add"
// up across the scrims.
mClipsQsScrim = false;
@@ -1210,6 +1214,12 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump
dispatchBackScrimState(mScrimBehind.getViewAlpha());
}
+ if (Flags.bouncerUiRevamp()) {
+ // Blur the notification scrim as needed. The blur is needed only when we show the
+ // expanded shade behind the bouncer. Without it, the notification scrim outline is
+ // visible behind the bouncer.
+ mNotificationsScrim.setBlurRadius(mState.getNotifBlurRadius());
+ }
// We also want to hide FLAG_SHOW_WHEN_LOCKED activities under the scrim.
boolean hideFlagShowWhenLockedActivities =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
index 8170e6d91a0f..5f423cf35edd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
@@ -24,6 +24,7 @@ import android.graphics.Color;
import com.android.app.tracing.coroutines.TrackTracer;
import com.android.systemui.Flags;
import com.android.systemui.dock.DockManager;
+import com.android.systemui.keyguard.ui.transitions.BlurConfig;
import com.android.systemui.res.R;
import com.android.systemui.scrim.ScrimView;
import com.android.systemui.shade.ui.ShadeColors;
@@ -149,7 +150,14 @@ public enum ScrimState {
@Override
public void prepare(ScrimState previousState) {
if (Flags.bouncerUiRevamp()) {
- mBehindAlpha = 0f;
+ if (previousState == SHADE_LOCKED) {
+ mBehindAlpha = previousState.getBehindAlpha();
+ mNotifAlpha = previousState.getNotifAlpha();
+ mNotifBlurRadius = mBlurConfig.getMaxBlurRadiusPx();
+ } else {
+ mNotifAlpha = 0f;
+ mBehindAlpha = 0f;
+ }
mFrontAlpha = TRANSPARENT_BOUNCER_SCRIM_ALPHA;
mFrontTint = mSurfaceColor;
return;
@@ -395,6 +403,7 @@ public enum ScrimState {
DozeParameters mDozeParameters;
DockManager mDockManager;
boolean mDisplayRequiresBlanking;
+ protected BlurConfig mBlurConfig;
boolean mLaunchingAffordanceWithPreview;
boolean mOccludeAnimationPlaying;
boolean mWakeLockScreenSensorActive;
@@ -403,8 +412,12 @@ public enum ScrimState {
boolean mClipQsScrim;
int mBackgroundColor;
+ // This is needed to blur the scrim behind the scrimmed bouncer to avoid showing
+ // the notification section border
+ protected float mNotifBlurRadius = 0.0f;
+
public void init(ScrimView scrimInFront, ScrimView scrimBehind, DozeParameters dozeParameters,
- DockManager dockManager) {
+ DockManager dockManager, BlurConfig blurConfig) {
mBackgroundColor = scrimBehind.getContext().getColor(R.color.shade_scrim_background_dark);
mScrimInFront = scrimInFront;
mScrimBehind = scrimBehind;
@@ -412,6 +425,7 @@ public enum ScrimState {
mDozeParameters = dozeParameters;
mDockManager = dockManager;
mDisplayRequiresBlanking = dozeParameters.getDisplayNeedsBlanking();
+ mBlurConfig = blurConfig;
}
/** Prepare state for transition. */
@@ -518,4 +532,8 @@ public enum ScrimState {
public void setClipQsScrim(boolean clipsQsScrim) {
mClipQsScrim = clipsQsScrim;
}
+
+ public float getNotifBlurRadius() {
+ return mNotifBlurRadius;
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTransitionAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTransitionAnimatorController.kt
index 705a11df83fc..e12b21eb2c4a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTransitionAnimatorController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTransitionAnimatorController.kt
@@ -1,6 +1,7 @@
package com.android.systemui.statusbar.phone
import android.view.View
+import com.android.systemui.Flags
import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.animation.TransitionAnimator
import com.android.systemui.animation.TransitionAnimator.Companion.getProgress
@@ -22,7 +23,7 @@ class StatusBarTransitionAnimatorController(
private val notificationShadeWindowController: NotificationShadeWindowController,
private val commandQueue: CommandQueue,
@DisplayId private val displayId: Int,
- private val isLaunchForActivity: Boolean = true
+ private val isLaunchForActivity: Boolean = true,
) : ActivityTransitionAnimator.Controller by delegate {
private var hideIconsDuringLaunchAnimation: Boolean = true
@@ -41,8 +42,16 @@ class StatusBarTransitionAnimatorController(
}
override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
- delegate.onTransitionAnimationStart(isExpandingFullyAbove)
- shadeAnimationInteractor.setIsLaunchingActivity(true)
+ if (Flags.shadeLaunchAccessibility()) {
+ // We set this before calling the delegate to make sure that accessibility is disabled
+ // for the whole duration of the transition, so that we don't have stray TalkBack events
+ // once the animating view becomes invisible.
+ shadeAnimationInteractor.setIsLaunchingActivity(true)
+ delegate.onTransitionAnimationStart(isExpandingFullyAbove)
+ } else {
+ delegate.onTransitionAnimationStart(isExpandingFullyAbove)
+ shadeAnimationInteractor.setIsLaunchingActivity(true)
+ }
if (!isExpandingFullyAbove) {
shadeController.collapseWithDuration(
ActivityTransitionAnimator.TIMINGS.totalDuration.toInt()
@@ -59,7 +68,7 @@ class StatusBarTransitionAnimatorController(
override fun onTransitionAnimationProgress(
state: TransitionAnimator.State,
progress: Float,
- linearProgress: Float
+ linearProgress: Float,
) {
delegate.onTransitionAnimationProgress(state, progress, linearProgress)
val hideIcons =
@@ -67,7 +76,7 @@ class StatusBarTransitionAnimatorController(
ActivityTransitionAnimator.TIMINGS,
linearProgress,
ANIMATION_DELAY_ICON_FADE_IN,
- 100
+ 100,
) == 0.0f
if (hideIcons != hideIconsDuringLaunchAnimation) {
hideIconsDuringLaunchAnimation = hideIcons
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt
index 69b7e892a380..9795cda97f37 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt
@@ -49,8 +49,10 @@ import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.android.systemui.inputdevice.tutorial.ui.composable.DoneButton
+import com.android.systemui.keyboard.shortcut.ui.composable.hasCompactWindowSize
import com.android.systemui.res.R
import com.android.systemui.touchpad.tutorial.ui.gesture.isFourFingerTouchpadSwipe
import com.android.systemui.touchpad.tutorial.ui.gesture.isThreeFingerTouchpadSwipe
@@ -80,6 +82,7 @@ fun TutorialSelectionScreen(
}
),
) {
+ val padding = if (hasCompactWindowSize()) 24.dp else 60.dp
val configuration = LocalConfiguration.current
when (configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> {
@@ -88,7 +91,7 @@ fun TutorialSelectionScreen(
onHomeTutorialClicked = onHomeTutorialClicked,
onRecentAppsTutorialClicked = onRecentAppsTutorialClicked,
onSwitchAppsTutorialClicked = onSwitchAppsTutorialClicked,
- modifier = Modifier.weight(1f).padding(60.dp),
+ modifier = Modifier.weight(1f).padding(padding),
lastSelectedScreen,
)
}
@@ -98,7 +101,7 @@ fun TutorialSelectionScreen(
onHomeTutorialClicked = onHomeTutorialClicked,
onRecentAppsTutorialClicked = onRecentAppsTutorialClicked,
onSwitchAppsTutorialClicked = onSwitchAppsTutorialClicked,
- modifier = Modifier.weight(1f).padding(60.dp),
+ modifier = Modifier.weight(1f).padding(padding),
lastSelectedScreen,
)
}
@@ -106,7 +109,7 @@ fun TutorialSelectionScreen(
// because other composables have weight 1, Done button will be positioned first
DoneButton(
onDoneButtonClicked = onDoneButtonClicked,
- modifier = Modifier.padding(horizontal = 60.dp),
+ modifier = Modifier.padding(horizontal = padding),
)
}
}
@@ -146,7 +149,7 @@ private fun VerticalSelectionButtons(
lastSelectedScreen: Screen,
) {
Column(
- verticalArrangement = Arrangement.spacedBy(20.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier,
) {
@@ -244,8 +247,13 @@ private fun TutorialButton(
modifier = Modifier.width(30.dp).height(30.dp),
tint = iconColor,
)
- Spacer(modifier = Modifier.height(16.dp))
- Text(text = text, style = MaterialTheme.typography.headlineLarge, color = iconColor)
+ if (!hasCompactWindowSize()) Spacer(modifier = Modifier.height(16.dp))
+ Text(
+ text = text,
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.headlineLarge,
+ color = iconColor,
+ )
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt
index 39b434ad65f1..83b7c1818341 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt
@@ -16,7 +16,6 @@
package com.android.systemui.volume.dialog
-import android.app.Dialog
import android.content.Context
import android.graphics.PixelFormat
import android.os.Bundle
@@ -24,6 +23,7 @@ import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
+import androidx.activity.ComponentDialog
import com.android.app.tracing.coroutines.coroutineScopeTraced
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.lifecycle.repeatWhenAttached
@@ -40,7 +40,7 @@ constructor(
@Application context: Context,
private val componentFactory: VolumeDialogComponent.Factory,
private val visibilityInteractor: VolumeDialogVisibilityInteractor,
-) : Dialog(context, R.style.Theme_SystemUI_Dialog_Volume) {
+) : ComponentDialog(context, R.style.Theme_SystemUI_Dialog_Volume) {
init {
with(window!!) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt
index 8c018606ebda..e261ceebf33e 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt
@@ -17,6 +17,8 @@
package com.android.systemui.volume.dialog.domain.interactor
import android.annotation.SuppressLint
+import android.media.AudioManager.RINGER_MODE_NORMAL
+import android.media.AudioManager.RINGER_MODE_SILENT
import android.os.Handler
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.plugins.VolumeDialogController
@@ -60,10 +62,10 @@ constructor(
awaitClose { volumeDialogController.removeCallback(producer) }
}
.buffer(capacity = BUFFER_CAPACITY, onBufferOverflow = BufferOverflow.DROP_OLDEST)
- .shareIn(replay = 0, scope = coroutineScope, started = SharingStarted.WhileSubscribed())
+ .shareIn(replay = 0, scope = coroutineScope, started = SharingStarted.Eagerly)
.onStart { emit(VolumeDialogEventModel.SubscribedToEvents) }
- private class VolumeDialogEventModelProducer(
+ private inner class VolumeDialogEventModelProducer(
private val scope: ProducerScope<VolumeDialogEventModel>
) : VolumeDialogController.Callbacks {
override fun onShowRequested(reason: Int, keyguardLocked: Boolean, lockTaskModeState: Int) {
@@ -93,14 +95,6 @@ constructor(
// Configuration change is never emitted by the VolumeDialogControllerImpl now.
override fun onConfigurationChanged() = Unit
- override fun onShowVibrateHint() {
- scope.trySend(VolumeDialogEventModel.ShowVibrateHint)
- }
-
- override fun onShowSilentHint() {
- scope.trySend(VolumeDialogEventModel.ShowSilentHint)
- }
-
override fun onScreenOff() {
scope.trySend(VolumeDialogEventModel.ScreenOff)
}
@@ -113,16 +107,6 @@ constructor(
scope.trySend(VolumeDialogEventModel.AccessibilityModeChanged(showA11yStream == true))
}
- // Captions button is remove from the Volume Dialog
- override fun onCaptionComponentStateChanged(
- isComponentEnabled: Boolean,
- fromTooltip: Boolean,
- ) = Unit
-
- // Captions button is remove from the Volume Dialog
- override fun onCaptionEnabledStateChanged(isEnabled: Boolean, checkBeforeSwitch: Boolean) =
- Unit
-
override fun onShowCsdWarning(csdWarning: Int, durationMs: Int) {
scope.trySend(
VolumeDialogEventModel.ShowCsdWarning(
@@ -135,5 +119,25 @@ constructor(
override fun onVolumeChangedFromKey() {
scope.trySend(VolumeDialogEventModel.VolumeChangedFromKey)
}
+
+ // This should've been handled in side the controller itself.
+ override fun onShowVibrateHint() {
+ volumeDialogController.setRingerMode(RINGER_MODE_SILENT, false)
+ }
+
+ // This should've been handled in side the controller itself.
+ override fun onShowSilentHint() {
+ volumeDialogController.setRingerMode(RINGER_MODE_NORMAL, false)
+ }
+
+ // Captions button is remove from the Volume Dialog
+ override fun onCaptionComponentStateChanged(
+ isComponentEnabled: Boolean,
+ fromTooltip: Boolean,
+ ) = Unit
+
+ // Captions button is remove from the Volume Dialog
+ override fun onCaptionEnabledStateChanged(isEnabled: Boolean, checkBeforeSwitch: Boolean) =
+ Unit
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogEventModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogEventModel.kt
index 9793d2be6b98..a0214dc957a4 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogEventModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogEventModel.kt
@@ -38,10 +38,6 @@ sealed interface VolumeDialogEventModel {
data class LayoutDirectionChanged(val layoutDirection: Int) : VolumeDialogEventModel
- data object ShowVibrateHint : VolumeDialogEventModel
-
- data object ShowSilentHint : VolumeDialogEventModel
-
data object ScreenOff : VolumeDialogEventModel
data class ShowSafetyWarning(val flags: Int) : VolumeDialogEventModel
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt
index 40719185e290..73f6236393b2 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt
@@ -47,7 +47,6 @@ constructor(
private val audioSystemRepository: AudioSystemRepository,
private val ringerFeedbackRepository: VolumeDialogRingerFeedbackRepository,
) {
-
val ringerModel: Flow<VolumeDialogRingerModel> =
volumeDialogStateInteractor.volumeDialogState
.mapNotNull { toRingerModel(it) }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponent.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponent.kt
index 940c79c78d76..577e47bb3b83 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponent.kt
@@ -18,7 +18,6 @@ package com.android.systemui.volume.dialog.sliders.dagger
import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType
import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogOverscrollViewBinder
-import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderHapticsViewBinder
import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderViewBinder
import dagger.BindsInstance
import dagger.Subcomponent
@@ -33,8 +32,6 @@ interface VolumeDialogSliderComponent {
fun sliderViewBinder(): VolumeDialogSliderViewBinder
- fun sliderHapticsViewBinder(): VolumeDialogSliderHapticsViewBinder
-
fun overscrollViewBinder(): VolumeDialogOverscrollViewBinder
@Subcomponent.Factory
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/data/repository/VolumeDialogSliderTouchEventsRepository.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/data/repository/VolumeDialogSliderTouchEventsRepository.kt
index 82885d65c513..07954f850286 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/data/repository/VolumeDialogSliderTouchEventsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/data/repository/VolumeDialogSliderTouchEventsRepository.kt
@@ -16,8 +16,8 @@
package com.android.systemui.volume.dialog.sliders.data.repository
-import android.view.MotionEvent
import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope
+import com.android.systemui.volume.dialog.sliders.shared.model.SliderInputEvent
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -26,10 +26,11 @@ import kotlinx.coroutines.flow.filterNotNull
@VolumeDialogSliderScope
class VolumeDialogSliderTouchEventsRepository @Inject constructor() {
- private val mutableSliderTouchEvents: MutableStateFlow<MotionEvent?> = MutableStateFlow(null)
- val sliderTouchEvent: Flow<MotionEvent> = mutableSliderTouchEvents.filterNotNull()
+ private val mutableSliderTouchEvents: MutableStateFlow<SliderInputEvent.Touch?> =
+ MutableStateFlow(null)
+ val sliderTouchEvent: Flow<SliderInputEvent.Touch> = mutableSliderTouchEvents.filterNotNull()
- fun update(event: MotionEvent) {
- mutableSliderTouchEvents.tryEmit(MotionEvent.obtain(event))
+ fun update(touch: SliderInputEvent.Touch) {
+ mutableSliderTouchEvents.value = touch
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInputEventsInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInputEventsInteractor.kt
index c7b4184a9f2f..351832bd275a 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInputEventsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInputEventsInteractor.kt
@@ -16,7 +16,6 @@
package com.android.systemui.volume.dialog.sliders.domain.interactor
-import android.view.MotionEvent
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogCallbacksInteractor
import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogVisibilityInteractor
@@ -45,7 +44,7 @@ constructor(
val event: Flow<SliderInputEvent> =
merge(
- repository.sliderTouchEvent.map { SliderInputEvent.Touch(it) },
+ repository.sliderTouchEvent,
volumeDialogCallbacksInteractor.event
.filterIsInstance(VolumeDialogEventModel.VolumeChangedFromKey::class)
.map { SliderInputEvent.Button },
@@ -55,7 +54,7 @@ constructor(
event.onEach { visibilityInteractor.resetDismissTimeout() }.launchIn(coroutineScope)
}
- fun onTouchEvent(newEvent: MotionEvent) {
- repository.update(newEvent)
+ fun onTouchEvent(pointerEvent: SliderInputEvent.Touch) {
+ repository.update(pointerEvent)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/shared/model/SliderInputEvent.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/shared/model/SliderInputEvent.kt
index 37dbb4b3a81d..841730857d71 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/shared/model/SliderInputEvent.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/shared/model/SliderInputEvent.kt
@@ -16,12 +16,20 @@
package com.android.systemui.volume.dialog.sliders.shared.model
-import android.view.MotionEvent
-
/** Models input event happened on the Volume Slider */
sealed interface SliderInputEvent {
- data class Touch(val event: MotionEvent) : SliderInputEvent
+ interface Touch : SliderInputEvent {
+
+ val x: Float
+ val y: Float
+
+ data class Start(override val x: Float, override val y: Float) : Touch
+
+ data class Move(override val x: Float, override val y: Float) : Touch
+
+ data class End(override val x: Float, override val y: Float) : Touch
+ }
data object Button : SliderInputEvent
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinder.kt
index 8109b50aa34a..38feb69aad7b 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinder.kt
@@ -20,11 +20,9 @@ import android.view.View
import androidx.dynamicanimation.animation.FloatValueHolder
import androidx.dynamicanimation.animation.SpringAnimation
import androidx.dynamicanimation.animation.SpringForce
-import com.android.systemui.res.R
import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope
import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogOverscrollViewModel
import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogOverscrollViewModel.OverscrollEventModel
-import com.google.android.material.slider.Slider
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
@@ -51,10 +49,6 @@ constructor(private val viewModel: VolumeDialogOverscrollViewModel) {
)
.addUpdateListener { _, value, _ -> viewsToAnimate.setTranslationY(value) }
- view.requireViewById<Slider>(R.id.volume_dialog_slider).addOnChangeListener { s, value, _ ->
- viewModel.setSlider(value = value, min = s.valueFrom, max = s.valueTo)
- }
-
viewModel.overscrollEvent
.onEach { event ->
when (event) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinder.kt
deleted file mode 100644
index 5a7fbc6341f2..000000000000
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinder.kt
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.volume.dialog.sliders.ui
-
-import android.view.View
-import com.android.systemui.haptics.slider.HapticSlider
-import com.android.systemui.haptics.slider.HapticSliderPlugin
-import com.android.systemui.res.R
-import com.android.systemui.statusbar.VibratorHelper
-import com.android.systemui.util.time.SystemClock
-import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope
-import com.android.systemui.volume.dialog.sliders.shared.model.SliderInputEvent
-import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderInputEventsViewModel
-import com.google.android.material.slider.Slider
-import com.google.android.msdl.domain.MSDLPlayer
-import javax.inject.Inject
-import kotlin.math.roundToInt
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-
-@VolumeDialogSliderScope
-class VolumeDialogSliderHapticsViewBinder
-@Inject
-constructor(
- private val inputEventsViewModel: VolumeDialogSliderInputEventsViewModel,
- private val vibratorHelper: VibratorHelper,
- private val msdlPlayer: MSDLPlayer,
- private val systemClock: SystemClock,
-) {
-
- fun CoroutineScope.bind(view: View) {
- val sliderView = view.requireViewById<Slider>(R.id.volume_dialog_slider)
- val hapticSliderPlugin =
- HapticSliderPlugin(
- slider = HapticSlider.Slider(sliderView),
- vibratorHelper = vibratorHelper,
- msdlPlayer = msdlPlayer,
- systemClock = systemClock,
- )
- hapticSliderPlugin.startInScope(this)
-
- sliderView.addOnChangeListener { _, value, fromUser ->
- hapticSliderPlugin.onProgressChanged(value.roundToInt(), fromUser)
- }
- sliderView.addOnSliderTouchListener(
- object : Slider.OnSliderTouchListener {
-
- override fun onStartTrackingTouch(slider: Slider) {
- hapticSliderPlugin.onStartTrackingTouch()
- }
-
- override fun onStopTrackingTouch(slider: Slider) {
- hapticSliderPlugin.onStopTrackingTouch()
- }
- }
- )
-
- inputEventsViewModel.event
- .onEach {
- when (it) {
- is SliderInputEvent.Button -> hapticSliderPlugin.onKeyDown()
- is SliderInputEvent.Touch -> hapticSliderPlugin.onTouchEvent(it.event)
- }
- }
- .launchIn(this)
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
index d40302408dd6..21a392776235 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
@@ -16,96 +16,211 @@
package com.android.systemui.volume.dialog.sliders.ui
-import android.annotation.SuppressLint
+import android.graphics.drawable.Drawable
import android.view.View
-import androidx.dynamicanimation.animation.FloatPropertyCompat
-import androidx.dynamicanimation.animation.SpringAnimation
-import androidx.dynamicanimation.animation.SpringForce
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.SliderDefaults
+import androidx.compose.material3.SliderState
+import androidx.compose.material3.VerticalSlider
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.compose.ui.graphics.painter.DrawablePainter
+import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel
+import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.res.R
import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope
-import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderInputEventsViewModel
-import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderStateModel
+import com.android.systemui.volume.dialog.sliders.ui.compose.VolumeDialogSliderTrack
+import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogOverscrollViewModel
import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderViewModel
-import com.google.android.material.slider.Slider
-import com.google.android.material.slider.Slider.OnSliderTouchListener
+import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider
import javax.inject.Inject
+import kotlin.math.round
import kotlin.math.roundToInt
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.isActive
@VolumeDialogSliderScope
class VolumeDialogSliderViewBinder
@Inject
constructor(
private val viewModel: VolumeDialogSliderViewModel,
- private val inputViewModel: VolumeDialogSliderInputEventsViewModel,
+ private val overscrollViewModel: VolumeDialogOverscrollViewModel,
+ private val hapticsViewModelFactory: SliderHapticsViewModel.Factory,
) {
+ fun bind(view: View) {
+ val sliderComposeView: ComposeView = view.requireViewById(R.id.volume_dialog_slider)
+ sliderComposeView.setContent {
+ VolumeDialogSlider(
+ viewModel = viewModel,
+ overscrollViewModel = overscrollViewModel,
+ hapticsViewModelFactory =
+ if (com.android.systemui.Flags.hapticsForComposeSliders()) {
+ hapticsViewModelFactory
+ } else {
+ null
+ },
+ )
+ }
+ }
+}
- private val sliderValueProperty =
- object : FloatPropertyCompat<Slider>("value") {
- override fun getValue(slider: Slider): Float = slider.value
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+private fun VolumeDialogSlider(
+ viewModel: VolumeDialogSliderViewModel,
+ overscrollViewModel: VolumeDialogOverscrollViewModel,
+ hapticsViewModelFactory: SliderHapticsViewModel.Factory?,
+ modifier: Modifier = Modifier,
+) {
+
+ val colors =
+ SliderDefaults.colors(
+ thumbColor = MaterialTheme.colorScheme.primary,
+ activeTickColor = MaterialTheme.colorScheme.surfaceContainerHighest,
+ inactiveTickColor = MaterialTheme.colorScheme.primary,
+ activeTrackColor = MaterialTheme.colorScheme.primary,
+ inactiveTrackColor = MaterialTheme.colorScheme.surfaceContainerHighest,
+ )
+ val collectedSliderState by viewModel.state.collectAsStateWithLifecycle(null)
+ val sliderState = collectedSliderState ?: return
- override fun setValue(slider: Slider, value: Float) {
- slider.value = value
+ val interactionSource = remember { MutableInteractionSource() }
+ val hapticsViewModel: SliderHapticsViewModel? =
+ hapticsViewModelFactory?.let {
+ rememberViewModel(traceName = "SliderHapticsViewModel") {
+ it.create(
+ interactionSource,
+ sliderState.valueRange,
+ Orientation.Vertical,
+ VolumeHapticsConfigsProvider.sliderHapticFeedbackConfig(sliderState.valueRange),
+ VolumeHapticsConfigsProvider.seekableSliderTrackerConfig,
+ )
}
}
- private val springForce =
- SpringForce().apply {
- stiffness = SpringForce.STIFFNESS_MEDIUM
- dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY
- }
- @SuppressLint("ClickableViewAccessibility")
- fun CoroutineScope.bind(view: View) {
- var isInitialUpdate = true
- val sliderView: Slider = view.requireViewById(R.id.volume_dialog_slider)
- val animation = SpringAnimation(sliderView, sliderValueProperty)
- animation.spring = springForce
- sliderView.setOnTouchListener { _, event ->
- inputViewModel.onTouchEvent(event)
- false
- }
- sliderView.addOnChangeListener { _, value, fromUser ->
- viewModel.setStreamVolume(value.roundToInt(), fromUser)
+ val state =
+ remember(sliderState.valueRange) {
+ SliderState(
+ value = sliderState.value,
+ valueRange = sliderState.valueRange,
+ steps =
+ (sliderState.valueRange.endInclusive - sliderState.valueRange.start - 1)
+ .toInt(),
+ )
+ .apply {
+ onValueChangeFinished = {
+ viewModel.onStreamChangeFinished(value.roundToInt())
+ hapticsViewModel?.onValueChangeEnded()
+ }
+ setOnValueChangeListener {
+ value = it
+ hapticsViewModel?.addVelocityDataPoint(it)
+ overscrollViewModel.setSlider(
+ value = value,
+ min = valueRange.start,
+ max = valueRange.endInclusive,
+ )
+ viewModel.setStreamVolume(it, true)
+ }
+ }
}
- sliderView.addOnSliderTouchListener(
- object : OnSliderTouchListener {
- override fun onStartTrackingTouch(slider: Slider) {}
+ var lastDiscreteStep by remember { mutableFloatStateOf(round(sliderState.value)) }
+ LaunchedEffect(sliderState.value) {
+ state.value = sliderState.value
+ snapshotFlow { sliderState.value }
+ .map { round(it) }
+ .filter { it != lastDiscreteStep }
+ .distinctUntilChanged()
+ .collect { discreteStep ->
+ lastDiscreteStep = discreteStep
+ hapticsViewModel?.onValueChange(discreteStep)
+ }
+ }
- override fun onStopTrackingTouch(slider: Slider) {
- viewModel.onStreamChangeFinished(slider.value.roundToInt())
+ VerticalSlider(
+ state = state,
+ enabled = !sliderState.isDisabled,
+ reverseDirection = true,
+ colors = colors,
+ interactionSource = interactionSource,
+ modifier =
+ modifier.pointerInput(Unit) {
+ coroutineScope {
+ val currentContext = currentCoroutineContext()
+ awaitPointerEventScope {
+ while (currentContext.isActive) {
+ viewModel.onTouchEvent(awaitPointerEvent())
+ }
+ }
}
- }
- )
+ },
+ track = {
+ VolumeDialogSliderTrack(
+ state,
+ colors = colors,
+ isEnabled = !sliderState.isDisabled,
+ activeTrackEndIcon = { iconsState ->
+ VolumeIcon(sliderState.icon, iconsState.isActiveTrackEndIconVisible)
+ },
+ inactiveTrackEndIcon = { iconsState ->
+ VolumeIcon(sliderState.icon, !iconsState.isActiveTrackEndIconVisible)
+ },
+ )
+ },
+ )
+}
- viewModel.isDisabledByZenMode.onEach { sliderView.isEnabled = !it }.launchIn(this)
- viewModel.state
- .onEach {
- sliderView.setModel(it, animation, isInitialUpdate)
- isInitialUpdate = false
- }
- .launchIn(this)
+@Composable
+private fun BoxScope.VolumeIcon(
+ drawable: Drawable,
+ isVisible: Boolean,
+ modifier: Modifier = Modifier,
+) {
+ AnimatedVisibility(
+ visible = isVisible,
+ enter = fadeIn(animationSpec = tween(durationMillis = 50)),
+ exit = fadeOut(animationSpec = tween(durationMillis = 50)),
+ modifier = modifier.align(Alignment.Center).size(40.dp).padding(10.dp),
+ ) {
+ Icon(painter = DrawablePainter(drawable), contentDescription = null)
}
+}
- @SuppressLint("UseCompatLoadingForDrawables")
- private fun Slider.setModel(
- model: VolumeDialogSliderStateModel,
- animation: SpringAnimation,
- isInitialUpdate: Boolean,
- ) {
- valueFrom = model.minValue
- animation.setMinValue(model.minValue)
- valueTo = model.maxValue
- animation.setMaxValue(model.maxValue)
- // coerce the current value to the new value range before animating it. This prevents
- // animating from the value that is outside of current [valueFrom, valueTo].
- value = value.coerceIn(valueFrom, valueTo)
- trackIconActiveStart = model.icon
- if (isInitialUpdate) {
- value = model.value
- } else {
- animation.animateToFinalPosition(model.value)
- }
+@OptIn(ExperimentalMaterial3Api::class)
+fun SliderState.setOnValueChangeListener(onValueChange: ((Float) -> Unit)?) {
+ with(javaClass.getDeclaredField("onValueChange")) {
+ val oldIsAccessible = isAccessible
+ AutoCloseable { isAccessible = oldIsAccessible }
+ .use {
+ isAccessible = true
+ set(this@setOnValueChangeListener, onValueChange)
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt
index 75d427acc05b..c66955a0c187 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt
@@ -71,7 +71,6 @@ constructor(private val viewModel: VolumeDialogSlidersViewModel) {
viewsToAnimate: Array<View>,
) {
with(component.sliderViewBinder()) { bind(sliderContainer) }
- with(component.sliderHapticsViewBinder()) { bind(sliderContainer) }
with(component.overscrollViewBinder()) { bind(sliderContainer, viewsToAnimate) }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt
new file mode 100644
index 000000000000..1dd9ddac79be
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt
@@ -0,0 +1,347 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui.compose
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.SliderColors
+import androidx.compose.material3.SliderDefaults
+import androidx.compose.material3.SliderState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasurePolicy
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastFilter
+import androidx.compose.ui.util.fastFirst
+import kotlin.math.min
+
+@Composable
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
+fun VolumeDialogSliderTrack(
+ sliderState: SliderState,
+ colors: SliderColors,
+ isEnabled: Boolean,
+ modifier: Modifier = Modifier,
+ thumbTrackGapSize: Dp = 6.dp,
+ trackCornerSize: Dp = 12.dp,
+ trackInsideCornerSize: Dp = 2.dp,
+ trackSize: Dp = 40.dp,
+ activeTrackStartIcon: (@Composable BoxScope.(iconsState: SliderIconsState) -> Unit)? = null,
+ activeTrackEndIcon: (@Composable BoxScope.(iconsState: SliderIconsState) -> Unit)? = null,
+ inactiveTrackStartIcon: (@Composable BoxScope.(iconsState: SliderIconsState) -> Unit)? = null,
+ inactiveTrackEndIcon: (@Composable BoxScope.(iconsState: SliderIconsState) -> Unit)? = null,
+) {
+ val measurePolicy = remember(sliderState) { TrackMeasurePolicy(sliderState) }
+ Layout(
+ measurePolicy = measurePolicy,
+ content = {
+ SliderDefaults.Track(
+ sliderState = sliderState,
+ colors = colors,
+ enabled = isEnabled,
+ trackCornerSize = trackCornerSize,
+ trackInsideCornerSize = trackInsideCornerSize,
+ drawStopIndicator = null,
+ thumbTrackGapSize = thumbTrackGapSize,
+ drawTick = { _, _ -> },
+ modifier = Modifier.width(trackSize).layoutId(Contents.Track),
+ )
+
+ TrackIcon(
+ icon = activeTrackStartIcon,
+ contentsId = Contents.Active.TrackStartIcon,
+ isEnabled = isEnabled,
+ colors = colors,
+ state = measurePolicy,
+ )
+ TrackIcon(
+ icon = activeTrackEndIcon,
+ contentsId = Contents.Active.TrackEndIcon,
+ isEnabled = isEnabled,
+ colors = colors,
+ state = measurePolicy,
+ )
+ TrackIcon(
+ icon = inactiveTrackStartIcon,
+ contentsId = Contents.Inactive.TrackStartIcon,
+ isEnabled = isEnabled,
+ colors = colors,
+ state = measurePolicy,
+ )
+ TrackIcon(
+ icon = inactiveTrackEndIcon,
+ contentsId = Contents.Inactive.TrackEndIcon,
+ isEnabled = isEnabled,
+ colors = colors,
+ state = measurePolicy,
+ )
+ },
+ modifier = modifier,
+ )
+}
+
+@Composable
+private fun TrackIcon(
+ icon: (@Composable BoxScope.(sliderIconsState: SliderIconsState) -> Unit)?,
+ isEnabled: Boolean,
+ contentsId: Contents,
+ state: SliderIconsState,
+ colors: SliderColors,
+ modifier: Modifier = Modifier,
+) {
+ icon ?: return
+ Box(modifier = modifier.layoutId(contentsId).fillMaxSize()) {
+ CompositionLocalProvider(
+ LocalContentColor provides contentsId.getColor(colors, isEnabled)
+ ) {
+ icon(state)
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+private class TrackMeasurePolicy(private val sliderState: SliderState) :
+ MeasurePolicy, SliderIconsState {
+
+ private val isVisible: Map<Contents, MutableState<Boolean>> =
+ mutableMapOf(
+ Contents.Active.TrackStartIcon to mutableStateOf(false),
+ Contents.Active.TrackEndIcon to mutableStateOf(false),
+ Contents.Inactive.TrackStartIcon to mutableStateOf(false),
+ Contents.Inactive.TrackEndIcon to mutableStateOf(false),
+ )
+
+ override val isActiveTrackStartIconVisible: Boolean
+ get() = isVisible.getValue(Contents.Active.TrackStartIcon).value
+
+ override val isActiveTrackEndIconVisible: Boolean
+ get() = isVisible.getValue(Contents.Active.TrackEndIcon).value
+
+ override val isInactiveTrackStartIconVisible: Boolean
+ get() = isVisible.getValue(Contents.Inactive.TrackStartIcon).value
+
+ override val isInactiveTrackEndIconVisible: Boolean
+ get() = isVisible.getValue(Contents.Inactive.TrackEndIcon).value
+
+ override fun MeasureScope.measure(
+ measurables: List<Measurable>,
+ constraints: Constraints,
+ ): MeasureResult {
+ val track = measurables.fastFirst { it.layoutId == Contents.Track }.measure(constraints)
+
+ val iconSize = min(track.width, track.height)
+ val iconConstraints = constraints.copy(maxWidth = iconSize, maxHeight = iconSize)
+
+ val icons =
+ measurables
+ .fastFilter { it.layoutId != Contents.Track }
+ .associateBy(
+ keySelector = { it.layoutId as Contents },
+ valueTransform = { it.measure(iconConstraints) },
+ )
+
+ return layout(track.width, track.height) {
+ with(Contents.Track) {
+ performPlacing(
+ placeable = track,
+ width = track.width,
+ height = track.height,
+ sliderState = sliderState,
+ )
+ }
+
+ for (iconLayoutId in icons.keys) {
+ with(iconLayoutId) {
+ performPlacing(
+ placeable = icons.getValue(iconLayoutId),
+ width = track.width,
+ height = track.height,
+ sliderState = sliderState,
+ )
+
+ isVisible.getValue(iconLayoutId).value =
+ isVisible(
+ placeable = icons.getValue(iconLayoutId),
+ width = track.width,
+ height = track.height,
+ sliderState = sliderState,
+ )
+ }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+private sealed interface Contents {
+
+ data object Track : Contents {
+ override fun Placeable.PlacementScope.performPlacing(
+ placeable: Placeable,
+ width: Int,
+ height: Int,
+ sliderState: SliderState,
+ ) = placeable.place(x = 0, y = 0)
+
+ override fun isVisible(
+ placeable: Placeable,
+ width: Int,
+ height: Int,
+ sliderState: SliderState,
+ ) = true
+
+ override fun getColor(sliderColors: SliderColors, isEnabled: Boolean): Color =
+ error("Unsupported")
+ }
+
+ interface Active : Contents {
+ override fun getColor(sliderColors: SliderColors, isEnabled: Boolean): Color {
+ return if (isEnabled) {
+ sliderColors.activeTickColor
+ } else {
+ sliderColors.disabledActiveTickColor
+ }
+ }
+
+ data object TrackStartIcon : Active {
+ override fun Placeable.PlacementScope.performPlacing(
+ placeable: Placeable,
+ width: Int,
+ height: Int,
+ sliderState: SliderState,
+ ) =
+ placeable.place(
+ x = 0,
+ y = (height * (1 - sliderState.coercedValueAsFraction)).toInt(),
+ )
+
+ override fun isVisible(
+ placeable: Placeable,
+ width: Int,
+ height: Int,
+ sliderState: SliderState,
+ ): Boolean = (height * (sliderState.coercedValueAsFraction)).toInt() > placeable.height
+ }
+
+ data object TrackEndIcon : Active {
+ override fun Placeable.PlacementScope.performPlacing(
+ placeable: Placeable,
+ width: Int,
+ height: Int,
+ sliderState: SliderState,
+ ) = placeable.place(x = 0, y = (height - placeable.height))
+
+ override fun isVisible(
+ placeable: Placeable,
+ width: Int,
+ height: Int,
+ sliderState: SliderState,
+ ): Boolean = (height * (sliderState.coercedValueAsFraction)).toInt() > placeable.height
+ }
+ }
+
+ interface Inactive : Contents {
+
+ override fun getColor(sliderColors: SliderColors, isEnabled: Boolean): Color {
+ return if (isEnabled) {
+ sliderColors.inactiveTickColor
+ } else {
+ sliderColors.disabledInactiveTickColor
+ }
+ }
+
+ data object TrackStartIcon : Inactive {
+ override fun Placeable.PlacementScope.performPlacing(
+ placeable: Placeable,
+ width: Int,
+ height: Int,
+ sliderState: SliderState,
+ ) {
+ placeable.place(x = 0, y = 0)
+ }
+
+ override fun isVisible(
+ placeable: Placeable,
+ width: Int,
+ height: Int,
+ sliderState: SliderState,
+ ): Boolean =
+ (height * (1 - sliderState.coercedValueAsFraction)).toInt() > placeable.height
+ }
+
+ data object TrackEndIcon : Inactive {
+ override fun Placeable.PlacementScope.performPlacing(
+ placeable: Placeable,
+ width: Int,
+ height: Int,
+ sliderState: SliderState,
+ ) {
+ placeable.place(
+ x = 0,
+ y =
+ (height * (1 - sliderState.coercedValueAsFraction)).toInt() -
+ placeable.height,
+ )
+ }
+
+ override fun isVisible(
+ placeable: Placeable,
+ width: Int,
+ height: Int,
+ sliderState: SliderState,
+ ): Boolean =
+ (height * (1 - sliderState.coercedValueAsFraction)).toInt() > placeable.height
+ }
+ }
+
+ fun Placeable.PlacementScope.performPlacing(
+ placeable: Placeable,
+ width: Int,
+ height: Int,
+ sliderState: SliderState,
+ )
+
+ fun isVisible(placeable: Placeable, width: Int, height: Int, sliderState: SliderState): Boolean
+
+ fun getColor(sliderColors: SliderColors, isEnabled: Boolean): Color
+}
+
+/** Provides visibility state for each of the Slider's icons. */
+interface SliderIconsState {
+ val isActiveTrackStartIconVisible: Boolean
+ val isActiveTrackEndIconVisible: Boolean
+ val isInactiveTrackStartIconVisible: Boolean
+ val isInactiveTrackEndIconVisible: Boolean
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogOverscrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogOverscrollViewModel.kt
index 0d41860d9f57..0fdf5d6266d0 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogOverscrollViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogOverscrollViewModel.kt
@@ -95,18 +95,17 @@ constructor(
private fun overscrollEvents(direction: Float): Flow<OverscrollEventModel> {
var startPosition: Float? = null
return inputEventsInteractor.event
- .mapNotNull { (it as? SliderInputEvent.Touch)?.event }
+ .mapNotNull { it as? SliderInputEvent.Touch }
.transform { touchEvent ->
// Skip events from inside the slider bounds for the case when the user adjusts
- // slider
- // towards max when the slider is already on max value.
- if (touchEvent.isFinalEvent()) {
+ // slider towards max when the slider is already on max value.
+ if (touchEvent is SliderInputEvent.Touch.End) {
startPosition = null
emit(OverscrollEventModel.Animate(0f))
return@transform
}
val currentStartPosition = startPosition
- val newPosition: Float = touchEvent.rawY
+ val newPosition: Float = touchEvent.y
if (currentStartPosition == null) {
startPosition = newPosition
} else {
@@ -126,11 +125,6 @@ constructor(
}
}
- /** @return true when the [MotionEvent] indicates the end of the gesture. */
- private fun MotionEvent.isFinalEvent(): Boolean {
- return actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL
- }
-
/** Models overscroll event */
sealed interface OverscrollEventModel {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModel.kt
deleted file mode 100644
index 755776ac9723..000000000000
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModel.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.volume.dialog.sliders.ui.viewmodel
-
-import android.view.MotionEvent
-import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
-import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope
-import com.android.systemui.volume.dialog.sliders.domain.interactor.VolumeDialogSliderInputEventsInteractor
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.stateIn
-
-@VolumeDialogSliderScope
-class VolumeDialogSliderInputEventsViewModel
-@Inject
-constructor(
- @VolumeDialog private val coroutineScope: CoroutineScope,
- private val interactor: VolumeDialogSliderInputEventsInteractor,
-) {
-
- val event =
- interactor.event.stateIn(coroutineScope, SharingStarted.Eagerly, null).filterNotNull()
-
- fun onTouchEvent(event: MotionEvent) {
- interactor.onTouchEvent(event)
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt
index 8df9e788905c..b01046b377b0 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt
@@ -20,17 +20,20 @@ import android.graphics.drawable.Drawable
import com.android.systemui.volume.dialog.shared.model.VolumeDialogStreamModel
data class VolumeDialogSliderStateModel(
- val minValue: Float,
- val maxValue: Float,
val value: Float,
+ val isDisabled: Boolean,
+ val valueRange: ClosedFloatingPointRange<Float>,
val icon: Drawable,
)
-fun VolumeDialogStreamModel.toStateModel(icon: Drawable): VolumeDialogSliderStateModel {
+fun VolumeDialogStreamModel.toStateModel(
+ isDisabled: Boolean,
+ icon: Drawable,
+): VolumeDialogSliderStateModel {
return VolumeDialogSliderStateModel(
- minValue = levelMin.toFloat(),
value = level.toFloat(),
- maxValue = levelMax.toFloat(),
+ isDisabled = isDisabled,
+ valueRange = levelMin.toFloat()..levelMax.toFloat(),
icon = icon,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt
index a752f1f78e74..e89d5ab53560 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt
@@ -16,6 +16,9 @@
package com.android.systemui.volume.dialog.sliders.ui.viewmodel
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.pointer.PointerEvent
+import androidx.compose.ui.input.pointer.PointerEventType
import com.android.systemui.util.time.SystemClock
import com.android.systemui.volume.Events
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
@@ -23,20 +26,23 @@ import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogVisibili
import com.android.systemui.volume.dialog.shared.VolumeDialogLogger
import com.android.systemui.volume.dialog.shared.model.VolumeDialogStreamModel
import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope
+import com.android.systemui.volume.dialog.sliders.domain.interactor.VolumeDialogSliderInputEventsInteractor
import com.android.systemui.volume.dialog.sliders.domain.interactor.VolumeDialogSliderInteractor
import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType
+import com.android.systemui.volume.dialog.sliders.shared.model.SliderInputEvent
import javax.inject.Inject
+import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
@@ -62,6 +68,7 @@ constructor(
private val visibilityInteractor: VolumeDialogVisibilityInteractor,
@VolumeDialog private val coroutineScope: CoroutineScope,
private val volumeDialogSliderIconProvider: VolumeDialogSliderIconProvider,
+ private val inputEventsInteractor: VolumeDialogSliderInputEventsInteractor,
private val systemClock: SystemClock,
private val logger: VolumeDialogLogger,
) {
@@ -77,11 +84,12 @@ constructor(
.stateIn(coroutineScope, SharingStarted.Eagerly, null)
.filterNotNull()
- val isDisabledByZenMode: Flow<Boolean> = interactor.isDisabledByZenMode
val state: Flow<VolumeDialogSliderStateModel> =
- model
- .flatMapLatest { streamModel ->
- with(streamModel) {
+ combine(
+ interactor.isDisabledByZenMode,
+ model,
+ model.flatMapLatest { streamModel ->
+ with(streamModel) {
val isMuted = muteSupported && muted
when (sliderType) {
is VolumeDialogSliderType.Stream ->
@@ -101,7 +109,9 @@ constructor(
}
}
}
- .map { icon -> streamModel.toStateModel(icon) }
+ },
+ ) { isDisabledByZenMode, model, icon ->
+ model.toStateModel(icon = icon, isDisabled = isDisabledByZenMode)
}
.stateIn(coroutineScope, SharingStarted.Eagerly, null)
.filterNotNull()
@@ -116,11 +126,14 @@ constructor(
.launchIn(coroutineScope)
}
- fun setStreamVolume(volume: Int, fromUser: Boolean) {
+ fun setStreamVolume(volume: Float, fromUser: Boolean) {
if (fromUser) {
visibilityInteractor.resetDismissTimeout()
userVolumeUpdates.value =
- VolumeUpdate(newVolumeLevel = volume, timestampMillis = getTimestampMillis())
+ VolumeUpdate(
+ newVolumeLevel = volume.roundToInt(),
+ timestampMillis = getTimestampMillis(),
+ )
}
}
@@ -128,6 +141,28 @@ constructor(
logger.onVolumeSliderAdjustmentFinished(volume = volume, stream = sliderType.audioStream)
}
+ fun onTouchEvent(pointerEvent: PointerEvent) {
+ val position: Offset = pointerEvent.changes.first().position
+ when (pointerEvent.type) {
+ PointerEventType.Press ->
+ inputEventsInteractor.onTouchEvent(
+ SliderInputEvent.Touch.Start(position.x, position.y)
+ )
+ PointerEventType.Move ->
+ inputEventsInteractor.onTouchEvent(
+ SliderInputEvent.Touch.Move(position.x, position.y)
+ )
+ PointerEventType.Scroll ->
+ inputEventsInteractor.onTouchEvent(
+ SliderInputEvent.Touch.Move(position.x, position.y)
+ )
+ PointerEventType.Release ->
+ inputEventsInteractor.onTouchEvent(
+ SliderInputEvent.Touch.End(position.x, position.y)
+ )
+ }
+ }
+
private fun getTimestampMillis(): Long = systemClock.uptimeMillis()
private data class VolumeUpdate(val newVolumeLevel: Int, val timestampMillis: Long)
diff --git a/packages/SystemUI/src/com/android/systemui/volume/haptics/ui/VolumeHapticsConfigsProvider.kt b/packages/SystemUI/src/com/android/systemui/volume/haptics/ui/VolumeHapticsConfigsProvider.kt
new file mode 100644
index 000000000000..92e9bf2d1ffc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/haptics/ui/VolumeHapticsConfigsProvider.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.haptics.ui
+
+import com.android.systemui.haptics.slider.SeekableSliderTrackerConfig
+import com.android.systemui.haptics.slider.SliderHapticFeedbackConfig
+
+object VolumeHapticsConfigsProvider {
+
+ fun sliderHapticFeedbackConfig(
+ valueRange: ClosedFloatingPointRange<Float>
+ ): SliderHapticFeedbackConfig {
+ val sliderStepSize = 1f / (valueRange.endInclusive - valueRange.start)
+ return SliderHapticFeedbackConfig(
+ lowerBookendScale = 0.2f,
+ progressBasedDragMinScale = 0.2f,
+ progressBasedDragMaxScale = 0.5f,
+ deltaProgressForDragThreshold = 0f,
+ additionalVelocityMaxBump = 0.2f,
+ maxVelocityToScale = 0.1f, /* slider progress(from 0 to 1) per sec */
+ sliderStepSize = sliderStepSize,
+ )
+ }
+
+ val seekableSliderTrackerConfig =
+ SeekableSliderTrackerConfig(lowerBookendThreshold = 0f, upperBookendThreshold = 1f)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/GradientColorWallpaper.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/GradientColorWallpaper.kt
index c1fb0e80cafc..760e94c72f19 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/GradientColorWallpaper.kt
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/GradientColorWallpaper.kt
@@ -16,6 +16,7 @@
package com.android.systemui.wallpapers
+import android.app.Flags
import android.graphics.Canvas
import android.graphics.Paint
import android.service.wallpaper.WallpaperService
@@ -26,7 +27,15 @@ import androidx.core.graphics.toRectF
/** A wallpaper that shows a static gradient color image wallpaper. */
class GradientColorWallpaper : WallpaperService() {
- override fun onCreateEngine(): Engine = GradientColorWallpaperEngine()
+ override fun onCreateEngine(): Engine =
+ if (Flags.enableConnectedDisplaysWallpaper()) {
+ GradientColorWallpaperEngine()
+ } else {
+ EmptyWallpaperEngine()
+ }
+
+ /** Empty engine used when the feature flag is disabled. */
+ inner class EmptyWallpaperEngine : Engine()
inner class GradientColorWallpaperEngine : Engine() {
init {
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt
index 8487ee751948..ec74f4f47bc9 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt
@@ -36,4 +36,5 @@ class NoopWallpaperRepository @Inject constructor() : WallpaperRepository {
override val wallpaperInfo: StateFlow<WallpaperInfo?> = MutableStateFlow(null).asStateFlow()
override val wallpaperSupportsAmbientMode = flowOf(false)
override var rootView: View? = null
+ override val shouldSendFocalArea: StateFlow<Boolean> = MutableStateFlow(false).asStateFlow()
}
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt
index ed43f8323c31..9794c619041e 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt
@@ -31,7 +31,6 @@ import com.android.systemui.Flags
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.keyguard.data.repository.KeyguardClockRepository
import com.android.systemui.keyguard.data.repository.KeyguardRepository
import com.android.systemui.shared.Flags.ambientAod
import com.android.systemui.user.data.model.SelectedUserModel
@@ -45,6 +44,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
@@ -65,6 +65,9 @@ interface WallpaperRepository {
/** Set rootView to get its windowToken afterwards */
var rootView: View?
+
+ /** when we use magic portrait wallpapers, we should always get its bounds from keyguard */
+ val shouldSendFocalArea: StateFlow<Boolean>
}
@SysUISingleton
@@ -76,7 +79,6 @@ constructor(
broadcastDispatcher: BroadcastDispatcher,
userRepository: UserRepository,
keyguardRepository: KeyguardRepository,
- keyguardClockRepository: KeyguardClockRepository,
private val wallpaperManager: WallpaperManager,
context: Context,
) : WallpaperRepository {
@@ -97,27 +99,7 @@ constructor(
// Only update the wallpaper status once the user selection has finished.
.filter { it.selectionStatus == SelectionStatus.SELECTION_COMPLETE }
- /** The bottom of notification stack respect to the top of screen. */
- private val notificationStackAbsoluteBottom: StateFlow<Float> =
- keyguardRepository.notificationStackAbsoluteBottom
-
- /** The top of shortcut respect to the top of screen. */
- private val shortcutAbsoluteTop: StateFlow<Float> = keyguardRepository.shortcutAbsoluteTop
-
- /**
- * The top of notification stack to give a default state of lockscreen remaining space for
- * states with notifications to compare with. It's the bottom of smartspace date and weather
- * smartspace in small clock state, plus proper bottom margin.
- */
- private val notificationStackDefaultTop = keyguardClockRepository.notificationDefaultTop
@VisibleForTesting var sendLockscreenLayoutJob: Job? = null
- private val lockscreenRemainingSpaceWithNotification: Flow<Triple<Float, Float, Float>> =
- combine(
- notificationStackAbsoluteBottom,
- notificationStackDefaultTop,
- shortcutAbsoluteTop,
- ::Triple,
- )
override val wallpaperInfo: StateFlow<WallpaperInfo?> =
if (!wallpaperManager.isWallpaperSupported) {
@@ -140,15 +122,16 @@ constructor(
override var rootView: View? = null
- val shouldSendNotificationLayout =
+ override val shouldSendFocalArea =
wallpaperInfo
.map {
- val shouldSendNotificationLayout = shouldSendNotificationLayout(it)
+ val shouldSendNotificationLayout =
+ it?.component?.className == MAGIC_PORTRAIT_CLASSNAME
if (shouldSendNotificationLayout) {
sendLockscreenLayoutJob =
scope.launch {
- lockscreenRemainingSpaceWithNotification.collect {
- (notificationBottom, notificationDefaultTop, shortcutTop) ->
+ keyguardRepository.wallpaperFocalAreaBounds.collect {
+ wallpaperFocalAreaBounds ->
wallpaperManager.sendWallpaperCommand(
/* windowToken = */ rootView?.windowToken,
/* action = */ WallpaperManager
@@ -157,14 +140,22 @@ constructor(
/* y = */ 0,
/* z = */ 0,
/* extras = */ Bundle().apply {
- putFloat("screenLeft", 0F)
- putFloat("smartspaceBottom", notificationDefaultTop)
- putFloat("notificationBottom", notificationBottom)
putFloat(
- "screenRight",
- context.resources.displayMetrics.widthPixels.toFloat(),
+ "wallpaperFocalAreaLeft",
+ wallpaperFocalAreaBounds.left,
+ )
+ putFloat(
+ "wallpaperFocalAreaRight",
+ wallpaperFocalAreaBounds.right,
+ )
+ putFloat(
+ "wallpaperFocalAreaTop",
+ wallpaperFocalAreaBounds.top,
+ )
+ putFloat(
+ "wallpaperFocalAreaBottom",
+ wallpaperFocalAreaBounds.bottom,
)
- putFloat("shortCutTop", shortcutTop)
},
)
}
@@ -176,10 +167,9 @@ constructor(
}
.stateIn(
scope,
- // Always be listening for wallpaper changes.
- if (Flags.magicPortraitWallpapers()) SharingStarted.Eagerly
- else SharingStarted.Lazily,
- initialValue = false,
+ // Always be listening for wallpaper changes when magic portrait flag is on
+ if (Flags.magicPortraitWallpapers()) SharingStarted.Eagerly else WhileSubscribed(),
+ initialValue = Flags.magicPortraitWallpapers(),
)
private suspend fun getWallpaper(selectedUser: SelectedUserModel): WallpaperInfo? {
@@ -188,14 +178,6 @@ constructor(
}
}
- private fun shouldSendNotificationLayout(wallpaperInfo: WallpaperInfo?): Boolean {
- return if (wallpaperInfo != null && wallpaperInfo.component != null) {
- wallpaperInfo.component!!.className == MAGIC_PORTRAIT_CLASSNAME
- } else {
- false
- }
- }
-
companion object {
const val MAGIC_PORTRAIT_CLASSNAME =
"com.google.android.apps.magicportrait.service.MagicPortraitWallpaperService"
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index 2645811fa4ad..312d2ffd74e4 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -38,6 +38,7 @@ import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_STATE_CANCELL
import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_STATE_STOPPED;
import static com.android.keyguard.KeyguardUpdateMonitor.DEFAULT_CANCEL_SIGNAL_TIMEOUT;
import static com.android.keyguard.KeyguardUpdateMonitor.HAL_POWER_PRESS_TIMEOUT;
+import static com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2;
import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED;
import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN;
@@ -139,6 +140,7 @@ import com.android.systemui.biometrics.AuthController;
import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider;
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor;
import com.android.systemui.deviceentry.data.repository.FaceWakeUpTriggersConfig;
import com.android.systemui.deviceentry.data.repository.FaceWakeUpTriggersConfigImpl;
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor;
@@ -180,6 +182,9 @@ import org.mockito.MockitoAnnotations;
import org.mockito.MockitoSession;
import org.mockito.quality.Strictness;
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -189,9 +194,6 @@ import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
-import platform.test.runner.parameterized.Parameters;
-
@SmallTest
@RunWith(ParameterizedAndroidJunit4.class)
@TestableLooper.RunWithLooper
@@ -304,6 +306,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase {
private JavaAdapter mJavaAdapter;
@Mock
private SceneInteractor mSceneInteractor;
+ @Mock
+ private CommunalSceneInteractor mCommunalSceneInteractor;
@Captor
private ArgumentCaptor<FaceAuthenticationListener> mFaceAuthenticationListener;
@@ -1084,6 +1088,49 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase {
}
@Test
+ @EnableFlags(FLAG_GLANCEABLE_HUB_V2)
+ public void udfpsStopsListeningWhenCommunalShowing() {
+ // GIVEN keyguard showing
+ mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
+ mKeyguardUpdateMonitor.setKeyguardShowing(true, false);
+
+ assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(true)).isTrue();
+
+ // WHEN communal is shown
+ mKeyguardUpdateMonitor.onCommunalShowingChanged(true);
+
+ // THEN shouldn't listen for fingerprint
+ assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(true)).isFalse();
+
+ // WHEN alternate bouncer shows on top of communal, we should listen for fingerprint
+ mKeyguardUpdateMonitor.setAlternateBouncerVisibility(true);
+ assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(true)).isTrue();
+
+ // WHEN communal is hidden
+ mKeyguardUpdateMonitor.onCommunalShowingChanged(false);
+ mKeyguardUpdateMonitor.setAlternateBouncerVisibility(false);
+
+ // THEN listen for fingerprint
+ assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(true)).isTrue();
+ }
+
+ @Test
+ @EnableFlags(FLAG_GLANCEABLE_HUB_V2)
+ public void sfpsNotAffectedByCommunalShowing() {
+ // GIVEN keyguard showing
+ mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
+ mKeyguardUpdateMonitor.setKeyguardShowing(true, false);
+
+ assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue();
+
+ // WHEN communal is shown
+ mKeyguardUpdateMonitor.onCommunalShowingChanged(true);
+
+ // THEN we should still listen for fingerprint if not UDFPS
+ assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue();
+ }
+
+ @Test
public void testFingerprintPowerPressed_restartsFingerprintListeningStateWithDelay() {
mKeyguardUpdateMonitor.mFingerprintAuthenticationCallback
.onAuthenticationError(FingerprintManager.BIOMETRIC_ERROR_POWER_PRESSED, "");
@@ -2669,7 +2716,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase {
mTaskStackChangeListeners, mSelectedUserInteractor, mActivityTaskManager,
() -> mAlternateBouncerInteractor,
() -> mJavaAdapter,
- () -> mSceneInteractor);
+ () -> mSceneInteractor,
+ mCommunalSceneInteractor);
setAlternateBouncerVisibility(false);
setPrimaryBouncerVisibility(false);
setStrongAuthTracker(KeyguardUpdateMonitorTest.this.mStrongAuthTracker);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
index 21519b0cb38a..ab691c630f97 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
@@ -304,9 +304,7 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() {
testScope = TestScope(testDispatcher)
underTest =
KeyguardQuickAffordanceInteractor(
- keyguardInteractor =
- KeyguardInteractorFactory.create(featureFlags = featureFlags)
- .keyguardInteractor,
+ keyguardInteractor = kosmos.keyguardInteractor,
shadeInteractor = kosmos.shadeInteractor,
lockPatternUtils = lockPatternUtils,
keyguardStateController = keyguardStateController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
index b5a227104900..051aba3d593f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
@@ -44,9 +44,10 @@ import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanc
import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
@@ -191,9 +192,8 @@ class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() {
dockManager = DockManagerFake()
biometricSettingsRepository = FakeBiometricSettingsRepository()
- val withDeps = KeyguardInteractorFactory.create()
- keyguardInteractor = withDeps.keyguardInteractor
- repository = withDeps.repository
+ keyguardInteractor = kosmos.keyguardInteractor
+ repository = kosmos.fakeKeyguardRepository
whenever(userTracker.userHandle).thenReturn(mock())
whenever(lockPatternUtils.getStrongAuthForUser(ArgumentMatchers.anyInt()))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index e68153ad2606..70450d29c74e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -57,7 +57,10 @@ import com.android.systemui.res.R
import com.android.systemui.settings.brightness.data.repository.BrightnessMirrorShowingRepository
import com.android.systemui.settings.brightness.domain.interactor.BrightnessMirrorShowingInteractor
import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler
+import com.android.systemui.shade.data.repository.ShadeAnimationRepository
+import com.android.systemui.shade.data.repository.ShadeRepositoryImpl
import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor
+import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractorLegacyImpl
import com.android.systemui.statusbar.BlurUtils
import com.android.systemui.statusbar.DragDownHelper
import com.android.systemui.statusbar.LockscreenShadeTransitionController
@@ -219,6 +222,10 @@ class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization) :
notificationShadeDepthController,
view,
shadeViewController,
+ ShadeAnimationInteractorLegacyImpl(
+ ShadeAnimationRepository(),
+ ShadeRepositoryImpl(testScope),
+ ),
panelExpansionInteractor,
ShadeExpansionStateManager(),
stackScrollLayoutController,
@@ -521,6 +528,18 @@ class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization) :
verify(view).findViewById<ViewGroup>(R.id.keyguard_message_area)
}
+ @EnableFlags(Flags.FLAG_SHADE_LAUNCH_ACCESSIBILITY)
+ @Test
+ fun notifiesTheViewWhenLaunchAnimationIsRunning() {
+ testScope.runTest {
+ underTest.setExpandAnimationRunning(true)
+ verify(view).setAnimatingContentLaunch(true)
+
+ underTest.setExpandAnimationRunning(false)
+ verify(view).setAnimatingContentLaunch(false)
+ }
+ }
+
@Test
@DisableSceneContainer
fun setsUpCommunalHubLayout_whenFlagEnabled() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt
index eae828562223..a5cd81ff3116 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt
@@ -53,6 +53,7 @@ import com.android.systemui.shade.ShadeHeaderController.Companion.QQS_HEADER_CON
import com.android.systemui.shade.ShadeHeaderController.Companion.QS_HEADER_CONSTRAINT
import com.android.systemui.shade.carrier.ShadeCarrierGroup
import com.android.systemui.shade.carrier.ShadeCarrierGroupController
+import com.android.systemui.shade.data.repository.shadeDisplaysRepository
import com.android.systemui.statusbar.data.repository.fakeStatusBarContentInsetsProviderStore
import com.android.systemui.statusbar.phone.StatusIconContainer
import com.android.systemui.statusbar.phone.StatusOverlayHoverListenerFactory
@@ -96,7 +97,7 @@ class ShadeHeaderControllerTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val insetsProviderStore = kosmos.fakeStatusBarContentInsetsProviderStore
- private val insetsProvider = insetsProviderStore.defaultDisplay
+ private val insetsProvider = insetsProviderStore.forDisplay(context.displayId)
@Mock(answer = Answers.RETURNS_MOCKS) private lateinit var view: MotionLayout
@Mock private lateinit var statusIcons: StatusIconContainer
@@ -196,6 +197,7 @@ class ShadeHeaderControllerTest : SysuiTestCase() {
privacyIconsController,
insetsProviderStore,
configurationController,
+ kosmos.shadeDisplaysRepository,
variableDateViewControllerFactory,
batteryMeterViewController,
dumpManager,
@@ -809,6 +811,43 @@ class ShadeHeaderControllerTest : SysuiTestCase() {
}
@Test
+ fun sameInsetsTwice_listenerCallsOnApplyWindowInsetsOnlyOnce() {
+ val windowInsets = createWindowInsets()
+
+ val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
+ verify(view).setOnApplyWindowInsetsListener(capture(captor))
+
+ val listener = captor.value
+
+ listener.onApplyWindowInsets(view, windowInsets)
+
+ verify(view, times(1)).onApplyWindowInsets(any())
+
+ listener.onApplyWindowInsets(view, windowInsets)
+
+ verify(view, times(1)).onApplyWindowInsets(any())
+ }
+
+ @Test
+ fun twoDifferentInsets_listenerCallsOnApplyWindowInsetsTwice() {
+ val windowInsets1 = WindowInsets(Rect(1, 2, 3, 4))
+ val windowInsets2 = WindowInsets(Rect(5, 6, 7, 8))
+
+ val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java)
+ verify(view).setOnApplyWindowInsetsListener(capture(captor))
+
+ val listener = captor.value
+
+ listener.onApplyWindowInsets(view, windowInsets1)
+
+ verify(view, times(1)).onApplyWindowInsets(any())
+
+ listener.onApplyWindowInsets(view, windowInsets2)
+
+ verify(view, times(2)).onApplyWindowInsets(any())
+ }
+
+ @Test
fun alarmIconNotIgnored() {
verify(statusIcons, Mockito.never())
.addIgnoredSlot(context.getString(com.android.internal.R.string.status_bar_alarm_clock))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
index 5aee92939ed5..493468e8f675 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
@@ -69,6 +69,7 @@ import com.android.systemui.statusbar.notification.FeedbackIcon;
import com.android.systemui.statusbar.notification.SourceType;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.headsup.PinnedStatus;
+import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded;
import com.android.systemui.statusbar.notification.row.ExpandableView.OnHeightChangedListener;
import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
import com.android.systemui.statusbar.notification.shared.NotificationContentAlphaOptimization;
@@ -878,6 +879,85 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
}
@Test
+ @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME)
+ public void isExpanded_sensitivePromotedNotification_notExpanded() throws Exception {
+ // GIVEN
+ final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+ row.setPromotedOngoing(true);
+ row.setSensitive(/* sensitive= */true, /* hideSensitive= */false);
+ row.setHideSensitiveForIntrinsicHeight(/* hideSensitive= */true);
+
+ // THEN
+ assertThat(row.isExpanded()).isFalse();
+ }
+
+ @Test
+ @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME)
+ public void isExpanded_promotedNotificationNotOnKeyguard_expanded() throws Exception {
+ // GIVEN
+ final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+ row.setPromotedOngoing(true);
+ row.setOnKeyguard(false);
+
+ // THEN
+ assertThat(row.isExpanded()).isTrue();
+ }
+
+ @Test
+ @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME)
+ public void isExpanded_promotedNotificationAllowOnKeyguard_expanded() throws Exception {
+ // GIVEN
+ final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+ row.setPromotedOngoing(true);
+ row.setOnKeyguard(true);
+
+ // THEN
+ assertThat(row.isExpanded(/* allowOnKeyguard = */ true)).isTrue();
+ }
+
+ @Test
+ @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME)
+ public void isExpanded_promotedNotificationIgnoreLockscreenConstraints_expanded()
+ throws Exception {
+ // GIVEN
+ final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+ row.setPromotedOngoing(true);
+ row.setOnKeyguard(true);
+ row.setIgnoreLockscreenConstraints(true);
+
+ // THEN
+ assertThat(row.isExpanded()).isTrue();
+ }
+
+ @Test
+ @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME)
+ public void isExpanded_promotedNotificationSaveSpaceOnLockScreen_notExpanded()
+ throws Exception {
+ // GIVEN
+ final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+ row.setPromotedOngoing(true);
+ row.setOnKeyguard(true);
+ row.setSaveSpaceOnLockscreen(true);
+
+ // THEN
+ assertThat(row.isExpanded()).isFalse();
+ }
+
+ @Test
+ @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME)
+ public void isExpanded_promotedNotificationNotSaveSpaceOnLockScreen_expanded()
+ throws Exception {
+ // GIVEN
+ final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+ row.setPromotedOngoing(true);
+ row.setOnKeyguard(true);
+ row.setSaveSpaceOnLockscreen(false);
+
+ // THEN
+ assertThat(row.isExpanded()).isTrue();
+ }
+
+ @Test
public void onDisappearAnimationFinished_shouldSetFalse_headsUpAnimatingAway()
throws Exception {
final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
@@ -941,6 +1021,57 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
assertThat(row.getImageResolver().getContext()).isSameInstanceAs(userContext);
}
+ @Test
+ @EnableFlags(com.android.systemui.Flags.FLAG_NOTIFICATIONS_PINNED_HUN_IN_SHADE)
+ public void mustStayOnScreen_false() throws Exception {
+ final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+ assertThat(row.mustStayOnScreen()).isFalse();
+ }
+
+ @Test
+ @EnableFlags(com.android.systemui.Flags.FLAG_NOTIFICATIONS_PINNED_HUN_IN_SHADE)
+ public void mustStayOnScreen_isHeadsUp_markedAsSeen() throws Exception {
+ final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+ // When the row is a HUN
+ row.setHeadsUp(true);
+ //Then it must stay on screen
+ assertThat(row.mustStayOnScreen()).isTrue();
+ // And when the user has seen it
+ row.markHeadsUpSeen();
+ // Then it should NOT stay on screen anymore
+ assertThat(row.mustStayOnScreen()).isFalse();
+ }
+
+ @Test
+ @EnableFlags(com.android.systemui.Flags.FLAG_NOTIFICATIONS_PINNED_HUN_IN_SHADE)
+ public void mustStayOnScreen_isPinned_markedAsSeen() throws Exception {
+ final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+ // When a HUN is pinned
+ row.setHeadsUp(true);
+ row.setPinnedStatus(PinnedStatus.PinnedBySystem);
+ //Then it must stay on screen
+ assertThat(row.mustStayOnScreen()).isTrue();
+ // And when the user has seen it
+ row.markHeadsUpSeen();
+ // Then it should still stay on screen
+ assertThat(row.mustStayOnScreen()).isTrue();
+ }
+
+ @Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_NOTIFICATIONS_PINNED_HUN_IN_SHADE)
+ public void mustStayOnScreen_isPinned_markedAsSeen_false() throws Exception {
+ final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+ // When a HUN is pinned
+ row.setHeadsUp(true);
+ row.setPinnedStatus(PinnedStatus.PinnedBySystem);
+ //Then it must stay on screen
+ assertThat(row.mustStayOnScreen()).isTrue();
+ // And when the user has seen it
+ row.markHeadsUpSeen();
+ // Then it should NOT stay on screen anymore
+ assertThat(row.mustStayOnScreen()).isFalse();
+ }
+
private void setDrawableIconsInImageView(CachingIconView icon, Drawable iconDrawable,
Drawable rightIconDrawable) {
ImageView iconView = mock(ImageView.class);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt
index 61943f2283e0..8645a40319f4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt
@@ -444,6 +444,7 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() {
eq(true), /* wasShownHighPriority */
eq(assistantFeedbackController),
any<MetricsLogger>(),
+ any<View.OnClickListener>(),
)
}
@@ -476,6 +477,7 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() {
eq(false), /* wasShownHighPriority */
eq(assistantFeedbackController),
any<MetricsLogger>(),
+ any<View.OnClickListener>(),
)
}
@@ -508,6 +510,7 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() {
eq(false), /* wasShownHighPriority */
eq(assistantFeedbackController),
any<MetricsLogger>(),
+ any<View.OnClickListener>(),
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewBinderTest.kt
index 1eb88c5a5616..0457255fee4e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewBinderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewBinderTest.kt
@@ -19,10 +19,12 @@ import android.app.Notification
import android.app.Person
import android.platform.test.annotations.EnableFlags
import android.testing.TestableLooper
+import android.view.View.GONE
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.R
import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.RankingBuilder
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_SINGLE_LINE
import com.android.systemui.statusbar.notification.row.SingleLineViewInflater.inflatePrivateSingleLineView
@@ -90,6 +92,7 @@ class SingleLineViewBinderTest : SysuiTestCase() {
builder = notificationBuilder,
systemUiContext = context,
redactText = false,
+ summarization = null
)
// WHEN: binds the viewHolder
@@ -151,6 +154,7 @@ class SingleLineViewBinderTest : SysuiTestCase() {
builder = notificationBuilder,
systemUiContext = context,
redactText = false,
+ summarization = null
)
// WHEN: binds the view
SingleLineViewBinder.bind(viewModel, view)
@@ -200,6 +204,7 @@ class SingleLineViewBinderTest : SysuiTestCase() {
builder = notificationBuilder,
systemUiContext = context,
redactText = false,
+ summarization = null
)
// WHEN: binds the view with the view model
SingleLineViewBinder.bind(viewModel, view)
@@ -211,6 +216,70 @@ class SingleLineViewBinderTest : SysuiTestCase() {
assertNull(viewModel.conversationData)
}
+ @Test
+ @EnableFlags(AsyncHybridViewInflation.FLAG_NAME, android.app.Flags.FLAG_NM_SUMMARIZATION_UI,
+ android.app.Flags.FLAG_NM_SUMMARIZATION)
+ fun bindSummarizedGroupConversationSingleLineView() {
+ // GIVEN a row with a group conversation notification
+ val user =
+ Person.Builder()
+ .setName(USER_NAME)
+ .build()
+ val style =
+ Notification.MessagingStyle(user)
+ .addMessage(MESSAGE_TEXT, System.currentTimeMillis(), user)
+ .addMessage(
+ "How about lunch?",
+ System.currentTimeMillis(),
+ Person.Builder().setName("user2").build(),
+ )
+ .setGroupConversation(true)
+ notificationBuilder.setStyle(style).setShortcutId(SHORTCUT_ID)
+ val notification = notificationBuilder.build()
+ val row = helper.createRow(notification)
+ val rb = RankingBuilder(row.entry.ranking)
+ rb.setSummarization("summary!")
+ row.entry.ranking = rb.build()
+
+ val view =
+ inflatePrivateSingleLineView(
+ isConversation = true,
+ reinflateFlags = FLAG_CONTENT_VIEW_SINGLE_LINE,
+ entry = row.entry,
+ context = context,
+ logger = mock(),
+ )
+ as HybridConversationNotificationView
+
+ val publicView =
+ inflatePublicSingleLineView(
+ isConversation = true,
+ reinflateFlags = FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE,
+ entry = row.entry,
+ context = context,
+ logger = mock(),
+ )
+ as HybridConversationNotificationView
+ assertNotNull(publicView)
+
+ val viewModel =
+ SingleLineViewInflater.inflateSingleLineViewModel(
+ notification = notification,
+ messagingStyle = style,
+ builder = notificationBuilder,
+ systemUiContext = context,
+ redactText = false,
+ summarization = "summary"
+ )
+ // WHEN: binds the view
+ SingleLineViewBinder.bind(viewModel, view)
+
+ // THEN: the single-line conversation view should only include summarization content
+ assertEquals(viewModel.conversationData?.summarization, view.textView.text)
+ assertEquals("", view.conversationSenderNameView.text)
+ assertEquals(GONE, view.conversationSenderNameView.visibility)
+ }
+
private companion object {
const val CHANNEL_ID = "CHANNEL_ID"
const val CONTENT_TITLE = "A Cool New Feature"
@@ -218,5 +287,6 @@ class SingleLineViewBinderTest : SysuiTestCase() {
const val USER_NAME = "USER_NAME"
const val MESSAGE_TEXT = "MESSAGE_TEXT"
const val SHORTCUT_ID = "Shortcut"
+ const val SUMMARIZATION = "summarization"
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt
index ef70e277832e..13724a8b44da 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt
@@ -272,6 +272,35 @@ class SingleLineViewInflaterTest : SysuiTestCase() {
}
}
+ @Test
+ fun createViewModelForSummarizedConversationNotification() {
+ // Given: a non-group conversation notification
+ val notificationType = OneToOneConversation()
+ val notification = getNotification(notificationType)
+
+ // When: inflate the SingleLineViewModel
+ val singleLineViewModel = notification.makeSingleLineViewModel(notificationType)
+
+ // Then: the inflated SingleLineViewModel should be as expected
+ // titleText: Notification.ConversationTitle
+ // contentText: the last message text
+ // conversationSenderName: null, because it's not a group conversation
+ // conversationData.avatar: a single icon of the last sender
+ // summarizedText: the summary text from the ranking
+ assertEquals(CONVERSATION_TITLE, singleLineViewModel.titleText)
+ assertEquals(LAST_MESSAGE, singleLineViewModel.contentText)
+ assertNull(
+ singleLineViewModel.conversationData?.conversationSenderName,
+ "Sender name should be null for one-on-one conversation"
+ )
+ assertTrue {
+ singleLineViewModel.conversationData
+ ?.avatar
+ ?.equalsTo(SingleIcon(firstSenderIcon.loadDrawable(context))) == true
+ }
+ assertEquals("summary", singleLineViewModel.conversationData?.summarization)
+ }
+
sealed class NotificationType(val largeIcon: Icon? = null)
class NonMessaging(largeIcon: Icon? = null) : NotificationType(largeIcon)
@@ -380,7 +409,8 @@ class SingleLineViewInflaterTest : SysuiTestCase() {
if (isConversation) messagingStyle else null,
builder,
context,
- false
+ false,
+ "summary"
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
index 437ccb6a9821..68f66611c981 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
@@ -90,6 +90,8 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val statusBarContentInsetsProviderStore = kosmos.fakeStatusBarContentInsetsProviderStore
private val statusBarContentInsetsProvider = statusBarContentInsetsProviderStore.defaultDisplay
+ private val statusBarContentInsetsProviderForSecondaryDisplay =
+ statusBarContentInsetsProviderStore.forDisplay(SECONDARY_DISPLAY_ID)
private val fakeDarkIconDispatcher = kosmos.fakeDarkIconDispatcher
@Mock private lateinit var shadeViewController: ShadeViewController
@@ -144,6 +146,12 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() {
controller = createAndInitController(view)
}
+ `when`(
+ statusBarContentInsetsProviderForSecondaryDisplay
+ .getStatusBarContentInsetsForCurrentRotation()
+ )
+ .thenReturn(Insets.NONE)
+
val contextForSecondaryDisplay =
SysuiTestableContext(
mContext.createDisplayContext(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
index a02d333d1507..a7fe1ba76590 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
@@ -59,6 +59,7 @@ import com.android.internal.colorextraction.ColorExtractor.GradientColors;
import com.android.keyguard.BouncerPanelExpansionCalculator;
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.systemui.DejankUtils;
+import com.android.systemui.Flags;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.ShadeInterpolation;
import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants;
@@ -71,6 +72,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInterac
import com.android.systemui.keyguard.shared.model.KeyguardState;
import com.android.systemui.keyguard.shared.model.TransitionState;
import com.android.systemui.keyguard.shared.model.TransitionStep;
+import com.android.systemui.keyguard.ui.transitions.BlurConfig;
import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel;
import com.android.systemui.kosmos.KosmosJavaAdapter;
@@ -110,6 +112,9 @@ import java.util.Map;
@RunWith(AndroidJUnit4.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@SmallTest
+// TODO(b/381263619) there are more changes and tweaks required to match the new bouncer/shade specs
+// Disabling for now but it will be fixed before the flag is fully ramped up.
+@DisableFlags(Flags.FLAG_BOUNCER_UI_REVAMP)
public class ScrimControllerTest extends SysuiTestCase {
@Rule public Expect mExpect = Expect.create();
@@ -286,7 +291,8 @@ public class ScrimControllerTest extends SysuiTestCase {
mKeyguardTransitionInteractor,
mKeyguardInteractor,
mKosmos.getTestDispatcher(),
- mLinearLargeScreenShadeInterpolator);
+ mLinearLargeScreenShadeInterpolator,
+ new BlurConfig(0.0f, 0.0f));
mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible);
mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront);
mScrimController.setAnimatorListener(mAnimatorListener);
@@ -1251,7 +1257,8 @@ public class ScrimControllerTest extends SysuiTestCase {
mKeyguardTransitionInteractor,
mKeyguardInteractor,
mKosmos.getTestDispatcher(),
- mLinearLargeScreenShadeInterpolator);
+ mLinearLargeScreenShadeInterpolator,
+ new BlurConfig(0.0f, 0.0f));
mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible);
mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront);
mScrimController.setAnimatorListener(mAnimatorListener);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index fab7922f58e7..5d88f72b805b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -98,6 +98,7 @@ import android.view.View;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.filters.SmallTest;
@@ -2894,6 +2895,12 @@ public class BubblesTest extends SysuiTestCase {
@Override
public void animateBubbleBarLocation(BubbleBarLocation location) {
}
+
+ @Override
+ public void onDragItemOverBubbleBarDragZone(@NonNull BubbleBarLocation location) {}
+
+ @Override
+ public void onItemDraggedOutsideBubbleBarDropZone() {}
}
private static class FakeBubbleProperties implements BubbleProperties {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeFocusedDisplayRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeFocusedDisplayRepository.kt
index 83df5d874ad6..ad9370f7ac84 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeFocusedDisplayRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeFocusedDisplayRepository.kt
@@ -33,7 +33,7 @@ class FakeFocusedDisplayRepository @Inject constructor() : FocusedDisplayReposit
override val focusedDisplayId: StateFlow<Int>
get() = flow.asStateFlow()
- suspend fun emit(focusedDisplay: Int) = flow.emit(focusedDisplay)
+ suspend fun setDisplayId(focusedDisplay: Int) = flow.emit(focusedDisplay)
}
@Module
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
index 0192fa47b434..739f6c2af2b4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
@@ -79,7 +79,7 @@ var Kosmos.shortcutHelperInputShortcutsSource: KeyboardShortcutGroupsSource by
var Kosmos.shortcutHelperCurrentAppShortcutsSource: KeyboardShortcutGroupsSource by
Kosmos.Fixture { CurrentAppShortcutsSource(windowManager) }
-val Kosmos.shortcutHelperAccessibilityShortcutsSource: KeyboardShortcutGroupsSource by
+var Kosmos.shortcutHelperAccessibilityShortcutsSource: KeyboardShortcutGroupsSource by
Kosmos.Fixture { AccessibilityShortcutsSource(mainResources) }
val Kosmos.shortcutHelperExclusions by
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfigKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfigKosmos.kt
deleted file mode 100644
index 568324832b33..000000000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfigKosmos.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.keyguard.data.quickaffordance
-
-import android.content.applicationContext
-import com.android.systemui.communal.data.repository.communalSceneRepository
-import com.android.systemui.communal.domain.interactor.communalInteractor
-import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.scene.domain.interactor.sceneInteractor
-
-val Kosmos.glanceableHubQuickAffordanceConfig by
- Kosmos.Fixture {
- GlanceableHubQuickAffordanceConfig(
- context = applicationContext,
- communalInteractor = communalInteractor,
- communalSceneRepository = communalSceneRepository,
- communalSettingsInteractor = communalSettingsInteractor,
- sceneInteractor = sceneInteractor,
- )
- }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index 8489d8380041..8ea80081a871 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -18,6 +18,7 @@
package com.android.systemui.keyguard.data.repository
import android.graphics.Point
+import android.graphics.RectF
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.shared.model.BiometricUnlockMode
import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
@@ -129,6 +130,10 @@ class FakeKeyguardRepository @Inject constructor() : KeyguardRepository {
override val notificationStackAbsoluteBottom: StateFlow<Float>
get() = _notificationStackAbsoluteBottom.asStateFlow()
+ private val _wallpaperFocalAreaBounds = MutableStateFlow(RectF(0f, 0f, 0f, 0f))
+ override val wallpaperFocalAreaBounds: StateFlow<RectF>
+ get() = _wallpaperFocalAreaBounds.asStateFlow()
+
private val _isKeyguardEnabled = MutableStateFlow(true)
override val isKeyguardEnabled: StateFlow<Boolean> = _isKeyguardEnabled.asStateFlow()
@@ -287,6 +292,10 @@ class FakeKeyguardRepository @Inject constructor() : KeyguardRepository {
_notificationStackAbsoluteBottom.value = bottom
}
+ override fun setWallpaperFocalAreaBounds(bounds: RectF) {
+ _wallpaperFocalAreaBounds.value = bounds
+ }
+
override fun setCanIgnoreAuthAndReturnToGone(canWake: Boolean) {
_canIgnoreAuthAndReturnToGone.value = canWake
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/WallpaperFocalAreaInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/WallpaperFocalAreaInteractorKosmos.kt
new file mode 100644
index 000000000000..8fd6f62b315f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/WallpaperFocalAreaInteractorKosmos.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.content.applicationContext
+import com.android.systemui.keyguard.data.repository.keyguardClockRepository
+import com.android.systemui.keyguard.data.repository.keyguardRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.shade.data.repository.shadeRepository
+import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
+import com.android.systemui.wallpapers.data.repository.wallpaperRepository
+
+val Kosmos.wallpaperFocalAreaInteractor by
+ Kosmos.Fixture {
+ WallpaperFocalAreaInteractor(
+ applicationScope = applicationCoroutineScope,
+ context = applicationContext,
+ keyguardRepository = keyguardRepository,
+ shadeRepository = shadeRepository,
+ activeNotificationsInteractor = activeNotificationsInteractor,
+ keyguardClockRepository = keyguardClockRepository,
+ wallpaperRepository = wallpaperRepository,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelFactoryKosmos.kt
index 2de0e8f76a4b..16d3fdc26613 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelFactoryKosmos.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2024 The Android Open Source Project
+ * Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,16 +14,17 @@
* limitations under the License.
*/
-package com.android.systemui.volume.dialog.sliders.ui.viewmodel
+package com.android.systemui.keyguard.ui.viewmodel
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.applicationCoroutineScope
-import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSliderInputEventsInteractor
+import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor
-val Kosmos.volumeDialogSliderInputEventsViewModel by
+val Kosmos.keyguardMediaViewModelFactory by
Kosmos.Fixture {
- VolumeDialogSliderInputEventsViewModel(
- applicationCoroutineScope,
- volumeDialogSliderInputEventsInteractor,
- )
+ object : KeyguardMediaViewModel.Factory {
+ override fun create(): KeyguardMediaViewModel {
+ return KeyguardMediaViewModel(mediaCarouselInteractor, keyguardInteractor)
+ }
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
index 40b8e0e62b03..37df05b68f9e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
@@ -20,6 +20,7 @@ import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.keyguard.domain.interactor.pulseExpansionInteractor
+import com.android.systemui.keyguard.domain.interactor.wallpaperFocalAreaInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.kosmos.applicationCoroutineScope
@@ -87,5 +88,6 @@ val Kosmos.keyguardRootViewModel by Fixture {
screenOffAnimationController = screenOffAnimationController,
aodBurnInViewModel = aodBurnInViewModel,
shadeInteractor = shadeInteractor,
+ wallpaperFocalAreaInteractor = wallpaperFocalAreaInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt
index 9d73ae3f176f..a1e7d5cede13 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt
@@ -40,7 +40,8 @@ class FakeVolumeDialogController(private val audioManager: AudioManager) : Volum
private val callbacks = CopyOnWriteArraySet<VolumeDialogController.Callbacks>()
private var hasVibrator: Boolean = true
- private var state = VolumeDialogController.State()
+ var state = VolumeDialogController.State()
+ private set
override fun setActiveStream(stream: Int) {
updateState {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryKosmos.kt
index aaef27d257c5..d9a348d93533 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryKosmos.kt
@@ -16,12 +16,15 @@
package com.android.systemui.shade.data.repository
+import com.android.systemui.display.data.repository.FakeFocusedDisplayRepository
import com.android.systemui.display.data.repository.displayRepository
import com.android.systemui.keyguard.data.repository.keyguardRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
import com.android.systemui.shade.display.AnyExternalShadeDisplayPolicy
import com.android.systemui.shade.display.DefaultDisplayShadePolicy
+import com.android.systemui.shade.display.FakeShadeDisplayPolicy
+import com.android.systemui.shade.display.FocusShadeDisplayPolicy
import com.android.systemui.shade.display.ShadeDisplayPolicy
import com.android.systemui.shade.display.ShadeExpansionIntent
import com.android.systemui.shade.display.StatusBarTouchShadeDisplayPolicy
@@ -46,8 +49,6 @@ val Kosmos.statusBarTouchShadeDisplayPolicy: StatusBarTouchShadeDisplayPolicy by
StatusBarTouchShadeDisplayPolicy(
displayRepository = displayRepository,
backgroundScope = testScope.backgroundScope,
- keyguardRepository = keyguardRepository,
- shadeOnDefaultDisplayWhenLocked = false,
shadeInteractor = { shadeInteractor },
notificationElement = { notificationElement },
qsShadeElement = { qsElement },
@@ -55,13 +56,15 @@ val Kosmos.statusBarTouchShadeDisplayPolicy: StatusBarTouchShadeDisplayPolicy by
}
val Kosmos.shadeExpansionIntent: ShadeExpansionIntent by
Kosmos.Fixture { statusBarTouchShadeDisplayPolicy }
-val Kosmos.shadeDisplaysRepository: MutableShadeDisplaysRepository by
+val Kosmos.shadeDisplaysRepository: ShadeDisplaysRepository by
Kosmos.Fixture {
ShadeDisplaysRepositoryImpl(
bgScope = testScope.backgroundScope,
globalSettings = fakeGlobalSettings,
policies = shadeDisplayPolicies,
defaultPolicy = defaultShadeDisplayPolicy,
+ shadeOnDefaultDisplayWhenLocked = true,
+ keyguardRepository = keyguardRepository,
)
}
@@ -71,8 +74,16 @@ val Kosmos.shadeDisplayPolicies: Set<ShadeDisplayPolicy> by
defaultShadeDisplayPolicy,
anyExternalShadeDisplayPolicy,
statusBarTouchShadeDisplayPolicy,
+ FakeShadeDisplayPolicy,
)
}
val Kosmos.fakeShadeDisplaysRepository: FakeShadeDisplayRepository by
Kosmos.Fixture { FakeShadeDisplayRepository() }
+val Kosmos.fakeFocusedDisplayRepository: FakeFocusedDisplayRepository by
+ Kosmos.Fixture { FakeFocusedDisplayRepository() }
+
+val Kosmos.focusShadeDisplayPolicy: FocusShadeDisplayPolicy by
+ Kosmos.Fixture {
+ FocusShadeDisplayPolicy(focusedDisplayRepository = fakeFocusedDisplayRepository)
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorKosmos.kt
index 7892e962d63d..1b50094ec0b7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorKosmos.kt
@@ -16,14 +16,57 @@
package com.android.systemui.shade.domain.interactor
+import android.provider.Settings
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.shade.data.repository.fakeShadeRepository
import com.android.systemui.shade.data.repository.shadeRepository
+import com.android.systemui.shared.settings.data.repository.secureSettingsRepository
+import kotlinx.coroutines.launch
val Kosmos.shadeModeInteractor by Fixture {
ShadeModeInteractorImpl(
applicationScope = applicationCoroutineScope,
repository = shadeRepository,
+ secureSettingsRepository = secureSettingsRepository,
)
}
+
+// TODO(b/391578667): Make this user-aware once supported by FakeSecureSettingsRepository.
+/**
+ * Enables the Dual Shade setting, and (optionally) sets the shade layout to be wide (`true`)
+ * or narrow (`false`).
+ *
+ * In a wide layout, notifications and quick settings shades each take up only half the screen
+ * width. In a narrow layout, they each take up the entire screen width.
+ */
+fun Kosmos.enableDualShade(wideLayout: Boolean? = null) {
+ testScope.launch {
+ secureSettingsRepository.setInt(Settings.Secure.DUAL_SHADE, 1)
+
+ if (wideLayout != null) {
+ fakeShadeRepository.setShadeLayoutWide(wideLayout)
+ }
+ }
+}
+
+// TODO(b/391578667): Make this user-aware once supported by FakeSecureSettingsRepository.
+fun Kosmos.disableDualShade() {
+ testScope.launch { secureSettingsRepository.setInt(Settings.Secure.DUAL_SHADE, 0) }
+}
+
+fun Kosmos.enableSingleShade() {
+ testScope.launch {
+ disableDualShade()
+ fakeShadeRepository.setShadeLayoutWide(false)
+ }
+}
+
+fun Kosmos.enableSplitShade() {
+ testScope.launch {
+ disableDualShade()
+ fakeShadeRepository.setShadeLayoutWide(true)
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java
index 6cd6594c3404..c6daed1aa58f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java
@@ -61,6 +61,7 @@ public class RankingBuilder {
private boolean mIsBubble = false;
private int mProposedImportance = IMPORTANCE_UNSPECIFIED;
private boolean mSensitiveContent = false;
+ private String mSummarization = null;
public RankingBuilder() {
}
@@ -92,6 +93,7 @@ public class RankingBuilder {
mIsBubble = ranking.isBubble();
mProposedImportance = ranking.getProposedImportance();
mSensitiveContent = ranking.hasSensitiveContent();
+ mSummarization = ranking.getSummarization();
}
public Ranking build() {
@@ -122,7 +124,8 @@ public class RankingBuilder {
mRankingAdjustment,
mIsBubble,
mProposedImportance,
- mSensitiveContent);
+ mSensitiveContent,
+ mSummarization);
return ranking;
}
@@ -262,6 +265,11 @@ public class RankingBuilder {
return this;
}
+ public RankingBuilder setSummarization(String summary) {
+ mSummarization = summary;
+ return this;
+ }
+
private static <E> ArrayList<E> copyList(List<E> list) {
if (list == null) {
return null;
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt
index 36fa82f82f0d..4ca044d60f3f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt
@@ -32,10 +32,8 @@ import com.android.systemui.volume.dialog.data.repository.volumeDialogVisibility
import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType
import com.android.systemui.volume.dialog.sliders.domain.model.volumeDialogSliderType
import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogOverscrollViewBinder
-import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderHapticsViewBinder
import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderViewBinder
import com.android.systemui.volume.dialog.sliders.ui.volumeDialogOverscrollViewBinder
-import com.android.systemui.volume.dialog.sliders.ui.volumeDialogSliderHapticsViewBinder
import com.android.systemui.volume.dialog.sliders.ui.volumeDialogSliderViewBinder
import com.android.systemui.volume.mediaControllerRepository
import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.mediaControllerInteractor
@@ -65,9 +63,6 @@ fun Kosmos.volumeDialogSliderComponent(type: VolumeDialogSliderType): VolumeDial
override fun sliderViewBinder(): VolumeDialogSliderViewBinder =
localKosmos.volumeDialogSliderViewBinder
- override fun sliderHapticsViewBinder(): VolumeDialogSliderHapticsViewBinder =
- localKosmos.volumeDialogSliderHapticsViewBinder
-
override fun overscrollViewBinder(): VolumeDialogOverscrollViewBinder =
localKosmos.volumeDialogOverscrollViewBinder
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt
deleted file mode 100644
index d6845b1ff7e3..000000000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.volume.dialog.sliders.ui
-
-import com.android.systemui.haptics.msdl.msdlPlayer
-import com.android.systemui.haptics.vibratorHelper
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.util.time.systemClock
-import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSliderInputEventsViewModel
-
-val Kosmos.volumeDialogSliderHapticsViewBinder by
- Kosmos.Fixture {
- VolumeDialogSliderHapticsViewBinder(
- volumeDialogSliderInputEventsViewModel,
- vibratorHelper,
- msdlPlayer,
- systemClock,
- )
- }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt
index c6db717e004f..484a7cc30152 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt
@@ -16,14 +16,16 @@
package com.android.systemui.volume.dialog.sliders.ui
+import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSliderInputEventsViewModel
+import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogOverscrollViewModel
import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSliderViewModel
val Kosmos.volumeDialogSliderViewBinder by
Kosmos.Fixture {
VolumeDialogSliderViewBinder(
volumeDialogSliderViewModel,
- volumeDialogSliderInputEventsViewModel,
+ volumeDialogOverscrollViewModel,
+ sliderHapticsViewModelFactory,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModelKosmos.kt
index b26081c40c38..90bbb28ff519 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModelKosmos.kt
@@ -21,6 +21,7 @@ import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.util.time.systemClock
import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor
import com.android.systemui.volume.dialog.shared.volumeDialogLogger
+import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSliderInputEventsInteractor
import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSliderInteractor
import com.android.systemui.volume.dialog.sliders.domain.model.volumeDialogSliderType
@@ -29,6 +30,7 @@ val Kosmos.volumeDialogSliderViewModel by
VolumeDialogSliderViewModel(
sliderType = volumeDialogSliderType,
interactor = volumeDialogSliderInteractor,
+ inputEventsInteractor = volumeDialogSliderInputEventsInteractor,
visibilityInteractor = volumeDialogVisibilityInteractor,
coroutineScope = applicationCoroutineScope,
volumeDialogSliderIconProvider = volumeDialogSliderIconProvider,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryKosmos.kt
index ddb9a3ffee6d..f0c0d30e6db4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryKosmos.kt
@@ -19,7 +19,6 @@ package com.android.systemui.wallpapers.data.repository
import android.content.applicationContext
import com.android.app.wallpaperManager
import com.android.systemui.broadcast.broadcastDispatcher
-import com.android.systemui.keyguard.data.repository.keyguardClockRepository
import com.android.systemui.keyguard.data.repository.keyguardRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
@@ -34,8 +33,7 @@ val Kosmos.wallpaperRepository by Fixture {
bgDispatcher = testDispatcher,
broadcastDispatcher = broadcastDispatcher,
userRepository = userRepository,
- wallpaperManager = wallpaperManager,
- keyguardClockRepository = keyguardClockRepository,
keyguardRepository = keyguardRepository,
+ wallpaperManager = wallpaperManager,
)
}
diff --git a/packages/Vcn/framework-b/framework-vcn-jarjar-rules.txt b/packages/Vcn/framework-b/framework-vcn-jarjar-rules.txt
index 7e27b24f749c..33287b7f3449 100644
--- a/packages/Vcn/framework-b/framework-vcn-jarjar-rules.txt
+++ b/packages/Vcn/framework-b/framework-vcn-jarjar-rules.txt
@@ -1,2 +1,3 @@
rule android.net.vcn.persistablebundleutils.** android.net.connectivity.android.net.vcn.persistablebundleutils.@1
-rule android.net.vcn.util.** android.net.connectivity.android.net.vcn.util.@1 \ No newline at end of file
+rule android.net.vcn.util.** android.net.connectivity.android.net.vcn.util.@1
+rule android.util.IndentingPrintWriter android.net.connectivity.android.util.IndentingPrintWriter \ No newline at end of file
diff --git a/packages/Vcn/service-b/src/com/android/server/ConnectivityServiceInitializerB.java b/packages/Vcn/service-b/src/com/android/server/ConnectivityServiceInitializerB.java
index 81c7edf4adf1..b9dcc6160d68 100644
--- a/packages/Vcn/service-b/src/com/android/server/ConnectivityServiceInitializerB.java
+++ b/packages/Vcn/service-b/src/com/android/server/ConnectivityServiceInitializerB.java
@@ -32,8 +32,7 @@ import com.android.tools.r8.keepanno.annotations.UsedByReflection;
// Without this annotation, this class will be treated as unused class and be removed during build
// time.
@UsedByReflection(kind = KeepItemKind.CLASS_AND_METHODS)
-// TODO(b/374174952): Replace VANILLA_ICE_CREAM with BAKLAVA after Android B finalization
-@TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@TargetApi(Build.VERSION_CODES.BAKLAVA)
public final class ConnectivityServiceInitializerB extends SystemService {
private static final String TAG = ConnectivityServiceInitializerB.class.getSimpleName();
private final VcnManagementService mVcnManagementService;
diff --git a/packages/Vcn/service-b/src/com/android/server/VcnManagementService.java b/packages/Vcn/service-b/src/com/android/server/VcnManagementService.java
index c9a99d729e91..8edd63dc341f 100644
--- a/packages/Vcn/service-b/src/com/android/server/VcnManagementService.java
+++ b/packages/Vcn/service-b/src/com/android/server/VcnManagementService.java
@@ -165,8 +165,7 @@ import java.util.concurrent.TimeUnit;
* @hide
*/
// TODO(b/180451994): ensure all incoming + outgoing calls have a cleared calling identity
-// TODO(b/374174952): Replace VANILLA_ICE_CREAM with BAKLAVA after Android B finalization
-@TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@TargetApi(Build.VERSION_CODES.BAKLAVA)
public class VcnManagementService extends IVcnManagementService.Stub {
@NonNull private static final String TAG = VcnManagementService.class.getSimpleName();
@NonNull private static final String CONTEXT_ATTRIBUTION_TAG = "VCN";
diff --git a/packages/Vcn/service-b/src/com/android/server/vcn/TelephonySubscriptionTracker.java b/packages/Vcn/service-b/src/com/android/server/vcn/TelephonySubscriptionTracker.java
index b04e25dff276..cedb2d16808f 100644
--- a/packages/Vcn/service-b/src/com/android/server/vcn/TelephonySubscriptionTracker.java
+++ b/packages/Vcn/service-b/src/com/android/server/vcn/TelephonySubscriptionTracker.java
@@ -79,8 +79,7 @@ import java.util.Set;
*
* @hide
*/
-// TODO(b/374174952): Replace VANILLA_ICE_CREAM with BAKLAVA after Android B finalization
-@TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@TargetApi(Build.VERSION_CODES.BAKLAVA)
public class TelephonySubscriptionTracker extends BroadcastReceiver {
@NonNull private static final String TAG = TelephonySubscriptionTracker.class.getSimpleName();
private static final boolean LOG_DBG = false; // STOPSHIP if true
diff --git a/packages/Vcn/service-b/src/com/android/server/vcn/Vcn.java b/packages/Vcn/service-b/src/com/android/server/vcn/Vcn.java
index 97f86b1bff5b..0f8b2885cf4d 100644
--- a/packages/Vcn/service-b/src/com/android/server/vcn/Vcn.java
+++ b/packages/Vcn/service-b/src/com/android/server/vcn/Vcn.java
@@ -77,8 +77,7 @@ import java.util.Set;
*
* @hide
*/
-// TODO(b/374174952): Replace VANILLA_ICE_CREAM with BAKLAVA after Android B finalization
-@TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@TargetApi(Build.VERSION_CODES.BAKLAVA)
public class Vcn extends Handler {
private static final String TAG = Vcn.class.getSimpleName();
diff --git a/packages/Vcn/service-b/src/com/android/server/vcn/VcnGatewayConnection.java b/packages/Vcn/service-b/src/com/android/server/vcn/VcnGatewayConnection.java
index 300b80f942ef..da411174f95c 100644
--- a/packages/Vcn/service-b/src/com/android/server/vcn/VcnGatewayConnection.java
+++ b/packages/Vcn/service-b/src/com/android/server/vcn/VcnGatewayConnection.java
@@ -174,8 +174,7 @@ import java.util.function.Consumer;
*
* @hide
*/
-// TODO(b/374174952): Replace VANILLA_ICE_CREAM with BAKLAVA after Android B finalization
-@TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@TargetApi(Build.VERSION_CODES.BAKLAVA)
public class VcnGatewayConnection extends StateMachine {
private static final String TAG = VcnGatewayConnection.class.getSimpleName();
diff --git a/packages/Vcn/service-b/src/com/android/server/vcn/VcnNetworkProvider.java b/packages/Vcn/service-b/src/com/android/server/vcn/VcnNetworkProvider.java
index 38fcf09145d9..bc815eb27454 100644
--- a/packages/Vcn/service-b/src/com/android/server/vcn/VcnNetworkProvider.java
+++ b/packages/Vcn/service-b/src/com/android/server/vcn/VcnNetworkProvider.java
@@ -58,8 +58,7 @@ import java.util.concurrent.Executor;
*/
// TODO(b/388919146): Implement a more generic solution to prevent concurrent modifications on
// mListeners and mRequests
-// TODO(b/374174952): Replace VANILLA_ICE_CREAM with BAKLAVA after Android B finalization
-@TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@TargetApi(Build.VERSION_CODES.BAKLAVA)
public class VcnNetworkProvider extends NetworkProvider {
private static final String TAG = VcnNetworkProvider.class.getSimpleName();
diff --git a/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java b/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java
index c8c645f1276d..aff7068b6c4f 100644
--- a/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java
+++ b/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java
@@ -61,8 +61,7 @@ import java.util.concurrent.TimeUnit;
*
* <p>This class is flag gated by "network_metric_monitor" and "ipsec_tramsform_state"
*/
-// TODO(b/374174952) Replace VANILLA_ICE_CREAM with BAKLAVA after Android B finalization
-@TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@TargetApi(Build.VERSION_CODES.BAKLAVA)
public class IpSecPacketLossDetector extends NetworkMetricMonitor {
private static final String TAG = IpSecPacketLossDetector.class.getSimpleName();
diff --git a/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/NetworkMetricMonitor.java b/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/NetworkMetricMonitor.java
index 55829a5fe978..fc9c7ac8a335 100644
--- a/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/NetworkMetricMonitor.java
+++ b/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/NetworkMetricMonitor.java
@@ -44,8 +44,7 @@ import java.util.concurrent.Executor;
*
* <p>This class is flag gated by "network_metric_monitor"
*/
-// TODO(b/374174952): Replace VANILLA_ICE_CREAM with BAKLAVA after Android B finalization
-@TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@TargetApi(Build.VERSION_CODES.BAKLAVA)
public abstract class NetworkMetricMonitor implements AutoCloseable {
private static final String TAG = NetworkMetricMonitor.class.getSimpleName();
diff --git a/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java b/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java
index 705141f3f1b4..7cb3257193a5 100644
--- a/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java
+++ b/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java
@@ -52,8 +52,7 @@ import java.util.Map;
import java.util.Set;
/** @hide */
-// TODO(b/374174952): Replace VANILLA_ICE_CREAM with BAKLAVA after Android B finalization
-@TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@TargetApi(Build.VERSION_CODES.BAKLAVA)
class NetworkPriorityClassifier {
@NonNull private static final String TAG = NetworkPriorityClassifier.class.getSimpleName();
/**
diff --git a/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/UnderlyingNetworkController.java b/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
index bc552e7e6afd..37ec0e8f40dc 100644
--- a/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
+++ b/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
@@ -75,8 +75,7 @@ import java.util.TreeSet;
*
* @hide
*/
-// TODO(b/374174952): Replace VANILLA_ICE_CREAM with BAKLAVA after Android B finalization
-@TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@TargetApi(Build.VERSION_CODES.BAKLAVA)
public class UnderlyingNetworkController {
@NonNull private static final String TAG = UnderlyingNetworkController.class.getSimpleName();
diff --git a/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java b/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java
index 776931bad73b..164b59f6f0cd 100644
--- a/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java
+++ b/packages/Vcn/service-b/src/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java
@@ -52,8 +52,7 @@ import java.util.concurrent.TimeUnit;
*
* @hide
*/
-// TODO(b/374174952): Replace VANILLA_ICE_CREAM with BAKLAVA after Android B finalization
-@TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@TargetApi(Build.VERSION_CODES.BAKLAVA)
public class UnderlyingNetworkEvaluator {
private static final String TAG = UnderlyingNetworkEvaluator.class.getSimpleName();
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index 8e998426685b..65550f2b4273 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -175,9 +175,9 @@ java_library {
}
java_device_for_host {
- name: "ravenwood-junit-impl-for-ravenizer",
+ name: "ravenwood-junit-for-ravenizer",
libs: [
- "ravenwood-junit-impl",
+ "ravenwood-junit",
],
visibility: [":__subpackages__"],
}
@@ -661,6 +661,9 @@ android_ravenwood_libgroup {
// StatsD
"framework-statsd.ravenwood",
+ // Graphics
+ "framework-graphics.ravenwood",
+
// Provide runtime versions of utils linked in below
"junit",
"truth",
diff --git a/ravenwood/Framework.bp b/ravenwood/Framework.bp
index 99fc31b258e9..71496b0d5766 100644
--- a/ravenwood/Framework.bp
+++ b/ravenwood/Framework.bp
@@ -399,3 +399,55 @@ java_genrule {
"framework-statsd.ravenwood.jar",
],
}
+
+//////////////////////
+// framework-graphics
+//////////////////////
+
+java_genrule {
+ name: "framework-graphics.ravenwood-base",
+ tools: ["hoststubgen"],
+ cmd: "$(location hoststubgen) " +
+ "@$(location :ravenwood-standard-options) " +
+
+ "--debug-log $(location framework-graphics.log) " +
+ "--stats-file $(location framework-graphics_stats.csv) " +
+ "--supported-api-list-file $(location framework-graphics_apis.csv) " +
+ "--gen-keep-all-file $(location framework-graphics_keep_all.txt) " +
+ "--gen-input-dump-file $(location framework-graphics_dump.txt) " +
+
+ "--out-impl-jar $(location ravenwood.jar) " +
+ "--in-jar $(location :framework-graphics.impl{.jar}) " +
+
+ "--policy-override-file $(location :ravenwood-common-policies) ",
+ srcs: [
+ ":framework-graphics.impl{.jar}",
+
+ ":ravenwood-common-policies",
+ ":ravenwood-standard-options",
+ ],
+ out: [
+ "ravenwood.jar",
+
+ // Following files are created just as FYI.
+ "framework-graphics_keep_all.txt",
+ "framework-graphics_dump.txt",
+
+ "framework-graphics.log",
+ "framework-graphics_stats.csv",
+ "framework-graphics_apis.csv",
+ ],
+ visibility: ["//visibility:private"],
+}
+
+java_genrule {
+ name: "framework-graphics.ravenwood",
+ defaults: ["ravenwood-internal-only-visibility-genrule"],
+ cmd: "cp $(in) $(out)",
+ srcs: [
+ ":framework-graphics.ravenwood-base{ravenwood.jar}",
+ ],
+ out: [
+ "framework-graphics.ravenwood.jar",
+ ],
+}
diff --git a/ravenwood/scripts/pta-framework.sh b/ravenwood/scripts/pta-framework.sh
index 224ab59e2e09..46c2c01c8ee8 100755
--- a/ravenwood/scripts/pta-framework.sh
+++ b/ravenwood/scripts/pta-framework.sh
@@ -79,6 +79,7 @@ run_pta() {
$extra_args
if ! [[ -f $OUT_SCRIPT ]] ; then
+ echo "No files need updating."
# no operations generated.
exit 0
fi
@@ -88,4 +89,4 @@ run_pta() {
return 0
}
-run_pta "$extra_args" \ No newline at end of file
+run_pta "$extra_args"
diff --git a/ravenwood/texts/ravenwood-framework-policies.txt b/ravenwood/texts/ravenwood-framework-policies.txt
index 4033782c607e..fff9e6ad41d5 100644
--- a/ravenwood/texts/ravenwood-framework-policies.txt
+++ b/ravenwood/texts/ravenwood-framework-policies.txt
@@ -62,4 +62,4 @@ class android.text.ClipboardManager keep # no-pta
# Just enough to allow ResourcesManager to run
class android.hardware.display.DisplayManagerGlobal keep # no-pta
- method getInstance ()Landroid/hardware/display/DisplayManagerGlobal; ignore
+ method getInstance ()Landroid/hardware/display/DisplayManagerGlobal; ignore # no-pta
diff --git a/ravenwood/texts/ravenwood-standard-options.txt b/ravenwood/texts/ravenwood-standard-options.txt
index 27223d8b72ff..91fd9283aff2 100644
--- a/ravenwood/texts/ravenwood-standard-options.txt
+++ b/ravenwood/texts/ravenwood-standard-options.txt
@@ -31,6 +31,9 @@
--remove-annotation
android.ravenwood.annotation.RavenwoodRemove
+--ignore-annotation
+ android.ravenwood.annotation.RavenwoodIgnore
+
--substitute-annotation
android.ravenwood.annotation.RavenwoodReplace
diff --git a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/Annotations.kt b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/Annotations.kt
index 4a11259a8ef7..ef1cb5dfca89 100644
--- a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/Annotations.kt
+++ b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/Annotations.kt
@@ -45,7 +45,8 @@ class Annotations {
"@android.ravenwood.annotation.RavenwoodRedirect"
FilterPolicy.Throw ->
"@android.ravenwood.annotation.RavenwoodThrow"
- FilterPolicy.Ignore -> null // Ignore has no annotation. (because it's not very safe.)
+ FilterPolicy.Ignore ->
+ "@android.ravenwood.annotation.RavenwoodIgnore"
FilterPolicy.Remove ->
"@android.ravenwood.annotation.RavenwoodRemove"
}
diff --git a/ravenwood/tools/ravenizer/Android.bp b/ravenwood/tools/ravenizer/Android.bp
index 2892d0778ec6..a52a04b44f2d 100644
--- a/ravenwood/tools/ravenizer/Android.bp
+++ b/ravenwood/tools/ravenizer/Android.bp
@@ -19,7 +19,7 @@ java_binary_host {
"ow2-asm-tree",
"ow2-asm-util",
"junit",
- "ravenwood-junit-impl-for-ravenizer",
+ "ravenwood-junit-for-ravenizer",
],
visibility: ["//visibility:public"],
}
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
index 75c629b77700..fda57d6bb986 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
@@ -45,6 +45,7 @@ import android.view.MotionEvent.PointerProperties;
import android.view.accessibility.AccessibilityEvent;
import com.android.server.LocalServices;
+import com.android.server.accessibility.autoclick.AutoclickController;
import com.android.server.accessibility.gestures.TouchExplorer;
import com.android.server.accessibility.magnification.FullScreenMagnificationController;
import com.android.server.accessibility.magnification.FullScreenMagnificationGestureHandler;
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 875b655fe3d2..91775f8eed96 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -5084,39 +5084,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
final List<String> permittedServices = dpm.getPermittedAccessibilityServices(userId);
// permittedServices null means all accessibility services are allowed.
- boolean allowed = permittedServices == null || permittedServices.contains(packageName);
- if (allowed) {
- if (android.permission.flags.Flags.enhancedConfirmationModeApisEnabled()
- && android.security.Flags.extendEcmToAllSettings()) {
- try {
- final EnhancedConfirmationManager userContextEcm =
- mContext.createContextAsUser(UserHandle.of(userId), /* flags = */ 0)
- .getSystemService(EnhancedConfirmationManager.class);
- if (userContextEcm != null) {
- return !userContextEcm.isRestricted(packageName,
- AppOpsManager.OPSTR_BIND_ACCESSIBILITY_SERVICE);
- }
- return false;
- } catch (PackageManager.NameNotFoundException e) {
- Log.e(LOG_TAG, "Exception when retrieving package:" + packageName, e);
- return false;
- }
- } else {
- try {
- final int mode = mContext.getSystemService(AppOpsManager.class)
- .noteOpNoThrow(AppOpsManager.OP_ACCESS_RESTRICTED_SETTINGS,
- uid, packageName);
- final boolean ecmEnabled = mContext.getResources().getBoolean(
- com.android.internal.R.bool.config_enhancedConfirmationModeEnabled);
- return !ecmEnabled || mode == AppOpsManager.MODE_ALLOWED
- || mode == AppOpsManager.MODE_DEFAULT;
- } catch (Exception e) {
- // Fallback in case if app ops is not available in testing.
- return false;
- }
- }
- }
- return false;
+ return permittedServices == null || permittedServices.contains(packageName);
} finally {
Binder.restoreCallingIdentity(identity);
}
diff --git a/services/accessibility/java/com/android/server/accessibility/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
index 8b758d29a2ac..1bc9c783df76 100644
--- a/services/accessibility/java/com/android/server/accessibility/AutoclickController.java
+++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
@@ -14,11 +14,14 @@
* limitations under the License.
*/
-package com.android.server.accessibility;
+package com.android.server.accessibility.autoclick;
-import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+import static android.view.MotionEvent.BUTTON_PRIMARY;
+import static android.view.accessibility.AccessibilityManager.AUTOCLICK_CURSOR_AREA_SIZE_DEFAULT;
+import static android.view.accessibility.AccessibilityManager.AUTOCLICK_DELAY_DEFAULT;
+import static android.view.accessibility.AccessibilityManager.AUTOCLICK_IGNORE_MINOR_CURSOR_MOVEMENT_DEFAULT;
-import static com.android.server.accessibility.AutoclickIndicatorView.SHOW_INDICATOR_DELAY_TIME;
+import static com.android.server.accessibility.autoclick.AutoclickIndicatorView.SHOW_INDICATOR_DELAY_TIME;
import android.accessibilityservice.AccessibilityTrace;
import android.annotation.NonNull;
@@ -26,7 +29,6 @@ import android.annotation.Nullable;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
-import android.graphics.PixelFormat;
import android.net.Uri;
import android.os.Handler;
import android.os.SystemClock;
@@ -37,10 +39,14 @@ import android.view.MotionEvent;
import android.view.MotionEvent.PointerCoords;
import android.view.MotionEvent.PointerProperties;
import android.view.WindowManager;
-import android.view.accessibility.AccessibilityManager;
import androidx.annotation.VisibleForTesting;
+import com.android.internal.accessibility.util.AccessibilityUtils;
+import com.android.server.accessibility.AccessibilityTraceManager;
+import com.android.server.accessibility.BaseEventStreamTransformation;
+import com.android.server.accessibility.Flags;
+
/**
* Implements "Automatically click on mouse stop" feature.
*
@@ -96,8 +102,7 @@ public class AutoclickController extends BaseEventStreamTransformation {
initiateAutoclickIndicator(handler);
}
- mClickScheduler =
- new ClickScheduler(handler, AccessibilityManager.AUTOCLICK_DELAY_DEFAULT);
+ mClickScheduler = new ClickScheduler(handler, AUTOCLICK_DELAY_DEFAULT);
mAutoclickSettingsObserver = new AutoclickSettingsObserver(mUserId, handler);
mAutoclickSettingsObserver.start(
mContext.getContentResolver(),
@@ -117,21 +122,8 @@ public class AutoclickController extends BaseEventStreamTransformation {
mAutoclickIndicatorScheduler = new AutoclickIndicatorScheduler(handler);
mAutoclickIndicatorView = new AutoclickIndicatorView(mContext);
- final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
- layoutParams.type = WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY;
- layoutParams.flags =
- WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
- | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
- | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
- layoutParams.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
- layoutParams.setFitInsetsTypes(0);
- layoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
- layoutParams.format = PixelFormat.TRANSLUCENT;
- layoutParams.setTitle(AutoclickIndicatorView.class.getSimpleName());
- layoutParams.inputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL;
-
mWindowManager = mContext.getSystemService(WindowManager.class);
- mWindowManager.addView(mAutoclickIndicatorView, layoutParams);
+ mWindowManager.addView(mAutoclickIndicatorView, mAutoclickIndicatorView.getLayoutParams());
}
@Override
@@ -209,6 +201,11 @@ public class AutoclickController extends BaseEventStreamTransformation {
private final Uri mAutoclickCursorAreaSizeSettingUri =
Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_AUTOCLICK_CURSOR_AREA_SIZE);
+ /** URI used to identify ignore minor cursor movement setting with content resolver. */
+ private final Uri mAutoclickIgnoreMinorCursorMovementSettingUri =
+ Settings.Secure.getUriFor(
+ Settings.Secure.ACCESSIBILITY_AUTOCLICK_IGNORE_MINOR_CURSOR_MOVEMENT);
+
private ContentResolver mContentResolver;
private ClickScheduler mClickScheduler;
private AutoclickIndicatorScheduler mAutoclickIndicatorScheduler;
@@ -247,18 +244,32 @@ public class AutoclickController extends BaseEventStreamTransformation {
mContentResolver = contentResolver;
mClickScheduler = clickScheduler;
mAutoclickIndicatorScheduler = autoclickIndicatorScheduler;
- mContentResolver.registerContentObserver(mAutoclickDelaySettingUri, false, this,
+ mContentResolver.registerContentObserver(
+ mAutoclickDelaySettingUri,
+ /* notifyForDescendants= */ false,
+ /* observer= */ this,
mUserId);
// Initialize mClickScheduler's initial delay value.
- onChange(true, mAutoclickDelaySettingUri);
+ onChange(/* selfChange= */ true, mAutoclickDelaySettingUri);
if (Flags.enableAutoclickIndicator()) {
// Register observer to listen to cursor area size setting change.
mContentResolver.registerContentObserver(
- mAutoclickCursorAreaSizeSettingUri, false, this, mUserId);
+ mAutoclickCursorAreaSizeSettingUri,
+ /* notifyForDescendants= */ false,
+ /* observer= */ this,
+ mUserId);
// Initialize mAutoclickIndicatorView's initial size.
- onChange(true, mAutoclickCursorAreaSizeSettingUri);
+ onChange(/* selfChange= */ true, mAutoclickCursorAreaSizeSettingUri);
+
+ // Register observer to listen to ignore minor cursor movement setting change.
+ mContentResolver.registerContentObserver(
+ mAutoclickIgnoreMinorCursorMovementSettingUri,
+ /* notifyForDescendants= */ false,
+ /* observer= */ this,
+ mUserId);
+ onChange(/* selfChange= */ true, mAutoclickIgnoreMinorCursorMovementSettingUri);
}
}
@@ -279,21 +290,41 @@ public class AutoclickController extends BaseEventStreamTransformation {
@Override
public void onChange(boolean selfChange, Uri uri) {
if (mAutoclickDelaySettingUri.equals(uri)) {
- int delay = Settings.Secure.getIntForUser(
- mContentResolver, Settings.Secure.ACCESSIBILITY_AUTOCLICK_DELAY,
- AccessibilityManager.AUTOCLICK_DELAY_DEFAULT, mUserId);
- mClickScheduler.updateDelay(delay);
- }
- if (Flags.enableAutoclickIndicator()
- && mAutoclickCursorAreaSizeSettingUri.equals(uri)) {
- int size =
+ int delay =
Settings.Secure.getIntForUser(
mContentResolver,
- Settings.Secure.ACCESSIBILITY_AUTOCLICK_CURSOR_AREA_SIZE,
- AccessibilityManager.AUTOCLICK_CURSOR_AREA_SIZE_DEFAULT,
+ Settings.Secure.ACCESSIBILITY_AUTOCLICK_DELAY,
+ AUTOCLICK_DELAY_DEFAULT,
mUserId);
- if (mAutoclickIndicatorScheduler != null) {
- mAutoclickIndicatorScheduler.updateCursorAreaSize(size);
+ mClickScheduler.updateDelay(delay);
+ }
+
+ if (Flags.enableAutoclickIndicator()) {
+ if (mAutoclickCursorAreaSizeSettingUri.equals(uri)) {
+ int size =
+ Settings.Secure.getIntForUser(
+ mContentResolver,
+ Settings.Secure.ACCESSIBILITY_AUTOCLICK_CURSOR_AREA_SIZE,
+ AUTOCLICK_CURSOR_AREA_SIZE_DEFAULT,
+ mUserId);
+ if (mAutoclickIndicatorScheduler != null) {
+ mAutoclickIndicatorScheduler.updateCursorAreaSize(size);
+ }
+ mClickScheduler.updateMovementSlope(size);
+ }
+
+ if (mAutoclickIgnoreMinorCursorMovementSettingUri.equals(uri)) {
+ boolean ignoreMinorCursorMovement =
+ Settings.Secure.getIntForUser(
+ mContentResolver,
+ Settings.Secure
+ .ACCESSIBILITY_AUTOCLICK_IGNORE_MINOR_CURSOR_MOVEMENT,
+ AUTOCLICK_IGNORE_MINOR_CURSOR_MOVEMENT_DEFAULT
+ ? AccessibilityUtils.State.ON
+ : AccessibilityUtils.State.OFF,
+ mUserId)
+ == AccessibilityUtils.State.ON;
+ mClickScheduler.setIgnoreMinorCursorMovement(ignoreMinorCursorMovement);
}
}
}
@@ -365,11 +396,16 @@ public class AutoclickController extends BaseEventStreamTransformation {
@VisibleForTesting
final class ClickScheduler implements Runnable {
/**
- * Minimal distance pointer has to move relative to anchor in order for movement not to be
- * discarded as noise. Anchor is the position of the last MOVE event that was not considered
- * noise.
+ * Default minimal distance pointer has to move relative to anchor in order for movement not
+ * to be discarded as noise. Anchor is the position of the last MOVE event that was not
+ * considered noise.
*/
- private static final double MOVEMENT_SLOPE = 20f;
+ private static final double DEFAULT_MOVEMENT_SLOPE = 20f;
+
+ private double mMovementSlope = DEFAULT_MOVEMENT_SLOPE;
+
+ /** Whether the minor cursor movement should be ignored. */
+ private boolean mIgnoreMinorCursorMovement = AUTOCLICK_IGNORE_MINOR_CURSOR_MOVEMENT_DEFAULT;
/** Whether there is pending click. */
private boolean mActive;
@@ -553,7 +589,19 @@ public class AutoclickController extends BaseEventStreamTransformation {
float deltaX = mAnchorCoords.x - event.getX(pointerIndex);
float deltaY = mAnchorCoords.y - event.getY(pointerIndex);
double delta = Math.hypot(deltaX, deltaY);
- return delta > MOVEMENT_SLOPE;
+ double slope =
+ ((Flags.enableAutoclickIndicator() && mIgnoreMinorCursorMovement)
+ ? mMovementSlope
+ : DEFAULT_MOVEMENT_SLOPE);
+ return delta > slope;
+ }
+
+ public void setIgnoreMinorCursorMovement(boolean ignoreMinorCursorMovement) {
+ mIgnoreMinorCursorMovement = ignoreMinorCursorMovement;
+ }
+
+ private void updateMovementSlope(double slope) {
+ mMovementSlope = slope;
}
/**
@@ -581,18 +629,30 @@ public class AutoclickController extends BaseEventStreamTransformation {
final long now = SystemClock.uptimeMillis();
- MotionEvent downEvent = MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN, 1,
- mTempPointerProperties, mTempPointerCoords, mMetaState,
- MotionEvent.BUTTON_PRIMARY, 1.0f, 1.0f, mLastMotionEvent.getDeviceId(), 0,
- mLastMotionEvent.getSource(), mLastMotionEvent.getFlags());
+ MotionEvent downEvent =
+ MotionEvent.obtain(
+ /* downTime= */ now,
+ /* eventTime= */ now,
+ MotionEvent.ACTION_DOWN,
+ /* pointerCount= */ 1,
+ mTempPointerProperties,
+ mTempPointerCoords,
+ mMetaState,
+ BUTTON_PRIMARY,
+ /* xPrecision= */ 1.0f,
+ /* yPrecision= */ 1.0f,
+ mLastMotionEvent.getDeviceId(),
+ /* edgeFlags= */ 0,
+ mLastMotionEvent.getSource(),
+ mLastMotionEvent.getFlags());
MotionEvent pressEvent = MotionEvent.obtain(downEvent);
pressEvent.setAction(MotionEvent.ACTION_BUTTON_PRESS);
- pressEvent.setActionButton(MotionEvent.BUTTON_PRIMARY);
+ pressEvent.setActionButton(BUTTON_PRIMARY);
MotionEvent releaseEvent = MotionEvent.obtain(downEvent);
releaseEvent.setAction(MotionEvent.ACTION_BUTTON_RELEASE);
- releaseEvent.setActionButton(MotionEvent.BUTTON_PRIMARY);
+ releaseEvent.setActionButton(BUTTON_PRIMARY);
releaseEvent.setButtonState(0);
MotionEvent upEvent = MotionEvent.obtain(downEvent);
diff --git a/services/accessibility/java/com/android/server/accessibility/AutoclickIndicatorView.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickIndicatorView.java
index f87dcdb200bb..557e1b2afcd5 100644
--- a/services/accessibility/java/com/android/server/accessibility/AutoclickIndicatorView.java
+++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickIndicatorView.java
@@ -14,17 +14,21 @@
* limitations under the License.
*/
-package com.android.server.accessibility;
+package com.android.server.accessibility.autoclick;
+import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import static android.view.accessibility.AccessibilityManager.AUTOCLICK_CURSOR_AREA_SIZE_DEFAULT;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
+import android.graphics.PixelFormat;
import android.graphics.RectF;
import android.util.DisplayMetrics;
import android.view.View;
+import android.view.WindowInsets;
+import android.view.WindowManager;
import android.view.accessibility.AccessibilityManager;
import android.view.animation.LinearInterpolator;
@@ -81,6 +85,26 @@ public class AutoclickIndicatorView extends View {
mRingRect = new RectF();
}
+ /**
+ * Retrieves the layout params for AutoclickIndicatorView, used when it's added to the Window
+ * Manager.
+ */
+ public final WindowManager.LayoutParams getLayoutParams() {
+ final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
+ layoutParams.type = WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY;
+ layoutParams.flags =
+ WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
+ | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
+ layoutParams.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
+ layoutParams.setFitInsetsTypes(WindowInsets.Type.statusBars());
+ layoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+ layoutParams.format = PixelFormat.TRANSLUCENT;
+ layoutParams.setTitle(AutoclickIndicatorView.class.getSimpleName());
+ layoutParams.inputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL;
+ return layoutParams;
+ }
+
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index a37b2b926c9c..02a8f6218468 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -612,7 +612,7 @@ public class CompanionDeviceManagerService extends SystemService {
@Override
public void enablePermissionsSync(int associationId) {
- if (UserHandle.getAppId(Binder.getCallingUid()) == SYSTEM_UID) {
+ if (UserHandle.getAppId(Binder.getCallingUid()) != SYSTEM_UID) {
throw new SecurityException("Caller must be system UID");
}
mSystemDataTransferProcessor.enablePermissionsSync(associationId);
@@ -620,7 +620,7 @@ public class CompanionDeviceManagerService extends SystemService {
@Override
public void disablePermissionsSync(int associationId) {
- if (UserHandle.getAppId(Binder.getCallingUid()) == SYSTEM_UID) {
+ if (UserHandle.getAppId(Binder.getCallingUid()) != SYSTEM_UID) {
throw new SecurityException("Caller must be system UID");
}
mSystemDataTransferProcessor.disablePermissionsSync(associationId);
@@ -628,7 +628,7 @@ public class CompanionDeviceManagerService extends SystemService {
@Override
public PermissionSyncRequest getPermissionSyncRequest(int associationId) {
- if (UserHandle.getAppId(Binder.getCallingUid()) == SYSTEM_UID) {
+ if (UserHandle.getAppId(Binder.getCallingUid()) != SYSTEM_UID) {
throw new SecurityException("Caller must be system UID");
}
return mSystemDataTransferProcessor.getPermissionSyncRequest(associationId);
@@ -704,7 +704,7 @@ public class CompanionDeviceManagerService extends SystemService {
@Override
public byte[] getBackupPayload(int userId) {
- if (UserHandle.getAppId(Binder.getCallingUid()) == SYSTEM_UID) {
+ if (UserHandle.getAppId(Binder.getCallingUid()) != SYSTEM_UID) {
throw new SecurityException("Caller must be system");
}
return mBackupRestoreProcessor.getBackupPayload(userId);
@@ -712,7 +712,7 @@ public class CompanionDeviceManagerService extends SystemService {
@Override
public void applyRestoredPayload(byte[] payload, int userId) {
- if (UserHandle.getAppId(Binder.getCallingUid()) == SYSTEM_UID) {
+ if (UserHandle.getAppId(Binder.getCallingUid()) != SYSTEM_UID) {
throw new SecurityException("Caller must be system");
}
mBackupRestoreProcessor.applyRestoredPayload(payload, userId);
diff --git a/services/core/java/com/android/server/MasterClearReceiver.java b/services/core/java/com/android/server/MasterClearReceiver.java
index 2b30c013235c..6f2ecd82a79c 100644
--- a/services/core/java/com/android/server/MasterClearReceiver.java
+++ b/services/core/java/com/android/server/MasterClearReceiver.java
@@ -130,7 +130,7 @@ public class MasterClearReceiver extends BroadcastReceiver {
if (mWipeExternalStorage) {
// thr will be started at the end of this task.
Slog.i(TAG, "Wiping external storage on async task");
- new WipeDataTask(context, thr).execute();
+ new WipeDataTask(context, thr).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} else {
Slog.i(TAG, "NOT wiping external storage; starting thread " + thr.getName());
thr.start();
diff --git a/services/core/java/com/android/server/am/BroadcastController.java b/services/core/java/com/android/server/am/BroadcastController.java
index bfacfbba4e22..bec5db79ff9d 100644
--- a/services/core/java/com/android/server/am/BroadcastController.java
+++ b/services/core/java/com/android/server/am/BroadcastController.java
@@ -317,7 +317,7 @@ class BroadcastController {
Slog.w(TAG, "registerReceiverWithFeature: no app for " + caller);
return null;
}
- if (callerApp.info.uid != SYSTEM_UID
+ if (!UserHandle.isCore(callerApp.info.uid)
&& !callerApp.getPkgList().containsKey(callerPackage)) {
throw new SecurityException("Given caller package " + callerPackage
+ " is not running in process " + callerApp);
diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java
index 0b7890167c08..3817ba1a28b9 100644
--- a/services/core/java/com/android/server/am/PendingIntentRecord.java
+++ b/services/core/java/com/android/server/am/PendingIntentRecord.java
@@ -305,10 +305,6 @@ public final class PendingIntentRecord extends IIntentSender.Stub {
this.stringName = null;
}
- @VisibleForTesting TempAllowListDuration getAllowlistDurationLocked(IBinder allowlistToken) {
- return mAllowlistDuration.get(allowlistToken);
- }
-
void setAllowBgActivityStarts(IBinder token, int flags) {
if (token == null) return;
if ((flags & FLAG_ACTIVITY_SENDER) != 0) {
@@ -327,12 +323,6 @@ public final class PendingIntentRecord extends IIntentSender.Stub {
mAllowBgActivityStartsForActivitySender.remove(token);
mAllowBgActivityStartsForBroadcastSender.remove(token);
mAllowBgActivityStartsForServiceSender.remove(token);
- if (mAllowlistDuration != null) {
- mAllowlistDuration.remove(token);
- if (mAllowlistDuration.isEmpty()) {
- mAllowlistDuration = null;
- }
- }
}
public void registerCancelListenerLocked(IResultReceiver receiver) {
@@ -713,7 +703,7 @@ public final class PendingIntentRecord extends IIntentSender.Stub {
return res;
}
- @VisibleForTesting BackgroundStartPrivileges getBackgroundStartPrivilegesForActivitySender(
+ private BackgroundStartPrivileges getBackgroundStartPrivilegesForActivitySender(
IBinder allowlistToken) {
return mAllowBgActivityStartsForActivitySender.contains(allowlistToken)
? BackgroundStartPrivileges.allowBackgroundActivityStarts(allowlistToken)
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index 8e09e3b8e112..833599810210 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -604,7 +604,7 @@ public class AppOpsService extends IAppOpsService.Stub {
}
}
- /** Returned from {@link #verifyAndGetBypass(int, String, String, String, boolean)}. */
+ /** Returned from {@link #verifyAndGetBypass(int, String, String, int, String, boolean)}. */
private static final class PackageVerificationResult {
final RestrictionBypass bypass;
@@ -3087,10 +3087,10 @@ public class AppOpsService extends IAppOpsService.Stub {
public int checkPackage(int uid, String packageName) {
Objects.requireNonNull(packageName);
try {
- verifyAndGetBypass(uid, packageName, null, null, true);
+ verifyAndGetBypass(uid, packageName, null, Process.INVALID_UID, null, true);
// When the caller is the system, it's possible that the packageName is the special
// one (e.g., "root") which isn't actually existed.
- if (resolveUid(packageName) == uid
+ if (resolveNonAppUid(packageName) == uid
|| (isPackageExisted(packageName)
&& !filterAppAccessUnlocked(packageName, UserHandle.getUserId(uid)))) {
return AppOpsManager.MODE_ALLOWED;
@@ -3306,7 +3306,7 @@ public class AppOpsService extends IAppOpsService.Stub {
boolean shouldCollectMessage, int notedCount) {
PackageVerificationResult pvr;
try {
- pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyPackageName);
+ pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyUid, proxyPackageName);
if (!pvr.isAttributionTagValid) {
attributionTag = null;
}
@@ -3930,7 +3930,7 @@ public class AppOpsService extends IAppOpsService.Stub {
// Test if the proxied operation will succeed before starting the proxy operation
final SyncNotedAppOp testProxiedOp = startOperationDryRun(code,
proxiedUid, resolvedProxiedPackageName, proxiedAttributionTag,
- proxiedVirtualDeviceId, resolvedProxyPackageName, proxiedFlags,
+ proxiedVirtualDeviceId, proxyUid, resolvedProxyPackageName, proxiedFlags,
startIfModeDefault);
if (!shouldStartForMode(testProxiedOp.getOpMode(), startIfModeDefault)) {
@@ -3970,7 +3970,7 @@ public class AppOpsService extends IAppOpsService.Stub {
int attributionChainId) {
PackageVerificationResult pvr;
try {
- pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyPackageName);
+ pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyUid, proxyPackageName);
if (!pvr.isAttributionTagValid) {
attributionTag = null;
}
@@ -4097,11 +4097,11 @@ public class AppOpsService extends IAppOpsService.Stub {
*/
private SyncNotedAppOp startOperationDryRun(int code, int uid,
@NonNull String packageName, @Nullable String attributionTag, int virtualDeviceId,
- String proxyPackageName, @OpFlags int flags,
+ int proxyUid, String proxyPackageName, @OpFlags int flags,
boolean startIfModeDefault) {
PackageVerificationResult pvr;
try {
- pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyPackageName);
+ pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyUid, proxyPackageName);
if (!pvr.isAttributionTagValid) {
attributionTag = null;
}
@@ -4656,13 +4656,17 @@ public class AppOpsService extends IAppOpsService.Stub {
private boolean isSpecialPackage(int callingUid, @Nullable String packageName) {
final String resolvedPackage = AppOpsManager.resolvePackageName(callingUid, packageName);
return callingUid == Process.SYSTEM_UID
- || resolveUid(resolvedPackage) != Process.INVALID_UID;
+ || resolveNonAppUid(resolvedPackage) != Process.INVALID_UID;
}
private boolean isCallerAndAttributionTrusted(@NonNull AttributionSource attributionSource) {
if (attributionSource.getUid() != Binder.getCallingUid()
&& attributionSource.isTrusted(mContext)) {
- return true;
+ // if there is a next attribution source, it must be trusted, as well.
+ if (attributionSource.getNext() == null
+ || attributionSource.getNext().isTrusted(mContext)) {
+ return true;
+ }
}
return mContext.checkPermission(android.Manifest.permission.UPDATE_APP_OPS_STATS,
Binder.getCallingPid(), Binder.getCallingUid(), null)
@@ -4757,19 +4761,20 @@ public class AppOpsService extends IAppOpsService.Stub {
}
/**
- * @see #verifyAndGetBypass(int, String, String, String, boolean)
+ * @see #verifyAndGetBypass(int, String, String, int, String, boolean)
*/
private @NonNull PackageVerificationResult verifyAndGetBypass(int uid, String packageName,
@Nullable String attributionTag) {
- return verifyAndGetBypass(uid, packageName, attributionTag, null);
+ return verifyAndGetBypass(uid, packageName, attributionTag, Process.INVALID_UID, null);
}
/**
- * @see #verifyAndGetBypass(int, String, String, String, boolean)
+ * @see #verifyAndGetBypass(int, String, String, int, String, boolean)
*/
private @NonNull PackageVerificationResult verifyAndGetBypass(int uid, String packageName,
- @Nullable String attributionTag, @Nullable String proxyPackageName) {
- return verifyAndGetBypass(uid, packageName, attributionTag, proxyPackageName, false);
+ @Nullable String attributionTag, int proxyUid, @Nullable String proxyPackageName) {
+ return verifyAndGetBypass(uid, packageName, attributionTag, proxyUid, proxyPackageName,
+ false);
}
/**
@@ -4780,14 +4785,15 @@ public class AppOpsService extends IAppOpsService.Stub {
* @param uid The uid the package belongs to
* @param packageName The package the might belong to the uid
* @param attributionTag attribution tag or {@code null} if no need to verify
- * @param proxyPackageName The proxy package, from which the attribution tag is to be pulled
+ * @param proxyUid The proxy uid, from which the attribution tag is to be pulled
+ * @param proxyPackageName The proxy package, from which the attribution tag may be pulled
* @param suppressErrorLogs Whether to print to logcat about nonmatching parameters
*
* @return PackageVerificationResult containing {@link RestrictionBypass} and whether the
* attribution tag is valid
*/
private @NonNull PackageVerificationResult verifyAndGetBypass(int uid, String packageName,
- @Nullable String attributionTag, @Nullable String proxyPackageName,
+ @Nullable String attributionTag, int proxyUid, @Nullable String proxyPackageName,
boolean suppressErrorLogs) {
if (uid == Process.ROOT_UID) {
// For backwards compatibility, don't check package name for root UID.
@@ -4831,34 +4837,47 @@ public class AppOpsService extends IAppOpsService.Stub {
int callingUid = Binder.getCallingUid();
- // Allow any attribution tag for resolvable uids
- int pkgUid;
+ // Allow any attribution tag for resolvable, non-app uids
+ int nonAppUid;
if (Objects.equals(packageName, "com.android.shell")) {
// Special case for the shell which is a package but should be able
// to bypass app attribution tag restrictions.
- pkgUid = Process.SHELL_UID;
+ nonAppUid = Process.SHELL_UID;
} else {
- pkgUid = resolveUid(packageName);
+ nonAppUid = resolveNonAppUid(packageName);
}
- if (pkgUid != Process.INVALID_UID) {
- if (pkgUid != UserHandle.getAppId(uid)) {
+ if (nonAppUid != Process.INVALID_UID) {
+ if (nonAppUid != UserHandle.getAppId(uid)) {
if (!suppressErrorLogs) {
Slog.e(TAG, "Bad call made by uid " + callingUid + ". "
- + "Package \"" + packageName + "\" does not belong to uid " + uid
- + ".");
+ + "Package \"" + packageName + "\" does not belong to uid " + uid
+ + ".");
+ }
+ String otherUidMessage =
+ DEBUG ? " but it is really " + nonAppUid : " but it is not";
+ throw new SecurityException("Specified package \"" + packageName
+ + "\" under uid " + UserHandle.getAppId(uid) + otherUidMessage);
+ }
+ // We only allow bypassing the attribution tag verification if the proxy is a
+ // system app (or is null), in order to prevent abusive apps clogging the appops
+ // system with unlimited attribution tags via proxy calls.
+ boolean proxyIsSystemAppOrNull = true;
+ if (proxyPackageName != null) {
+ int proxyAppId = UserHandle.getAppId(proxyUid);
+ if (proxyAppId >= Process.FIRST_APPLICATION_UID) {
+ proxyIsSystemAppOrNull =
+ mPackageManagerInternal.isSystemPackage(proxyPackageName);
}
- String otherUidMessage = DEBUG ? " but it is really " + pkgUid : " but it is not";
- throw new SecurityException("Specified package \"" + packageName + "\" under uid "
- + UserHandle.getAppId(uid) + otherUidMessage);
}
return new PackageVerificationResult(RestrictionBypass.UNRESTRICTED,
- /* isAttributionTagValid */ true);
+ /* isAttributionTagValid */ proxyIsSystemAppOrNull);
}
int userId = UserHandle.getUserId(uid);
RestrictionBypass bypass = null;
boolean isAttributionTagValid = false;
+ int pkgUid = nonAppUid;
final long ident = Binder.clearCallingIdentity();
try {
PackageManagerInternal pmInt = LocalServices.getService(PackageManagerInternal.class);
@@ -5649,7 +5668,7 @@ public class AppOpsService extends IAppOpsService.Stub {
if (nonpackageUid != -1) {
packageName = null;
} else {
- packageUid = resolveUid(packageName);
+ packageUid = resolveNonAppUid(packageName);
if (packageUid < 0) {
packageUid = AppGlobals.getPackageManager().getPackageUid(packageName,
PackageManager.MATCH_UNINSTALLED_PACKAGES, userId);
@@ -6749,7 +6768,13 @@ public class AppOpsService extends IAppOpsService.Stub {
if (restricted && attrOp.isRunning()) {
attrOp.pause();
} else if (attrOp.isPaused()) {
- attrOp.resume();
+ RestrictionBypass bypass = verifyAndGetBypass(uid, ops.packageName, attrOp.tag)
+ .bypass;
+ if (!isOpRestrictedLocked(uid, code, ops.packageName, attrOp.tag,
+ Context.DEVICE_ID_DEFAULT, bypass, false)) {
+ // Only resume if there are no other restrictions remaining on this op
+ attrOp.resume();
+ }
}
}
}
@@ -7198,7 +7223,7 @@ public class AppOpsService extends IAppOpsService.Stub {
}
}
- private static int resolveUid(String packageName) {
+ private static int resolveNonAppUid(String packageName) {
if (packageName == null) {
return Process.INVALID_UID;
}
diff --git a/services/core/java/com/android/server/audio/AudioManagerShellCommand.java b/services/core/java/com/android/server/audio/AudioManagerShellCommand.java
index 030ce12f5063..fece7a899f0a 100644
--- a/services/core/java/com/android/server/audio/AudioManagerShellCommand.java
+++ b/services/core/java/com/android/server/audio/AudioManagerShellCommand.java
@@ -65,6 +65,12 @@ class AudioManagerShellCommand extends ShellCommand {
return setRingerMode();
case "set-volume":
return setVolume();
+ case "get-min-volume":
+ return getMinVolume();
+ case "get-max-volume":
+ return getMaxVolume();
+ case "get-stream-volume":
+ return getStreamVolume();
case "set-device-volume":
return setDeviceVolume();
case "adj-mute":
@@ -106,6 +112,12 @@ class AudioManagerShellCommand extends ShellCommand {
pw.println(" Sets the Ringer mode to one of NORMAL|SILENT|VIBRATE");
pw.println(" set-volume STREAM_TYPE VOLUME_INDEX");
pw.println(" Sets the volume for STREAM_TYPE to VOLUME_INDEX");
+ pw.println(" get-min-volume STREAM_TYPE");
+ pw.println(" Gets the min volume for STREAM_TYPE");
+ pw.println(" get-max-volume STREAM_TYPE");
+ pw.println(" Gets the max volume for STREAM_TYPE");
+ pw.println(" get-stream-volume STREAM_TYPE");
+ pw.println(" Gets the volume for STREAM_TYPE");
pw.println(" set-device-volume STREAM_TYPE VOLUME_INDEX NATIVE_DEVICE_TYPE");
pw.println(" Sets for NATIVE_DEVICE_TYPE the STREAM_TYPE volume to VOLUME_INDEX");
pw.println(" adj-mute STREAM_TYPE");
@@ -296,6 +308,33 @@ class AudioManagerShellCommand extends ShellCommand {
return 0;
}
+ private int getMinVolume() {
+ final Context context = mService.mContext;
+ final AudioManager am = context.getSystemService(AudioManager.class);
+ final int stream = readIntArg();
+ final int result = am.getStreamMinVolume(stream);
+ getOutPrintWriter().println("AudioManager.getStreamMinVolume(" + stream + ") -> " + result);
+ return 0;
+ }
+
+ private int getMaxVolume() {
+ final Context context = mService.mContext;
+ final AudioManager am = context.getSystemService(AudioManager.class);
+ final int stream = readIntArg();
+ final int result = am.getStreamMaxVolume(stream);
+ getOutPrintWriter().println("AudioManager.getStreamMaxVolume(" + stream + ") -> " + result);
+ return 0;
+ }
+
+ private int getStreamVolume() {
+ final Context context = mService.mContext;
+ final AudioManager am = context.getSystemService(AudioManager.class);
+ final int stream = readIntArg();
+ final int result = am.getStreamVolume(stream);
+ getOutPrintWriter().println("AudioManager.getStreamVolume(" + stream + ") -> " + result);
+ return 0;
+ }
+
private int setDeviceVolume() {
final Context context = mService.mContext;
final AudioDeviceVolumeManager advm = (AudioDeviceVolumeManager) context.getSystemService(
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 709c13bc9704..320dd8f188c0 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -47,9 +47,10 @@ import static android.media.AudioManager.RINGER_MODE_NORMAL;
import static android.media.AudioManager.RINGER_MODE_SILENT;
import static android.media.AudioManager.RINGER_MODE_VIBRATE;
import static android.media.AudioManager.STREAM_SYSTEM;
-import static android.media.IAudioManagerNative.HardeningType;
import static android.media.audio.Flags.autoPublicVolumeApiHardening;
import static android.media.audio.Flags.automaticBtDeviceType;
+import static android.media.audio.Flags.cacheGetStreamMinMaxVolume;
+import static android.media.audio.Flags.cacheGetStreamVolume;
import static android.media.audio.Flags.concurrentAudioRecordBypassPermission;
import static android.media.audio.Flags.featureSpatialAudioHeadtrackingLowLatency;
import static android.media.audio.Flags.focusFreezeTestApi;
@@ -898,6 +899,16 @@ public class AudioService extends IAudioService.Stub
public void permissionUpdateBarrier() {
AudioService.this.permissionUpdateBarrier();
}
+
+ /**
+ * Update mute state event for port
+ * @param portId Port id to update
+ * @param event the mute event containing info about the mute
+ */
+ @Override
+ public void portMuteEvent(int portId, int event) {
+ mPlaybackMonitor.portMuteEvent(portId, event, Binder.getCallingUid());
+ }
};
// List of binder death handlers for setMode() client processes.
@@ -1900,6 +1911,12 @@ public class AudioService extends IAudioService.Stub
mSpatializerHelper.onRoutingUpdated();
}
checkMuteAwaitConnection();
+ if (cacheGetStreamVolume()) {
+ if (DEBUG_VOL) {
+ Log.d(TAG, "Clear volume cache after routing update");
+ }
+ AudioManager.clearVolumeCache(AudioManager.VOLUME_CACHING_API);
+ }
}
//-----------------------------------------------------------------
@@ -4976,6 +4993,10 @@ public class AudioService extends IAudioService.Stub
+ ringMyCar());
pw.println("\tandroid.media.audio.Flags.concurrentAudioRecordBypassPermission:"
+ concurrentAudioRecordBypassPermission());
+ pw.println("\tandroid.media.audio.Flags.cacheGetStreamMinMaxVolume:"
+ + cacheGetStreamMinMaxVolume());
+ pw.println("\tandroid.media.audio.Flags.cacheGetStreamVolume:"
+ + cacheGetStreamVolume());
}
private void dumpAudioMode(PrintWriter pw) {
@@ -7042,6 +7063,13 @@ public class AudioService extends IAudioService.Stub
streamState.mIsMuted = false;
}
}
+ if (cacheGetStreamVolume()) {
+ if (DEBUG_VOL) {
+ Log.d(TAG,
+ "Clear volume cache after possibly changing mute in readAudioSettings");
+ }
+ AudioManager.clearVolumeCache(AudioManager.VOLUME_CACHING_API);
+ }
}
readVolumeGroupsSettings(userSwitch);
@@ -9262,11 +9290,23 @@ public class AudioService extends IAudioService.Stub
public void put(int key, int value) {
super.put(key, value);
record("put", key, value);
+ if (cacheGetStreamVolume()) {
+ if (DEBUG_VOL) {
+ Log.d(TAG, "Clear volume cache after update index map");
+ }
+ AudioManager.clearVolumeCache(AudioManager.VOLUME_CACHING_API);
+ }
}
@Override
public void setValueAt(int index, int value) {
super.setValueAt(index, value);
record("setValueAt", keyAt(index), value);
+ if (cacheGetStreamVolume()) {
+ if (DEBUG_VOL) {
+ Log.d(TAG, "Clear volume cache after update index map");
+ }
+ AudioManager.clearVolumeCache(AudioManager.VOLUME_CACHING_API);
+ }
}
// Record all changes in the VolumeStreamState
@@ -9366,6 +9406,12 @@ public class AudioService extends IAudioService.Stub
mIndexMinNoPerm = mIndexMin;
}
}
+ if (cacheGetStreamMinMaxVolume() && mStreamType == AudioSystem.STREAM_VOICE_CALL) {
+ if (DEBUG_VOL) {
+ Log.d(TAG, "Clear min volume cache from updateIndexFactors");
+ }
+ AudioManager.clearVolumeCache(AudioManager.VOLUME_MIN_CACHING_API);
+ }
final int status = AudioSystem.initStreamVolume(
mStreamType, indexMinVolCurve, indexMaxVolCurve);
@@ -9403,11 +9449,19 @@ public class AudioService extends IAudioService.Stub
* @param index minimum index expressed in "UI units", i.e. no 10x factor
*/
public void updateNoPermMinIndex(int index) {
+ boolean changedNoPermMinIndex =
+ cacheGetStreamMinMaxVolume() && (index * 10) != mIndexMinNoPerm;
mIndexMinNoPerm = index * 10;
if (mIndexMinNoPerm < mIndexMin) {
Log.e(TAG, "Invalid mIndexMinNoPerm for stream " + mStreamType);
mIndexMinNoPerm = mIndexMin;
}
+ if (changedNoPermMinIndex) {
+ if (DEBUG_VOL) {
+ Log.d(TAG, "Clear min volume cache from updateNoPermMinIndex");
+ }
+ AudioManager.clearVolumeCache(AudioManager.VOLUME_MIN_CACHING_API);
+ }
}
/**
@@ -9982,8 +10036,9 @@ public class AudioService extends IAudioService.Stub
* @return true if the mute state was changed
*/
public boolean mute(boolean state, boolean apply, String src) {
+ boolean changed;
synchronized (VolumeStreamState.class) {
- boolean changed = state != mIsMuted;
+ changed = state != mIsMuted;
if (changed) {
sMuteLogger.enqueue(
new AudioServiceEvents.StreamMuteEvent(mStreamType, state, src));
@@ -10001,8 +10056,16 @@ public class AudioService extends IAudioService.Stub
doMute();
}
}
- return changed;
}
+
+ if (cacheGetStreamVolume() && changed) {
+ if (DEBUG_VOL) {
+ Log.d(TAG, "Clear volume cache after changing mute state");
+ }
+ AudioManager.clearVolumeCache(AudioManager.VOLUME_CACHING_API);
+ }
+
+ return changed;
}
public void doMute() {
diff --git a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
index e2e06b63c7d6..57b5febf4df0 100644
--- a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
+++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
@@ -435,6 +435,49 @@ public final class PlaybackActivityMonitor
/**
* Update event for port
* @param portId Port id to update
+ * @param event the mute event containing info about the mute
+ * @param binderUid Calling binder uid
+ */
+ public void portMuteEvent(int portId, @PlayerMuteEvent int event, int binderUid) {
+ if (!UserHandle.isCore(binderUid)) {
+ Log.e(TAG, "Forbidden operation from uid " + binderUid);
+ return;
+ }
+
+ synchronized (mPlayerLock) {
+ int piid;
+ if (portToPiidSimplification()) {
+ int idxOfPiid = mPiidToPortId.indexOfValue(portId);
+ if (idxOfPiid < 0) {
+ Log.w(TAG, "No piid assigned for invalid/internal port id " + portId);
+ return;
+ }
+ piid = mPiidToPortId.keyAt(idxOfPiid);
+ } else {
+ piid = mPortIdToPiid.get(portId, PLAYER_PIID_INVALID);
+ if (piid == PLAYER_PIID_INVALID) {
+ Log.w(TAG, "No piid assigned for invalid/internal port id " + portId);
+ return;
+ }
+ }
+ final AudioPlaybackConfiguration apc = mPlayers.get(piid);
+ if (apc == null) {
+ Log.w(TAG, "No AudioPlaybackConfiguration assigned for piid " + piid);
+ return;
+ }
+
+ if (apc.getPlayerType()
+ == AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL) {
+ // FIXME SoundPool not ready for state reporting
+ return;
+ }
+ mEventHandler.sendMessage(
+ mEventHandler.obtainMessage(MSG_IIL_UPDATE_PLAYER_MUTED_EVENT, piid, event, null));
+ }
+ }
+ /**
+ * Update event for port
+ * @param portId Port id to update
* @param event The new port event
* @param extras The values associated with this event
* @param binderUid Calling binder uid
@@ -479,15 +522,10 @@ public final class PlaybackActivityMonitor
return;
}
- if (event == AudioPlaybackConfiguration.PLAYER_UPDATE_MUTED) {
- mEventHandler.sendMessage(
- mEventHandler.obtainMessage(MSG_IIL_UPDATE_PLAYER_MUTED_EVENT, piid,
- portId,
- extras));
- } else if (event == AudioPlaybackConfiguration.PLAYER_UPDATE_FORMAT) {
+ if (event == AudioPlaybackConfiguration.PLAYER_UPDATE_FORMAT) {
mEventHandler.sendMessage(
mEventHandler.obtainMessage(MSG_IIL_UPDATE_PLAYER_FORMAT, piid,
- portId,
+ -1,
extras));
}
}
@@ -1695,9 +1733,7 @@ public final class PlaybackActivityMonitor
* event for player getting muted
* args:
* msg.arg1: piid
- * msg.arg2: port id
- * msg.obj: extras describing the mute reason
- * type: PersistableBundle
+ * msg.arg2: mute reason
*/
private static final int MSG_IIL_UPDATE_PLAYER_MUTED_EVENT = 2;
@@ -1705,7 +1741,6 @@ public final class PlaybackActivityMonitor
* event for player reporting playback format and spatialization status
* args:
* msg.arg1: piid
- * msg.arg2: port id
* msg.obj: extras describing the sample rate, channel mask, spatialized
* type: PersistableBundle
*/
@@ -1729,17 +1764,9 @@ public final class PlaybackActivityMonitor
break;
case MSG_IIL_UPDATE_PLAYER_MUTED_EVENT:
- // TODO: replace PersistableBundle with own struct
- PersistableBundle extras = (PersistableBundle) msg.obj;
- if (extras == null) {
- Log.w(TAG, "Received mute event with no extras");
- break;
- }
- @PlayerMuteEvent int eventValue = extras.getInt(EXTRA_PLAYER_EVENT_MUTE);
-
synchronized (mPlayerLock) {
int piid = msg.arg1;
-
+ @PlayerMuteEvent int eventValue = msg.arg2;
int[] eventValues = new int[1];
eventValues[0] = eventValue;
diff --git a/services/core/java/com/android/server/backup/SystemBackupAgent.java b/services/core/java/com/android/server/backup/SystemBackupAgent.java
index f66c7e115fc0..677e0c055455 100644
--- a/services/core/java/com/android/server/backup/SystemBackupAgent.java
+++ b/services/core/java/com/android/server/backup/SystemBackupAgent.java
@@ -35,7 +35,7 @@ import android.os.UserHandle;
import android.os.UserManager;
import android.util.Slog;
-import com.android.server.backup.Flags;
+import com.android.server.display.DisplayBackupHelper;
import com.android.server.notification.NotificationBackupHelper;
import com.google.android.collect.Sets;
@@ -67,6 +67,7 @@ public class SystemBackupAgent extends BackupAgentHelper {
private static final String APP_GENDER_HELPER = "app_gender";
private static final String COMPANION_HELPER = "companion";
private static final String SYSTEM_GENDER_HELPER = "system_gender";
+ private static final String DISPLAY_HELPER = "display";
// These paths must match what the WallpaperManagerService uses. The leaf *_FILENAME
// are also used in the full-backup file format, so must not change unless steps are
@@ -104,7 +105,8 @@ public class SystemBackupAgent extends BackupAgentHelper {
APP_LOCALES_HELPER,
COMPANION_HELPER,
APP_GENDER_HELPER,
- SYSTEM_GENDER_HELPER);
+ SYSTEM_GENDER_HELPER,
+ DISPLAY_HELPER);
/** Helpers that are enabled for full, non-system users. */
private static final Set<String> sEligibleHelpersForNonSystemUser =
@@ -146,6 +148,7 @@ public class SystemBackupAgent extends BackupAgentHelper {
addHelperIfEligibleForUser(COMPANION_HELPER, new CompanionBackupHelper(mUserId));
addHelperIfEligibleForUser(SYSTEM_GENDER_HELPER,
new SystemGrammaticalGenderBackupHelper(mUserId));
+ addHelperIfEligibleForUser(DISPLAY_HELPER, new DisplayBackupHelper(mUserId));
}
@Override
diff --git a/services/core/java/com/android/server/display/DisplayBackupHelper.java b/services/core/java/com/android/server/display/DisplayBackupHelper.java
new file mode 100644
index 000000000000..0d3a09fb2305
--- /dev/null
+++ b/services/core/java/com/android/server/display/DisplayBackupHelper.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display;
+
+
+import android.annotation.Nullable;
+import android.app.backup.BlobBackupHelper;
+import android.hardware.display.DisplayManagerInternal;
+import android.util.AtomicFile;
+import android.util.AtomicFileOutputStream;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.LocalServices;
+import com.android.server.display.feature.DisplayManagerFlags;
+import com.android.server.display.utils.DebugUtils;
+
+import java.io.IOException;
+
+/**
+ * Display manager specific information backup helper. Backs-up the entire files for the given
+ * user.
+ * @hide
+ */
+public class DisplayBackupHelper extends BlobBackupHelper {
+ private static final String TAG = "DisplayBackupHelper";
+
+ // current schema of the backup state blob
+ private static final int BLOB_VERSION = 1;
+
+ // key under which the data blob is committed to back up
+ private static final String KEY_DISPLAY = "display";
+
+ // To enable these logs, run:
+ // adb shell setprop persist.log.tag.DisplayBackupHelper DEBUG
+ // adb reboot
+ private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
+
+ private final int mUserId;
+ private final Injector mInjector;
+
+ /**
+ * Construct a helper to manage backup/restore of entire files within Display Manager.
+ *
+ * @param userId id of the user for which backup will be done.
+ */
+ public DisplayBackupHelper(int userId) {
+ this(userId, new Injector());
+ }
+
+ @VisibleForTesting
+ DisplayBackupHelper(int userId, Injector injector) {
+ super(BLOB_VERSION, KEY_DISPLAY);
+ mUserId = userId;
+ mInjector = injector;
+ }
+
+ @Override
+ protected byte[] getBackupPayload(String key) {
+ if (!KEY_DISPLAY.equals(key) || !mInjector.isDisplayTopologyFlagEnabled()) {
+ return null;
+ }
+ try {
+ var result = mInjector.readTopologyFile(mUserId);
+ Slog.i(TAG, "getBackupPayload for " + key + " done, size=" + result.length);
+ return result;
+ } catch (IOException e) {
+ if (DEBUG) Slog.d(TAG, "Skip topology backup", e);
+ return null;
+ }
+ }
+
+ @Override
+ protected void applyRestoredPayload(String key, byte[] payload) {
+ if (!KEY_DISPLAY.equals(key) || !mInjector.isDisplayTopologyFlagEnabled()) {
+ return;
+ }
+ try (var oStream = mInjector.writeTopologyFile(mUserId)) {
+ oStream.write(payload);
+ oStream.markSuccess();
+ Slog.i(TAG, "applyRestoredPayload for " + key + " size=" + payload.length
+ + " to " + oStream);
+ } catch (IOException e) {
+ Slog.e(TAG, "applyRestoredPayload failed", e);
+ return;
+ }
+ var displayManagerInternal = mInjector.getDisplayManagerInternal();
+ if (displayManagerInternal == null) {
+ Slog.e(TAG, "DisplayManagerInternal is null");
+ return;
+ }
+
+ displayManagerInternal.reloadTopologies(mUserId);
+ }
+
+ @VisibleForTesting
+ static class Injector {
+ private final boolean mIsDisplayTopologyEnabled =
+ new DisplayManagerFlags().isDisplayTopologyEnabled();
+
+ boolean isDisplayTopologyFlagEnabled() {
+ return mIsDisplayTopologyEnabled;
+ }
+
+ @Nullable
+ DisplayManagerInternal getDisplayManagerInternal() {
+ return LocalServices.getService(DisplayManagerInternal.class);
+ }
+
+ byte[] readTopologyFile(int userId) throws IOException {
+ return getTopologyFile(userId).readFully();
+ }
+
+ AtomicFileOutputStream writeTopologyFile(int userId) throws IOException {
+ return new AtomicFileOutputStream(getTopologyFile(userId));
+ }
+
+ private AtomicFile getTopologyFile(int userId) {
+ return new AtomicFile(DisplayTopologyXmlStore.getUserTopologyFile(userId),
+ /*commitTag=*/ "topology-state");
+ }
+ };
+}
diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
index d435144b28c6..b9ce8c93dbde 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -21,6 +21,7 @@ import android.os.Build;
import android.os.SystemProperties;
import android.text.TextUtils;
import android.util.Slog;
+import android.window.DesktopExperienceFlags;
import com.android.server.display.feature.flags.Flags;
import com.android.server.display.utils.DebugUtils;
@@ -250,7 +251,7 @@ public class DisplayManagerFlags {
);
private final FlagState mEnableDisplayContentModeManagementFlagState = new FlagState(
Flags.FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT,
- Flags::enableDisplayContentModeManagement
+ DesktopExperienceFlags.ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT::isTrue
);
private final FlagState mSubscribeGranularDisplayEvents = new FlagState(
diff --git a/services/core/java/com/android/server/display/plugin/PluginManager.java b/services/core/java/com/android/server/display/plugin/PluginManager.java
index d4099975cafa..cb0a4574361a 100644
--- a/services/core/java/com/android/server/display/plugin/PluginManager.java
+++ b/services/core/java/com/android/server/display/plugin/PluginManager.java
@@ -74,15 +74,17 @@ public class PluginManager {
/**
* Adds change listener for particular plugin type
*/
- public <T> void subscribe(PluginType<T> type, PluginChangeListener<T> listener) {
- mPluginStorage.addListener(type, listener);
+ public <T> void subscribe(PluginType<T> type, String uniqueDisplayId,
+ PluginChangeListener<T> listener) {
+ mPluginStorage.addListener(type, uniqueDisplayId, listener);
}
/**
* Removes change listener
*/
- public <T> void unsubscribe(PluginType<T> type, PluginChangeListener<T> listener) {
- mPluginStorage.removeListener(type, listener);
+ public <T> void unsubscribe(PluginType<T> type, String uniqueDisplayId,
+ PluginChangeListener<T> listener) {
+ mPluginStorage.removeListener(type, uniqueDisplayId, listener);
}
/**
diff --git a/services/core/java/com/android/server/display/plugin/PluginStorage.java b/services/core/java/com/android/server/display/plugin/PluginStorage.java
index dd3415fb614d..5102c2709329 100644
--- a/services/core/java/com/android/server/display/plugin/PluginStorage.java
+++ b/services/core/java/com/android/server/display/plugin/PluginStorage.java
@@ -20,10 +20,13 @@ import android.annotation.Nullable;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.tools.r8.keepanno.annotations.KeepForApi;
import java.io.PrintWriter;
import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -35,42 +38,97 @@ import java.util.Set;
public class PluginStorage {
private static final String TAG = "PluginStorage";
+ // Special ID used to indicate that given value is to be applied globally, rather than to a
+ // specific display. If both GLOBAL and specific display values are present - specific display
+ // value is selected.
+ @VisibleForTesting
+ static final String GLOBAL_ID = "GLOBAL";
+
private final Object mLock = new Object();
@GuardedBy("mLock")
- private final Map<PluginType<?>, Object> mValues = new HashMap<>();
+ private final Map<PluginType<?>, ValuesContainer<?>> mValues = new HashMap<>();
@GuardedBy("mLock")
private final Map<PluginType<?>, ListenersContainer<?>> mListeners = new HashMap<>();
@GuardedBy("mLock")
- private final PluginEventStorage mPluginEventStorage = new PluginEventStorage();
+ private final Map<String, PluginEventStorage> mPluginEventStorages = new HashMap<>();
+
+ /**
+ * Updates value in storage and forwards it to corresponding listeners for all displays
+ * that does not have display specific value.
+ * Should be called by OEM Plugin implementation in order to communicate with Framework
+ */
+ @KeepForApi
+ public <T> void updateGlobalValue(PluginType<T> type, @Nullable T value) {
+ updateValue(type, GLOBAL_ID, value);
+ }
/**
- * Updates value in storage and forwards it to corresponding listeners.
- * Should be called by OEM Plugin implementation in order to provide communicate with Framework
+ * Updates value in storage and forwards it to corresponding listeners for specific display.
+ * Should be called by OEM Plugin implementation in order to communicate with Framework
+ * @param type - plugin type, that need to be updated
+ * @param uniqueDisplayId - uniqueDisplayId that this type/value should be applied to
+ * @param value - plugin value for particular type and display
*/
@KeepForApi
- public <T> void updateValue(PluginType<T> type, @Nullable T value) {
- Slog.d(TAG, "updateValue, type=" + type.mName + "; value=" + value);
+ public <T> void updateValue(PluginType<T> type, String uniqueDisplayId, @Nullable T value) {
+ Slog.d(TAG, "updateValue, type=" + type.mName + "; value=" + value
+ + "; displayId=" + uniqueDisplayId);
Set<PluginManager.PluginChangeListener<T>> localListeners;
+ T valueToNotify;
synchronized (mLock) {
- mValues.put(type, value);
- mPluginEventStorage.onValueUpdated(type);
- ListenersContainer<T> container = getListenersContainerForTypeLocked(type);
- localListeners = new LinkedHashSet<>(container.mListeners);
+ ValuesContainer<T> valuesByType = getValuesContainerLocked(type);
+ valuesByType.updateValueLocked(uniqueDisplayId, value);
+ // if value was set to null, we might need to notify with GLOBAL value instead
+ valueToNotify = valuesByType.getValueLocked(uniqueDisplayId);
+
+ PluginEventStorage storage = mPluginEventStorages.computeIfAbsent(uniqueDisplayId,
+ d -> new PluginEventStorage());
+ storage.onValueUpdated(type);
+
+ localListeners = getListenersForUpdateLocked(type, uniqueDisplayId);
}
Slog.d(TAG, "updateValue, notifying listeners=" + localListeners);
- localListeners.forEach(l -> l.onChanged(value));
+ localListeners.forEach(l -> l.onChanged(valueToNotify));
+ }
+
+ @GuardedBy("mLock")
+ private <T> Set<PluginManager.PluginChangeListener<T>> getListenersForUpdateLocked(
+ PluginType<T> type, String uniqueDisplayId) {
+ ListenersContainer<T> listenersContainer = getListenersContainerLocked(type);
+ Set<PluginManager.PluginChangeListener<T>> localListeners = new LinkedHashSet<>();
+ // if GLOBAL value change we need to notify only listeners for displays that does not
+ // have display specific value
+ if (GLOBAL_ID.equals(uniqueDisplayId)) {
+ ValuesContainer<T> valuesContainer = getValuesContainerLocked(type);
+ Set<String> excludedDisplayIds = valuesContainer.getNonGlobalDisplaysLocked();
+ listenersContainer.mListeners.forEach((localDisplayId, listeners) -> {
+ if (!excludedDisplayIds.contains(localDisplayId)) {
+ localListeners.addAll(listeners);
+ }
+ });
+ } else {
+ localListeners.addAll(
+ listenersContainer.mListeners.getOrDefault(uniqueDisplayId, Set.of()));
+ }
+ return localListeners;
}
/**
* Adds listener for PluginType. If storage already has value for this type, listener will
* be notified immediately.
*/
- <T> void addListener(PluginType<T> type, PluginManager.PluginChangeListener<T> listener) {
+ <T> void addListener(PluginType<T> type, String uniqueDisplayId,
+ PluginManager.PluginChangeListener<T> listener) {
+ if (GLOBAL_ID.equals(uniqueDisplayId)) {
+ Slog.d(TAG, "addListener ignored for GLOBAL_ID, type=" + type.mName);
+ return;
+ }
T value = null;
synchronized (mLock) {
- ListenersContainer<T> container = getListenersContainerForTypeLocked(type);
- if (container.mListeners.add(listener)) {
- value = getValueForTypeLocked(type);
+ ListenersContainer<T> container = getListenersContainerLocked(type);
+ if (container.addListenerLocked(uniqueDisplayId, listener)) {
+ ValuesContainer<T> valuesContainer = getValuesContainerLocked(type);
+ value = valuesContainer.getValueLocked(uniqueDisplayId);
}
}
if (value != null) {
@@ -81,10 +139,15 @@ public class PluginStorage {
/**
* Removes listener
*/
- <T> void removeListener(PluginType<T> type, PluginManager.PluginChangeListener<T> listener) {
+ <T> void removeListener(PluginType<T> type, String uniqueDisplayId,
+ PluginManager.PluginChangeListener<T> listener) {
+ if (GLOBAL_ID.equals(uniqueDisplayId)) {
+ Slog.d(TAG, "removeListener ignored for GLOBAL_ID, type=" + type.mName);
+ return;
+ }
synchronized (mLock) {
- ListenersContainer<T> container = getListenersContainerForTypeLocked(type);
- container.mListeners.remove(listener);
+ ListenersContainer<T> container = getListenersContainerLocked(type);
+ container.removeListenerLocked(uniqueDisplayId, listener);
}
}
@@ -92,53 +155,106 @@ public class PluginStorage {
* Print the object's state and debug information into the given stream.
*/
void dump(PrintWriter pw) {
- Map<PluginType<?>, Object> localValues;
+ Map<PluginType<?>, Map<String, Object>> localValues = new HashMap<>();
@SuppressWarnings("rawtypes")
- Map<PluginType, Set> localListeners = new HashMap<>();
- List<PluginEventStorage.TimeFrame> timeFrames;
+ Map<PluginType, Map<String, Set>> localListeners = new HashMap<>();
+ Map<String, List<PluginEventStorage.TimeFrame>> timeFrames = new HashMap<>();
synchronized (mLock) {
- timeFrames = mPluginEventStorage.getTimeFrames();
- localValues = new HashMap<>(mValues);
- mListeners.forEach((type, container) -> localListeners.put(type, container.mListeners));
+ mPluginEventStorages.forEach((displayId, storage) -> {
+ timeFrames.put(displayId, storage.getTimeFrames());
+ });
+ mValues.forEach((type, valueContainer) -> {
+ localValues.put(type, new HashMap<>(valueContainer.mValues));
+ });
+ mListeners.forEach((type, container) -> {
+ localListeners.put(type, new HashMap<>(container.mListeners));
+ });
}
pw.println("PluginStorage:");
pw.println("values=" + localValues);
pw.println("listeners=" + localListeners);
pw.println("PluginEventStorage:");
- for (PluginEventStorage.TimeFrame timeFrame: timeFrames) {
- timeFrame.dump(pw);
+ for (Map.Entry<String, List<PluginEventStorage.TimeFrame>> timeFrameEntry :
+ timeFrames.entrySet()) {
+ pw.println("TimeFrames for displayId=" + timeFrameEntry.getKey());
+ for (PluginEventStorage.TimeFrame timeFrame : timeFrameEntry.getValue()) {
+ timeFrame.dump(pw);
+ }
}
}
@GuardedBy("mLock")
@SuppressWarnings("unchecked")
- private <T> T getValueForTypeLocked(PluginType<T> type) {
- Object value = mValues.get(type);
- if (value == null) {
- return null;
- } else if (type.mType == value.getClass()) {
- return (T) value;
+ private <T> ListenersContainer<T> getListenersContainerLocked(PluginType<T> type) {
+ ListenersContainer<?> container = mListeners.get(type);
+ if (container == null) {
+ ListenersContainer<T> lc = new ListenersContainer<>();
+ mListeners.put(type, lc);
+ return lc;
} else {
- Slog.d(TAG, "getValueForType: unexpected value type=" + value.getClass().getName()
- + ", expected=" + type.mType.getName());
- return null;
+ return (ListenersContainer<T>) container;
}
}
@GuardedBy("mLock")
@SuppressWarnings("unchecked")
- private <T> ListenersContainer<T> getListenersContainerForTypeLocked(PluginType<T> type) {
- ListenersContainer<?> container = mListeners.get(type);
+ private <T> ValuesContainer<T> getValuesContainerLocked(PluginType<T> type) {
+ ValuesContainer<?> container = mValues.get(type);
if (container == null) {
- ListenersContainer<T> lc = new ListenersContainer<>();
- mListeners.put(type, lc);
- return lc;
+ ValuesContainer<T> vc = new ValuesContainer<>();
+ mValues.put(type, vc);
+ return vc;
} else {
- return (ListenersContainer<T>) container;
+ return (ValuesContainer<T>) container;
}
}
private static final class ListenersContainer<T> {
- private final Set<PluginManager.PluginChangeListener<T>> mListeners = new LinkedHashSet<>();
+ private final Map<String, Set<PluginManager.PluginChangeListener<T>>> mListeners =
+ new LinkedHashMap<>();
+
+ private boolean addListenerLocked(
+ String uniqueDisplayId, PluginManager.PluginChangeListener<T> listener) {
+ Set<PluginManager.PluginChangeListener<T>> listenersForDisplay =
+ mListeners.computeIfAbsent(uniqueDisplayId, k -> new LinkedHashSet<>());
+ return listenersForDisplay.add(listener);
+ }
+
+ private void removeListenerLocked(String uniqueDisplayId,
+ PluginManager.PluginChangeListener<T> listener) {
+ Set<PluginManager.PluginChangeListener<T>> listenersForDisplay = mListeners.get(
+ uniqueDisplayId);
+ if (listenersForDisplay == null) {
+ return;
+ }
+
+ listenersForDisplay.remove(listener);
+
+ if (listenersForDisplay.isEmpty()) {
+ mListeners.remove(uniqueDisplayId);
+ }
+ }
+ }
+
+ private static final class ValuesContainer<T> {
+ private final Map<String, T> mValues = new HashMap<>();
+
+ private void updateValueLocked(String uniqueDisplayId, @Nullable T value) {
+ if (value == null) {
+ mValues.remove(uniqueDisplayId);
+ } else {
+ mValues.put(uniqueDisplayId, value);
+ }
+ }
+
+ private Set<String> getNonGlobalDisplaysLocked() {
+ Set<String> keys = new HashSet<>(mValues.keySet());
+ keys.remove(GLOBAL_ID);
+ return keys;
+ }
+
+ private @Nullable T getValueLocked(String displayId) {
+ return mValues.getOrDefault(displayId, mValues.get(GLOBAL_ID));
+ }
}
}
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index c0dbfa546a94..89f0d0edbf2b 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -710,7 +710,9 @@ public class HdmiControlService extends SystemService {
// Register ContentObserver to monitor the settings change.
registerContentObserver();
}
- mMhlController.setOption(OPTION_MHL_SERVICE_CONTROL, ENABLED);
+ if (mMhlController != null) {
+ mMhlController.setOption(OPTION_MHL_SERVICE_CONTROL, ENABLED);
+ }
}
@VisibleForTesting
diff --git a/services/core/java/com/android/server/input/InputGestureManager.java b/services/core/java/com/android/server/input/InputGestureManager.java
index fd755e3cefe2..32b36bfb50e5 100644
--- a/services/core/java/com/android/server/input/InputGestureManager.java
+++ b/services/core/java/com/android/server/input/InputGestureManager.java
@@ -23,7 +23,6 @@ import static com.android.hardware.input.Flags.enableVoiceAccessKeyGestures;
import static com.android.hardware.input.Flags.keyboardA11yShortcutControl;
import static com.android.server.flags.Flags.newBugreportKeyboardShortcut;
import static com.android.window.flags.Flags.enableMoveToNextDisplayShortcut;
-import static com.android.window.flags.Flags.enableTaskResizingKeyboardShortcuts;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -37,6 +36,7 @@ import android.os.SystemProperties;
import android.util.IndentingPrintWriter;
import android.util.SparseArray;
import android.view.KeyEvent;
+import android.window.DesktopModeFlags;
import com.android.internal.annotations.GuardedBy;
@@ -186,21 +186,11 @@ final class InputGestureManager {
KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT
),
createKeyGesture(
- KeyEvent.KEYCODE_DPAD_LEFT,
- KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON,
- KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT
- ),
- createKeyGesture(
KeyEvent.KEYCODE_DPAD_RIGHT,
KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON,
KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT
),
createKeyGesture(
- KeyEvent.KEYCODE_DPAD_RIGHT,
- KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON,
- KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT
- ),
- createKeyGesture(
KeyEvent.KEYCODE_SLASH,
KeyEvent.META_META_ON,
KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER
@@ -243,7 +233,7 @@ final class InputGestureManager {
KeyEvent.META_META_ON | KeyEvent.META_ALT_ON,
KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS));
}
- if (enableTaskResizingKeyboardShortcuts()) {
+ if (DesktopModeFlags.ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS.isTrue()) {
systemShortcuts.add(createKeyGesture(
KeyEvent.KEYCODE_LEFT_BRACKET,
KeyEvent.META_META_ON,
diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java
index 011659a616d3..3eb38a7029e6 100644
--- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java
+++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java
@@ -17,6 +17,7 @@
package com.android.server.media;
import static android.media.MediaRoute2Info.FEATURE_LIVE_AUDIO;
+import static android.media.MediaRoute2Info.FEATURE_LIVE_VIDEO;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -217,6 +218,28 @@ import java.util.stream.Stream;
}
}
+ @Override
+ public void setRouteVolume(long requestId, String routeOriginalId, int volume) {
+ synchronized (mLock) {
+ var targetProviderProxyId = mOriginalRouteIdToProviderId.get(routeOriginalId);
+ var targetProviderProxyRecord = mProxyRecords.get(targetProviderProxyId);
+ // Holds the target route, if it's managed by a provider service. Holds null otherwise.
+ if (targetProviderProxyRecord != null) {
+ var serviceTargetRoute =
+ targetProviderProxyRecord.mNewOriginalIdToSourceOriginalIdMap.get(
+ routeOriginalId);
+ if (serviceTargetRoute != null) {
+ targetProviderProxyRecord.mProxy.setRouteVolume(
+ requestId, serviceTargetRoute, volume);
+ } else {
+ notifyRequestFailed(
+ requestId, MediaRoute2ProviderService.REASON_ROUTE_NOT_AVAILABLE);
+ }
+ }
+ }
+ super.setRouteVolume(requestId, routeOriginalId, volume);
+ }
+
/**
* Returns the uid that corresponds to the given name and user handle, or {@link
* Process#INVALID_UID} if a uid couldn't be found.
@@ -463,11 +486,18 @@ import java.util.stream.Stream;
}
String id =
asSystemRouteId(providerInfo.getUniqueId(), sourceRoute.getOriginalId());
- var newRoute =
- new MediaRoute2Info.Builder(id, sourceRoute.getName())
- .addFeature(FEATURE_LIVE_AUDIO)
- .build();
- routesMap.put(id, newRoute);
+ var newRouteBuilder = new MediaRoute2Info.Builder(id, sourceRoute);
+ if ((sourceRoute.getSupportedRoutingTypes()
+ & MediaRoute2Info.FLAG_ROUTING_TYPE_SYSTEM_AUDIO)
+ != 0) {
+ newRouteBuilder.addFeature(FEATURE_LIVE_AUDIO);
+ }
+ if ((sourceRoute.getSupportedRoutingTypes()
+ & MediaRoute2Info.FLAG_ROUTING_TYPE_SYSTEM_VIDEO)
+ != 0) {
+ newRouteBuilder.addFeature(FEATURE_LIVE_VIDEO);
+ }
+ routesMap.put(id, newRouteBuilder.build());
idMap.put(id, sourceRoute.getOriginalId());
}
return new ProviderProxyRecord(
diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
index c428f39fd9d0..34a6cb951d46 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@@ -58,6 +58,7 @@ import android.media.projection.IMediaProjection;
import android.media.projection.IMediaProjectionCallback;
import android.media.projection.IMediaProjectionManager;
import android.media.projection.IMediaProjectionWatcherCallback;
+import android.media.projection.MediaProjectionEvent;
import android.media.projection.MediaProjectionInfo;
import android.media.projection.MediaProjectionManager;
import android.media.projection.ReviewGrantedConsentResult;
@@ -80,6 +81,7 @@ import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.DumpUtils;
+import com.android.media.projection.flags.Flags;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.Watchdog;
@@ -177,9 +179,31 @@ public final class MediaProjectionManagerService extends SystemService
private void maybeStopMediaProjection(int reason) {
synchronized (mLock) {
- if (!mMediaProjectionStopController.isExemptFromStopping(mProjectionGrant, reason)) {
- Slog.d(TAG, "Content Recording: Stopping MediaProjection due to "
- + MediaProjectionStopController.stopReasonToString(reason));
+ if (mMediaProjectionStopController.isExemptFromStopping(mProjectionGrant, reason)) {
+ return;
+ }
+
+ if (Flags.showStopDialogPostCallEnd()
+ && mMediaProjectionStopController.isStopReasonCallEnd(reason)) {
+ MediaProjectionEvent event =
+ new MediaProjectionEvent(
+ MediaProjectionEvent
+ .PROJECTION_STARTED_DURING_CALL_AND_ACTIVE_POST_CALL,
+ System.currentTimeMillis());
+ Slog.d(
+ TAG,
+ "Scheduling event: "
+ + event.getEventType()
+ + " for reason: "
+ + MediaProjectionStopController.stopReasonToString(reason));
+
+ // Post the PROJECTION_STARTED_DURING_CALL_AND_ACTIVE_POST_CALL event with a delay.
+ mHandler.postDelayed(() -> dispatchEvent(event), 500);
+ } else {
+ Slog.d(
+ TAG,
+ "Stopping MediaProjection due to reason: "
+ + MediaProjectionStopController.stopReasonToString(reason));
mProjectionGrant.stop(StopReason.STOP_DEVICE_LOCKED);
}
}
@@ -388,6 +412,24 @@ public final class MediaProjectionManagerService extends SystemService
mCallbackDelegate.dispatchSession(projectionInfo, session);
}
+ private void dispatchEvent(@NonNull MediaProjectionEvent event) {
+ if (!Flags.showStopDialogPostCallEnd()) {
+ Slog.d(
+ TAG,
+ "Event dispatch skipped. Reason: Flag showStopDialogPostCallEnd "
+ + "is disabled. Event details: "
+ + event);
+ return;
+ }
+ MediaProjectionInfo projectionInfo;
+ ContentRecordingSession session;
+ synchronized (mLock) {
+ projectionInfo = mProjectionGrant != null ? mProjectionGrant.getProjectionInfo() : null;
+ session = mProjectionGrant != null ? mProjectionGrant.mSession : null;
+ }
+ mCallbackDelegate.dispatchEvent(event, projectionInfo, session);
+ }
+
/**
* Returns {@code true} when updating the current mirroring session on WM succeeded, and
* {@code false} otherwise.
@@ -1467,6 +1509,25 @@ public final class MediaProjectionManagerService extends SystemService
}
}
+ private void dispatchEvent(
+ @NonNull MediaProjectionEvent event,
+ @Nullable MediaProjectionInfo info,
+ @Nullable ContentRecordingSession session) {
+ if (!Flags.showStopDialogPostCallEnd()) {
+ Slog.d(
+ TAG,
+ "Event dispatch skipped. Reason: Flag showStopDialogPostCallEnd "
+ + "is disabled. Event details: "
+ + event);
+ return;
+ }
+ synchronized (mLock) {
+ for (IMediaProjectionWatcherCallback callback : mWatcherCallbacks.values()) {
+ mHandler.post(new WatcherEventCallback(callback, event, info, session));
+ }
+ }
+ }
+
public void dispatchSession(
@NonNull MediaProjectionInfo projectionInfo,
@Nullable ContentRecordingSession session) {
@@ -1593,6 +1654,41 @@ public final class MediaProjectionManagerService extends SystemService
}
}
+ private static final class WatcherEventCallback implements Runnable {
+ private final IMediaProjectionWatcherCallback mCallback;
+ private final MediaProjectionEvent mEvent;
+ private final MediaProjectionInfo mProjectionInfo;
+ private final ContentRecordingSession mSession;
+
+ WatcherEventCallback(
+ @NonNull IMediaProjectionWatcherCallback callback,
+ @NonNull MediaProjectionEvent event,
+ @Nullable MediaProjectionInfo projectionInfo,
+ @Nullable ContentRecordingSession session) {
+ mCallback = callback;
+ mEvent = event;
+ mProjectionInfo = projectionInfo;
+ mSession = session;
+ }
+
+ @Override
+ public void run() {
+ if (!Flags.showStopDialogPostCallEnd()) {
+ Slog.d(
+ TAG,
+ "Not running WatcherEventCallback. Reason: Flag "
+ + "showStopDialogPostCallEnd is disabled. "
+ );
+ return;
+ }
+ try {
+ mCallback.onMediaProjectionEvent(mEvent, mProjectionInfo, mSession);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Failed to notify MediaProjectionEvent change", e);
+ }
+ }
+ }
+
private static final class WatcherSessionCallback implements Runnable {
private final IMediaProjectionWatcherCallback mCallback;
private final MediaProjectionInfo mProjectionInfo;
diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionStopController.java b/services/core/java/com/android/server/media/projection/MediaProjectionStopController.java
index c018e6bc1dc7..18f2f48b80a3 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionStopController.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionStopController.java
@@ -28,6 +28,7 @@ import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Binder;
import android.os.SystemClock;
+import android.os.UserHandle;
import android.provider.Settings;
import android.telecom.TelecomManager;
import android.telephony.TelephonyCallback;
@@ -38,6 +39,7 @@ import android.view.Display;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.SystemConfig;
+import java.util.List;
import java.util.function.Consumer;
/**
@@ -60,21 +62,35 @@ public class MediaProjectionStopController {
private final TelephonyManager mTelephonyManager;
private final AppOpsManager mAppOpsManager;
private final PackageManager mPackageManager;
- private final RoleManager mRoleManager;
+ private final RoleHolderProvider mRoleHolderProvider;
private final ContentResolver mContentResolver;
private boolean mIsInCall;
private long mLastCallStartTimeMillis;
+
+ @VisibleForTesting
+ interface RoleHolderProvider {
+ List<String> getRoleHoldersAsUser(String roleName, UserHandle user);
+ }
+
public MediaProjectionStopController(Context context, Consumer<Integer> stopReasonConsumer) {
+ this(context, stopReasonConsumer,
+ (roleName, user) -> context.getSystemService(RoleManager.class)
+ .getRoleHoldersAsUser(roleName, user));
+ }
+
+ @VisibleForTesting
+ MediaProjectionStopController(Context context, Consumer<Integer> stopReasonConsumer,
+ RoleHolderProvider roleHolderProvider) {
mStopReasonConsumer = stopReasonConsumer;
mKeyguardManager = context.getSystemService(KeyguardManager.class);
mTelecomManager = context.getSystemService(TelecomManager.class);
mTelephonyManager = context.getSystemService(TelephonyManager.class);
mAppOpsManager = context.getSystemService(AppOpsManager.class);
mPackageManager = context.getPackageManager();
- mRoleManager = context.getSystemService(RoleManager.class);
mContentResolver = context.getContentResolver();
+ mRoleHolderProvider = roleHolderProvider;
}
/**
@@ -95,6 +111,11 @@ public class MediaProjectionStopController {
}
}
+ /** Checks if the given stop reason corresponds to a call ending. */
+ public boolean isStopReasonCallEnd(int stopReason) {
+ return stopReason == STOP_REASON_CALL_END;
+ }
+
/**
* Checks whether the given projection grant is exempt from stopping restrictions.
*/
@@ -141,8 +162,9 @@ public class MediaProjectionStopController {
Slog.v(TAG, "Continuing MediaProjection for package with OP_PROJECT_MEDIA AppOp ");
return true;
}
- if (mRoleManager.getRoleHoldersAsUser(AssociationRequest.DEVICE_PROFILE_APP_STREAMING,
- projectionGrant.userHandle).contains(projectionGrant.packageName)) {
+ if (mRoleHolderProvider.getRoleHoldersAsUser(
+ AssociationRequest.DEVICE_PROFILE_APP_STREAMING, projectionGrant.userHandle)
+ .contains(projectionGrant.packageName)) {
Slog.v(TAG, "Continuing MediaProjection for package holding app streaming role.");
return true;
}
@@ -172,10 +194,6 @@ public class MediaProjectionStopController {
*/
public boolean isStartForbidden(
MediaProjectionManagerService.MediaProjection projectionGrant) {
- if (!android.companion.virtualdevice.flags.Flags.mediaProjectionKeyguardRestrictions()) {
- return false;
- }
-
if (!mKeyguardManager.isKeyguardLocked()) {
return false;
}
@@ -189,9 +207,6 @@ public class MediaProjectionStopController {
@VisibleForTesting
void onKeyguardLockedStateChanged(boolean isKeyguardLocked) {
if (!isKeyguardLocked) return;
- if (!android.companion.virtualdevice.flags.Flags.mediaProjectionKeyguardRestrictions()) {
- return;
- }
mStopReasonConsumer.accept(STOP_REASON_KEYGUARD);
}
diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java
index 2a5b779546d5..f6c94a7d9a5a 100644
--- a/services/core/java/com/android/server/media/quality/MediaQualityService.java
+++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java
@@ -29,11 +29,19 @@ import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.hardware.tv.mediaquality.AmbientBacklightColorFormat;
+import android.hardware.tv.mediaquality.DolbyAudioProcessing;
+import android.hardware.tv.mediaquality.DtsVirtualX;
import android.hardware.tv.mediaquality.IMediaQuality;
+import android.hardware.tv.mediaquality.IPictureProfileAdjustmentListener;
+import android.hardware.tv.mediaquality.IPictureProfileChangedListener;
+import android.hardware.tv.mediaquality.ISoundProfileAdjustmentListener;
+import android.hardware.tv.mediaquality.ISoundProfileChangedListener;
+import android.hardware.tv.mediaquality.ParamCapability;
import android.hardware.tv.mediaquality.PictureParameter;
import android.hardware.tv.mediaquality.PictureParameters;
import android.hardware.tv.mediaquality.SoundParameter;
import android.hardware.tv.mediaquality.SoundParameters;
+import android.hardware.tv.mediaquality.VendorParamCapability;
import android.media.quality.AmbientBacklightEvent;
import android.media.quality.AmbientBacklightMetadata;
import android.media.quality.AmbientBacklightSettings;
@@ -103,6 +111,10 @@ public class MediaQualityService extends SystemService {
private final BiMap<Long, String> mPictureProfileTempIdMap;
private final BiMap<Long, String> mSoundProfileTempIdMap;
private IMediaQuality mMediaQuality;
+ private IPictureProfileAdjustmentListener mPpAdjustmentListener;
+ private ISoundProfileAdjustmentListener mSpAdjustmentListener;
+ private IPictureProfileChangedListener mPpChangedListener;
+ private ISoundProfileChangedListener mSpChangedListener;
private final HalAmbientBacklightCallback mHalAmbientBacklightCallback;
private final Map<String, AmbientBacklightCallbackRecord> mCallbackRecords = new HashMap<>();
private final PackageManager mPackageManager;
@@ -138,18 +150,104 @@ public class MediaQualityService extends SystemService {
@Override
public void onStart() {
IBinder binder = ServiceManager.getService(IMediaQuality.DESCRIPTOR + "/default");
- if (binder != null) {
- Slogf.d(TAG, "binder is not null");
- mMediaQuality = IMediaQuality.Stub.asInterface(binder);
- if (mMediaQuality != null) {
- try {
- mMediaQuality.setAmbientBacklightCallback(mHalAmbientBacklightCallback);
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to set ambient backlight detector callback", e);
+ if (binder == null) {
+ Slogf.d(TAG, "Binder is null");
+ return;
+ }
+ Slogf.d(TAG, "Binder is not null");
+
+ mPpAdjustmentListener = new IPictureProfileAdjustmentListener.Stub() {
+ @Override
+ public void onPictureProfileAdjusted(
+ android.hardware.tv.mediaquality.PictureProfile pictureProfile)
+ throws RemoteException {
+ // TODO
+ }
+
+ @Override
+ public void onParamCapabilityChanged(long pictureProfileId, ParamCapability[] caps)
+ throws RemoteException {
+ // TODO
+ }
+
+ @Override
+ public void onVendorParamCapabilityChanged(long pictureProfileId,
+ VendorParamCapability[] caps) throws RemoteException {
+ // TODO
+ }
+
+ @Override
+ public void requestPictureParameters(long pictureProfileId) throws RemoteException {
+ // TODO
+ }
+
+ @Override
+ public void onStreamStatusChanged(long pictureProfileId, byte status)
+ throws RemoteException {
+ // TODO
+ }
+
+ @Override
+ public int getInterfaceVersion() throws RemoteException {
+ return 0;
+ }
+
+ @Override
+ public String getInterfaceHash() throws RemoteException {
+ return null;
}
+ };
+ mSpAdjustmentListener = new ISoundProfileAdjustmentListener.Stub() {
+
+ @Override
+ public void onSoundProfileAdjusted(
+ android.hardware.tv.mediaquality.SoundProfile soundProfile)
+ throws RemoteException {
+ // TODO
+ }
+
+ @Override
+ public void onParamCapabilityChanged(long soundProfileId, ParamCapability[] caps)
+ throws RemoteException {
+ // TODO
+ }
+
+ @Override
+ public void onVendorParamCapabilityChanged(long soundProfileId,
+ VendorParamCapability[] caps) throws RemoteException {
+ // TODO
+ }
+
+ @Override
+ public void requestSoundParameters(long soundProfileId) throws RemoteException {
+ // TODO
+ }
+
+ @Override
+ public int getInterfaceVersion() throws RemoteException {
+ return 0;
+ }
+
+ @Override
+ public String getInterfaceHash() throws RemoteException {
+ return null;
+ }
+ };
+
+ mMediaQuality = IMediaQuality.Stub.asInterface(binder);
+ if (mMediaQuality != null) {
+ try {
+ mMediaQuality.setAmbientBacklightCallback(mHalAmbientBacklightCallback);
+ mMediaQuality.setPictureProfileAdjustmentListener(mPpAdjustmentListener);
+ mMediaQuality.setSoundProfileAdjustmentListener(mSpAdjustmentListener);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to set ambient backlight detector callback", e);
}
}
+ mPpChangedListener = IPictureProfileChangedListener.Stub.asInterface(binder);
+ mSpChangedListener = ISoundProfileChangedListener.Stub.asInterface(binder);
+
publishBinderService(Context.MEDIA_QUALITY_SERVICE, new BinderService());
}
@@ -185,6 +283,30 @@ public class MediaQualityService extends SystemService {
return pp;
}
+ private void notifyHalOnPictureProfileChange(Long dbId, PersistableBundle params) {
+ // TODO: only notify HAL when the profile is active / being used
+ try {
+ mPpChangedListener.onPictureProfileChanged(convertToHalPictureProfile(dbId,
+ params));
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to notify HAL on picture profile change.", e);
+ }
+ }
+
+ private android.hardware.tv.mediaquality.PictureProfile convertToHalPictureProfile(Long id,
+ PersistableBundle params) {
+ PictureParameters pictureParameters = new PictureParameters();
+ pictureParameters.pictureParameters = convertPersistableBundleToPictureParameterList(
+ params);
+
+ android.hardware.tv.mediaquality.PictureProfile toReturn =
+ new android.hardware.tv.mediaquality.PictureProfile();
+ toReturn.pictureProfileId = id;
+ toReturn.parameters = pictureParameters;
+
+ return toReturn;
+ }
+
@Override
public void updatePictureProfile(String id, PictureProfile pp, UserHandle user) {
Long dbId = mPictureProfileTempIdMap.getKey(id);
@@ -205,6 +327,7 @@ public class MediaQualityService extends SystemService {
null, values);
notifyOnPictureProfileUpdated(mPictureProfileTempIdMap.getValue(dbId),
getPictureProfile(dbId), Binder.getCallingUid(), Binder.getCallingPid());
+ notifyHalOnPictureProfileChange(dbId, pp.getParameters());
}
private boolean hasPermissionToUpdatePictureProfile(Long dbId, PictureProfile toUpdate) {
@@ -238,6 +361,7 @@ public class MediaQualityService extends SystemService {
notifyOnPictureProfileRemoved(mPictureProfileTempIdMap.getValue(dbId), toDelete,
Binder.getCallingUid(), Binder.getCallingPid());
mPictureProfileTempIdMap.remove(dbId);
+ notifyHalOnPictureProfileChange(dbId, null);
}
}
@@ -357,6 +481,10 @@ public class MediaQualityService extends SystemService {
private PictureParameter[] convertPersistableBundleToPictureParameterList(
PersistableBundle params) {
+ if (params == null) {
+ return null;
+ }
+
List<PictureParameter> pictureParams = new ArrayList<>();
if (params.containsKey(PictureQuality.PARAMETER_BRIGHTNESS)) {
pictureParams.add(PictureParameter.brightness(params.getLong(
@@ -461,7 +589,7 @@ public class MediaQualityService extends SystemService {
}
if (params.containsKey(PictureQuality.PARAMETER_AUTO_SUPER_RESOLUTION_ENABLED)) {
pictureParams.add(PictureParameter.autoSuperResolutionEnabled(params.getBoolean(
- PictureQuality.PARAMETER_AUTO_SUPER_RESOLUTION_ENABLED)));
+ PictureQuality.PARAMETER_AUTO_SUPER_RESOLUTION_ENABLED)));
}
if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_RED_GAIN)) {
pictureParams.add(PictureParameter.colorTemperatureRedGain(params.getInt(
@@ -475,63 +603,210 @@ public class MediaQualityService extends SystemService {
pictureParams.add(PictureParameter.colorTemperatureBlueGain(params.getInt(
PictureQuality.PARAMETER_COLOR_TUNER_BLUE_GAIN)));
}
-
- /**
- * TODO: add conversion for following after adding to MediaQualityContract
- *
- * PictureParameter.levelRange
- * PictureParameter.gamutMapping
- * PictureParameter.pcMode
- * PictureParameter.lowLatency
- * PictureParameter.vrr
- * PictureParameter.cvrr
- * PictureParameter.hdmiRgbRange
- * PictureParameter.colorSpace
- * PictureParameter.panelInitMaxLuminceNits
- * PictureParameter.panelInitMaxLuminceValid
- * PictureParameter.gamma
- * PictureParameter.colorTemperatureRedOffset
- * PictureParameter.colorTemperatureGreenOffset
- * PictureParameter.colorTemperatureBlueOffset
- * PictureParameter.elevenPointRed
- * PictureParameter.elevenPointGreen
- * PictureParameter.elevenPointBlue
- * PictureParameter.lowBlueLight
- * PictureParameter.LdMode
- * PictureParameter.osdRedGain
- * PictureParameter.osdGreenGain
- * PictureParameter.osdBlueGain
- * PictureParameter.osdRedOffset
- * PictureParameter.osdGreenOffset
- * PictureParameter.osdBlueOffset
- * PictureParameter.osdHue
- * PictureParameter.osdSaturation
- * PictureParameter.osdContrast
- * PictureParameter.colorTunerSwitch
- * PictureParameter.colorTunerHueRed
- * PictureParameter.colorTunerHueGreen
- * PictureParameter.colorTunerHueBlue
- * PictureParameter.colorTunerHueCyan
- * PictureParameter.colorTunerHueMagenta
- * PictureParameter.colorTunerHueYellow
- * PictureParameter.colorTunerHueFlesh
- * PictureParameter.colorTunerSaturationRed
- * PictureParameter.colorTunerSaturationGreen
- * PictureParameter.colorTunerSaturationBlue
- * PictureParameter.colorTunerSaturationCyan
- * PictureParameter.colorTunerSaturationMagenta
- * PictureParameter.colorTunerSaturationYellow
- * PictureParameter.colorTunerSaturationFlesh
- * PictureParameter.colorTunerLuminanceRed
- * PictureParameter.colorTunerLuminanceGreen
- * PictureParameter.colorTunerLuminanceBlue
- * PictureParameter.colorTunerLuminanceCyan
- * PictureParameter.colorTunerLuminanceMagenta
- * PictureParameter.colorTunerLuminanceYellow
- * PictureParameter.colorTunerLuminanceFlesh
- * PictureParameter.activeProfile
- * PictureParameter.pictureQualityEventType
- */
+ if (params.containsKey(PictureQuality.PARAMETER_LEVEL_RANGE)) {
+ pictureParams.add(PictureParameter.levelRange(
+ (byte) params.getInt(PictureQuality.PARAMETER_LEVEL_RANGE)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_GAMUT_MAPPING)) {
+ pictureParams.add(PictureParameter.gamutMapping(params.getBoolean(
+ PictureQuality.PARAMETER_GAMUT_MAPPING)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_PC_MODE)) {
+ pictureParams.add(PictureParameter.pcMode(params.getBoolean(
+ PictureQuality.PARAMETER_PC_MODE)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_LOW_LATENCY)) {
+ pictureParams.add(PictureParameter.lowLatency(params.getBoolean(
+ PictureQuality.PARAMETER_LOW_LATENCY)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_VRR)) {
+ pictureParams.add(PictureParameter.vrr(params.getBoolean(
+ PictureQuality.PARAMETER_VRR)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_CVRR)) {
+ pictureParams.add(PictureParameter.cvrr(params.getBoolean(
+ PictureQuality.PARAMETER_CVRR)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_HDMI_RGB_RANGE)) {
+ pictureParams.add(PictureParameter.hdmiRgbRange(
+ (byte) params.getInt(PictureQuality.PARAMETER_HDMI_RGB_RANGE)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_COLOR_SPACE)) {
+ pictureParams.add(PictureParameter.colorSpace(
+ (byte) params.getInt(PictureQuality.PARAMETER_COLOR_SPACE)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_PANEL_INIT_MAX_LUMINCE_NITS)) {
+ pictureParams.add(PictureParameter.panelInitMaxLuminceNits(
+ params.getInt(PictureQuality.PARAMETER_PANEL_INIT_MAX_LUMINCE_NITS)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_PANEL_INIT_MAX_LUMINCE_VALID)) {
+ pictureParams.add(PictureParameter.panelInitMaxLuminceValid(
+ params.getBoolean(PictureQuality.PARAMETER_PANEL_INIT_MAX_LUMINCE_VALID)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_GAMMA)) {
+ pictureParams.add(PictureParameter.gamma(
+ (byte) params.getInt(PictureQuality.PARAMETER_GAMMA)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_COLOR_TEMPERATURE_RED_OFFSET)) {
+ pictureParams.add(PictureParameter.colorTemperatureRedOffset(params.getInt(
+ PictureQuality.PARAMETER_COLOR_TEMPERATURE_RED_OFFSET)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_COLOR_TEMPERATURE_GREEN_OFFSET)) {
+ pictureParams.add(PictureParameter.colorTemperatureGreenOffset(params.getInt(
+ PictureQuality.PARAMETER_COLOR_TEMPERATURE_GREEN_OFFSET)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_COLOR_TEMPERATURE_BLUE_OFFSET)) {
+ pictureParams.add(PictureParameter.colorTemperatureBlueOffset(params.getInt(
+ PictureQuality.PARAMETER_COLOR_TEMPERATURE_BLUE_OFFSET)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_ELEVEN_POINT_RED)) {
+ pictureParams.add(PictureParameter.elevenPointRed(params.getIntArray(
+ PictureQuality.PARAMETER_ELEVEN_POINT_RED)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_ELEVEN_POINT_GREEN)) {
+ pictureParams.add(PictureParameter.elevenPointGreen(params.getIntArray(
+ PictureQuality.PARAMETER_ELEVEN_POINT_GREEN)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_ELEVEN_POINT_BLUE)) {
+ pictureParams.add(PictureParameter.elevenPointBlue(params.getIntArray(
+ PictureQuality.PARAMETER_ELEVEN_POINT_BLUE)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_LOW_BLUE_LIGHT)) {
+ pictureParams.add(PictureParameter.lowBlueLight(
+ (byte) params.getInt(PictureQuality.PARAMETER_LOW_BLUE_LIGHT)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_LD_MODE)) {
+ pictureParams.add(PictureParameter.LdMode(
+ (byte) params.getInt(PictureQuality.PARAMETER_LD_MODE)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_OSD_RED_GAIN)) {
+ pictureParams.add(PictureParameter.osdRedGain(params.getInt(
+ PictureQuality.PARAMETER_OSD_RED_GAIN)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_OSD_GREEN_GAIN)) {
+ pictureParams.add(PictureParameter.osdGreenGain(params.getInt(
+ PictureQuality.PARAMETER_OSD_GREEN_GAIN)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_OSD_BLUE_GAIN)) {
+ pictureParams.add(PictureParameter.osdBlueGain(params.getInt(
+ PictureQuality.PARAMETER_OSD_BLUE_GAIN)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_OSD_RED_OFFSET)) {
+ pictureParams.add(PictureParameter.osdRedOffset(params.getInt(
+ PictureQuality.PARAMETER_OSD_RED_OFFSET)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_OSD_GREEN_OFFSET)) {
+ pictureParams.add(PictureParameter.osdGreenOffset(params.getInt(
+ PictureQuality.PARAMETER_OSD_GREEN_OFFSET)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_OSD_BLUE_OFFSET)) {
+ pictureParams.add(PictureParameter.osdBlueOffset(params.getInt(
+ PictureQuality.PARAMETER_OSD_BLUE_OFFSET)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_OSD_HUE)) {
+ pictureParams.add(PictureParameter.osdHue(params.getInt(
+ PictureQuality.PARAMETER_OSD_HUE)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_OSD_SATURATION)) {
+ pictureParams.add(PictureParameter.osdSaturation(params.getInt(
+ PictureQuality.PARAMETER_OSD_SATURATION)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_OSD_CONTRAST)) {
+ pictureParams.add(PictureParameter.osdContrast(params.getInt(
+ PictureQuality.PARAMETER_OSD_CONTRAST)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SWITCH)) {
+ pictureParams.add(PictureParameter.colorTunerSwitch(params.getBoolean(
+ PictureQuality.PARAMETER_COLOR_TUNER_SWITCH)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_HUE_RED)) {
+ pictureParams.add(PictureParameter.colorTunerHueRed(params.getInt(
+ PictureQuality.PARAMETER_COLOR_TUNER_HUE_RED)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_HUE_GREEN)) {
+ pictureParams.add(PictureParameter.colorTunerHueGreen(params.getInt(
+ PictureQuality.PARAMETER_COLOR_TUNER_HUE_GREEN)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_HUE_BLUE)) {
+ pictureParams.add(PictureParameter.colorTunerHueBlue(params.getInt(
+ PictureQuality.PARAMETER_COLOR_TUNER_HUE_BLUE)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_HUE_CYAN)) {
+ pictureParams.add(PictureParameter.colorTunerHueCyan(params.getInt(
+ PictureQuality.PARAMETER_COLOR_TUNER_HUE_CYAN)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_HUE_MAGENTA)) {
+ pictureParams.add(PictureParameter.colorTunerHueMagenta(params.getInt(
+ PictureQuality.PARAMETER_COLOR_TUNER_HUE_MAGENTA)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_HUE_YELLOW)) {
+ pictureParams.add(PictureParameter.colorTunerHueYellow(params.getInt(
+ PictureQuality.PARAMETER_COLOR_TUNER_HUE_YELLOW)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_HUE_FLESH)) {
+ pictureParams.add(PictureParameter.colorTunerHueFlesh(params.getInt(
+ PictureQuality.PARAMETER_COLOR_TUNER_HUE_FLESH)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_RED)) {
+ pictureParams.add(PictureParameter.colorTunerSaturationRed(params.getInt(
+ PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_RED)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_GREEN)) {
+ pictureParams.add(PictureParameter.colorTunerSaturationGreen(params.getInt(
+ PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_GREEN)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_BLUE)) {
+ pictureParams.add(PictureParameter.colorTunerSaturationBlue(params.getInt(
+ PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_BLUE)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_CYAN)) {
+ pictureParams.add(PictureParameter.colorTunerSaturationCyan(params.getInt(
+ PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_CYAN)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_MAGENTA)) {
+ pictureParams.add(PictureParameter.colorTunerSaturationMagenta(params.getInt(
+ PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_MAGENTA)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_YELLOW)) {
+ pictureParams.add(PictureParameter.colorTunerSaturationYellow(params.getInt(
+ PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_YELLOW)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_FLESH)) {
+ pictureParams.add(PictureParameter.colorTunerSaturationFlesh(params.getInt(
+ PictureQuality.PARAMETER_COLOR_TUNER_SATURATION_FLESH)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_RED)) {
+ pictureParams.add(PictureParameter.colorTunerLuminanceRed(params.getInt(
+ PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_RED)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_GREEN)) {
+ pictureParams.add(PictureParameter.colorTunerLuminanceGreen(params.getInt(
+ PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_GREEN)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_BLUE)) {
+ pictureParams.add(PictureParameter.colorTunerLuminanceBlue(params.getInt(
+ PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_BLUE)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_CYAN)) {
+ pictureParams.add(PictureParameter.colorTunerLuminanceCyan(params.getInt(
+ PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_CYAN)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_MAGENTA)) {
+ pictureParams.add(PictureParameter.colorTunerLuminanceMagenta(params.getInt(
+ PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_MAGENTA)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_YELLOW)) {
+ pictureParams.add(PictureParameter.colorTunerLuminanceYellow(params.getInt(
+ PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_YELLOW)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_FLESH)) {
+ pictureParams.add(PictureParameter.colorTunerLuminanceFlesh(params.getInt(
+ PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_FLESH)));
+ }
+ if (params.containsKey(PictureQuality.PARAMETER_PICTURE_QUALITY_EVENT_TYPE)) {
+ pictureParams.add(PictureParameter.pictureQualityEventType(
+ (byte) params.getInt(PictureQuality.PARAMETER_PICTURE_QUALITY_EVENT_TYPE)));
+ }
return (PictureParameter[]) pictureParams.toArray();
}
@@ -606,6 +881,28 @@ public class MediaQualityService extends SystemService {
return sp;
}
+ private void notifyHalOnSoundProfileChange(Long dbId, PersistableBundle params) {
+ // TODO: only notify HAL when the profile is active / being used
+ try {
+ mSpChangedListener.onSoundProfileChanged(convertToHalSoundProfile(dbId, params));
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to notify HAL on sound profile change.", e);
+ }
+ }
+
+ private android.hardware.tv.mediaquality.SoundProfile convertToHalSoundProfile(Long id,
+ PersistableBundle params) {
+ SoundParameters soundParameters = new SoundParameters();
+ soundParameters.soundParameters = convertPersistableBundleToSoundParameterList(params);
+
+ android.hardware.tv.mediaquality.SoundProfile toReturn =
+ new android.hardware.tv.mediaquality.SoundProfile();
+ toReturn.soundProfileId = id;
+ toReturn.parameters = soundParameters;
+
+ return toReturn;
+ }
+
@Override
public void updateSoundProfile(String id, SoundProfile sp, UserHandle user) {
Long dbId = mSoundProfileTempIdMap.getKey(id);
@@ -625,6 +922,7 @@ public class MediaQualityService extends SystemService {
db.replace(mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, null, values);
notifyOnSoundProfileUpdated(mSoundProfileTempIdMap.getValue(dbId),
getSoundProfile(dbId), Binder.getCallingUid(), Binder.getCallingPid());
+ notifyHalOnSoundProfileChange(dbId, sp.getParameters());
}
private boolean hasPermissionToUpdateSoundProfile(Long dbId, SoundProfile sp) {
@@ -657,6 +955,7 @@ public class MediaQualityService extends SystemService {
notifyOnSoundProfileRemoved(mSoundProfileTempIdMap.getValue(dbId), toDelete,
Binder.getCallingUid(), Binder.getCallingPid());
mSoundProfileTempIdMap.remove(dbId);
+ notifyHalOnSoundProfileChange(dbId, null);
}
}
@@ -775,6 +1074,10 @@ public class MediaQualityService extends SystemService {
private SoundParameter[] convertPersistableBundleToSoundParameterList(
PersistableBundle params) {
+ //TODO: set EqualizerDetail
+ if (params == null) {
+ return null;
+ }
List<SoundParameter> soundParams = new ArrayList<>();
if (params.containsKey(SoundQuality.PARAMETER_BALANCE)) {
soundParams.add(SoundParameter.balance(params.getInt(
@@ -811,15 +1114,50 @@ public class MediaQualityService extends SystemService {
soundParams.add(SoundParameter.surroundSoundEnabled(params.getBoolean(
SoundQuality.PARAMETER_DIGITAL_OUTPUT_DELAY_MILLIS)));
}
- //TODO: equalizerDetail
- //TODO: downmixMode
- //TODO: enhancedAudioReturnChannelEnabled
- //TODO: dolbyAudioProcessing
- //TODO: dolbyDialogueEnhancer
- //TODO: dtsVirtualX
- //TODO: digitalOutput
- //TODO: activeProfile
- //TODO: soundStyle
+ if (params.containsKey(SoundQuality.PARAMETER_EARC)) {
+ soundParams.add(SoundParameter.enhancedAudioReturnChannelEnabled(params.getBoolean(
+ SoundQuality.PARAMETER_EARC)));
+ }
+ if (params.containsKey(SoundQuality.PARAMETER_DOWN_MIX_MODE)) {
+ soundParams.add(SoundParameter.downmixMode((byte) params.getInt(
+ SoundQuality.PARAMETER_DOWN_MIX_MODE)));
+ }
+ if (params.containsKey(SoundQuality.PARAMETER_SOUND_STYLE)) {
+ soundParams.add(SoundParameter.soundStyle((byte) params.getInt(
+ SoundQuality.PARAMETER_SOUND_STYLE)));
+ }
+ if (params.containsKey(SoundQuality.PARAMETER_DIGITAL_OUTPUT_MODE)) {
+ soundParams.add(SoundParameter.digitalOutput((byte) params.getInt(
+ SoundQuality.PARAMETER_DIGITAL_OUTPUT_MODE)));
+ }
+ if (params.containsKey(SoundQuality.PARAMETER_DIALOGUE_ENHANCER)) {
+ soundParams.add(SoundParameter.dolbyDialogueEnhancer((byte) params.getInt(
+ SoundQuality.PARAMETER_DIALOGUE_ENHANCER)));
+ }
+
+ DolbyAudioProcessing dab = new DolbyAudioProcessing();
+ dab.soundMode =
+ (byte) params.getInt(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SOUND_MODE);
+ dab.volumeLeveler =
+ params.getBoolean(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_VOLUME_LEVELER);
+ dab.surroundVirtualizer = params.getBoolean(
+ SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SURROUND_VIRTUALIZER);
+ dab.dolbyAtmos =
+ params.getBoolean(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_DOLBY_ATMOS);
+ soundParams.add(SoundParameter.dolbyAudioProcessing(dab));
+
+ DtsVirtualX dts = new DtsVirtualX();
+ dts.tbHdx = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TBHDX);
+ dts.limiter = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_LIMITER);
+ dts.truSurroundX = params.getBoolean(
+ SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_SURROUND_X);
+ dts.truVolumeHd = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_VOLUME_HD);
+ dts.dialogClarity = params.getBoolean(
+ SoundQuality.PARAMETER_DTS_VIRTUAL_X_DIALOG_CLARITY);
+ dts.definition = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_DEFINITION);
+ dts.height = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_HEIGHT);
+ soundParams.add(SoundParameter.dtsVirtualX(dts));
+
return (SoundParameter[]) soundParams.toArray();
}
@@ -1472,7 +1810,13 @@ public class MediaQualityService extends SystemService {
RemoteCallbackList<IPictureProfileCallback> {
@Override
public void onCallbackDied(IPictureProfileCallback callback) {
- //todo
+ synchronized ("mPictureProfileLock") { //TODO: Change to lock
+ for (int i = 0; i < mUserStates.size(); i++) {
+ int userId = mUserStates.keyAt(i);
+ UserState userState = getOrCreateUserStateLocked(userId);
+ userState.mPictureProfileCallbackPidUidMap.remove(callback);
+ }
+ }
}
}
@@ -1480,7 +1824,13 @@ public class MediaQualityService extends SystemService {
RemoteCallbackList<ISoundProfileCallback> {
@Override
public void onCallbackDied(ISoundProfileCallback callback) {
- //todo
+ synchronized ("mSoundProfileLock") { //TODO: Change to lock
+ for (int i = 0; i < mUserStates.size(); i++) {
+ int userId = mUserStates.keyAt(i);
+ UserState userState = getOrCreateUserStateLocked(userId);
+ userState.mSoundProfileCallbackPidUidMap.remove(callback);
+ }
+ }
}
}
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
index 7de2815eba6b..1d376b4bdfd5 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -3612,13 +3612,16 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
final long token = Binder.clearCallingIdentity();
try {
config = mCarrierConfigManager.getConfigForSubId(subId);
- tm = mContext.getSystemService(TelephonyManager.class);
+ tm = mContext.getSystemService(TelephonyManager.class).createForSubscriptionId(subId);
} finally {
Binder.restoreCallingIdentity(token);
}
- // First check: does caller have carrier privilege?
- if (tm != null && tm.hasCarrierPrivileges(subId)) {
+ // First check: does callingPackage have carrier privilege?
+ // Note that we can't call TelephonyManager.hasCarrierPrivileges() which will check if
+ // ourself has carrier privileges
+ if (tm != null && (tm.checkCarrierPrivilegesForPackage(callingPackage)
+ == TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS)) {
return;
}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index daa1042bb255..b4a58ac72394 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -481,7 +481,8 @@ public class NotificationManagerService extends SystemService {
Adjustment.KEY_SENSITIVE_CONTENT,
Adjustment.KEY_RANKING_SCORE,
Adjustment.KEY_NOT_CONVERSATION,
- Adjustment.KEY_TYPE
+ Adjustment.KEY_TYPE,
+ Adjustment.KEY_SUMMARIZATION
};
static final Integer[] DEFAULT_ALLOWED_ADJUSTMENT_KEY_TYPES = new Integer[] {
@@ -631,6 +632,8 @@ public class NotificationManagerService extends SystemService {
// Minium number of sparse groups for a package before autogrouping them
private static final int AUTOGROUP_SPARSE_GROUPS_AT_COUNT = 3;
+ private static final Duration ZEN_BROADCAST_DELAY = Duration.ofMillis(250);
+
private IActivityManager mAm;
private ActivityTaskManagerInternal mAtm;
private ActivityManager mActivityManager;
@@ -3169,6 +3172,24 @@ public class NotificationManagerService extends SystemService {
sendRegisteredOnlyBroadcast(new Intent(action));
}
+ /**
+ * Schedules a broadcast to be sent to runtime receivers and DND-policy-access packages. The
+ * broadcast will be sent after {@link #ZEN_BROADCAST_DELAY}, unless a new broadcast is
+ * scheduled in the interim, in which case the previous one is dropped and the waiting period
+ * is <em>restarted</em>.
+ *
+ * <p>Note that this uses <em>equality of the {@link Intent#getAction}</em> as the criteria for
+ * deduplicating pending broadcasts, ignoring the extras and anything else. This is intentional
+ * so that e.g. rapidly changing some value A -> B -> C will only produce a broadcast for C
+ * (instead of every time because the extras are different).
+ */
+ private void sendZenBroadcastWithDelay(Intent intent) {
+ String token = "zen_broadcast:" + intent.getAction();
+ mHandler.removeCallbacksAndEqualMessages(token);
+ mHandler.postDelayed(() -> sendRegisteredOnlyBroadcast(intent), token,
+ ZEN_BROADCAST_DELAY.toMillis());
+ }
+
private void sendRegisteredOnlyBroadcast(Intent baseIntent) {
int[] userIds = mUmInternal.getProfileIds(mAmi.getCurrentUserId(), true);
if (Flags.nmBinderPerfReduceZenBroadcasts()) {
@@ -3362,14 +3383,25 @@ public class NotificationManagerService extends SystemService {
@GuardedBy("mNotificationLock")
private void updateEffectsSuppressorLocked() {
+ final long oldSuppressedEffects = mZenModeHelper.getSuppressedEffects();
final long updatedSuppressedEffects = calculateSuppressedEffects();
- if (updatedSuppressedEffects == mZenModeHelper.getSuppressedEffects()) return;
+ if (updatedSuppressedEffects == oldSuppressedEffects) return;
+
final List<ComponentName> suppressors = getSuppressors();
ZenLog.traceEffectsSuppressorChanged(
- mEffectsSuppressors, suppressors, updatedSuppressedEffects);
- mEffectsSuppressors = suppressors;
+ mEffectsSuppressors, suppressors, oldSuppressedEffects, updatedSuppressedEffects);
mZenModeHelper.setSuppressedEffects(updatedSuppressedEffects);
- sendRegisteredOnlyBroadcast(NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED);
+
+ if (Flags.nmBinderPerfThrottleEffectsSuppressorBroadcast()) {
+ if (!suppressors.equals(mEffectsSuppressors)) {
+ mEffectsSuppressors = suppressors;
+ sendZenBroadcastWithDelay(
+ new Intent(NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED));
+ }
+ } else {
+ mEffectsSuppressors = suppressors;
+ sendRegisteredOnlyBroadcast(NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED);
+ }
}
private void exitIdle() {
@@ -3491,12 +3523,18 @@ public class NotificationManagerService extends SystemService {
}
private ArrayList<ComponentName> getSuppressors() {
- ArrayList<ComponentName> names = new ArrayList<ComponentName>();
+ ArrayList<ComponentName> names = new ArrayList<>();
for (int i = mListenersDisablingEffects.size() - 1; i >= 0; --i) {
ArraySet<ComponentName> serviceInfoList = mListenersDisablingEffects.valueAt(i);
for (ComponentName info : serviceInfoList) {
- names.add(info);
+ if (Flags.nmBinderPerfThrottleEffectsSuppressorBroadcast()) {
+ if (!names.contains(info)) {
+ names.add(info);
+ }
+ } else {
+ names.add(info);
+ }
}
}
@@ -5018,14 +5056,8 @@ public class NotificationManagerService extends SystemService {
}
@Override
- public ParceledListSlice<NotificationChannel> getNotificationChannels(
- String callingPkg, String targetPkg, int userId) {
- return getOrCreateNotificationChannels(callingPkg, targetPkg, userId, false);
- }
-
- @Override
- public ParceledListSlice<NotificationChannel> getOrCreateNotificationChannels(
- String callingPkg, String targetPkg, int userId, boolean createPrefsIfNeeded) {
+ public ParceledListSlice<NotificationChannel> getNotificationChannels(String callingPkg,
+ String targetPkg, int userId) {
if (canNotifyAsPackage(callingPkg, targetPkg, userId)
|| isCallingUidSystem()) {
int targetUid = -1;
@@ -5035,8 +5067,7 @@ public class NotificationManagerService extends SystemService {
/* ignore */
}
return mPreferencesHelper.getNotificationChannels(
- targetPkg, targetUid, false /* includeDeleted */, true,
- createPrefsIfNeeded);
+ targetPkg, targetUid, false /* includeDeleted */, true);
}
throw new SecurityException("Pkg " + callingPkg
+ " cannot read channels for " + targetPkg + " in " + userId);
@@ -7212,7 +7243,7 @@ public class NotificationManagerService extends SystemService {
if (!mAssistants.isAdjustmentAllowed(potentialKey)) {
toRemove.add(potentialKey);
}
- if (notificationClassification() && adjustments.containsKey(KEY_TYPE)) {
+ if (notificationClassification() && potentialKey.equals(KEY_TYPE)) {
mAssistants.setNasUnsupportedDefaults(r.getSbn().getNormalizedUserId());
if (!mAssistants.isAdjustmentKeyTypeAllowed(adjustments.getInt(KEY_TYPE))) {
toRemove.add(potentialKey);
@@ -8467,6 +8498,9 @@ public class NotificationManagerService extends SystemService {
(userId == USER_ALL) ? USER_SYSTEM : userId);
Notification.addFieldsFromContext(ai, notification);
+ // can't be set by an app
+ notification.extras.remove(Notification.EXTRA_SUMMARIZED_CONTENT);
+
if (notification.isForegroundService() && fgsPolicy == NOT_FOREGROUND_SERVICE) {
notification.flags &= ~FLAG_FOREGROUND_SERVICE;
}
@@ -10395,7 +10429,8 @@ public class NotificationManagerService extends SystemService {
r.getRankingScore(),
r.isConversation(),
r.getProposedImportance(),
- r.hasSensitiveContent());
+ r.hasSensitiveContent(),
+ r.getSummarization());
extractorDataBefore.put(r.getKey(), extractorData);
mRankingHelper.extractSignals(r);
}
@@ -11715,7 +11750,8 @@ public class NotificationManagerService extends SystemService {
: (record.getRankingScore() > 0 ? RANKING_PROMOTED : RANKING_DEMOTED),
record.getNotification().isBubbleNotification(),
record.getProposedImportance(),
- hasSensitiveContent
+ hasSensitiveContent,
+ record.getSummarization()
);
rankings.add(ranking);
}
diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java
index 81af0d8a6d80..52101e336920 100644
--- a/services/core/java/com/android/server/notification/NotificationRecord.java
+++ b/services/core/java/com/android/server/notification/NotificationRecord.java
@@ -25,6 +25,7 @@ import static android.app.NotificationManager.IMPORTANCE_HIGH;
import static android.app.NotificationManager.IMPORTANCE_LOW;
import static android.app.NotificationManager.IMPORTANCE_MIN;
import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED;
+import static android.service.notification.Adjustment.KEY_SUMMARIZATION;
import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEUTRAL;
import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_POSITIVE;
@@ -225,6 +226,8 @@ public final class NotificationRecord {
// type of the bundle if the notification was classified
private @Adjustment.Types int mBundleType = Adjustment.TYPE_OTHER;
+ private String mSummarization = null;
+
public NotificationRecord(Context context, StatusBarNotification sbn,
NotificationChannel channel) {
this.sbn = sbn;
@@ -589,6 +592,7 @@ public final class NotificationRecord {
pw.println(prefix + "shortcut=" + notification.getShortcutId()
+ " found valid? " + (mShortcutInfo != null));
pw.println(prefix + "mUserVisOverride=" + getPackageVisibilityOverride());
+ pw.println(prefix + "hasSummarization=" + (mSummarization != null));
}
private void dumpNotification(PrintWriter pw, String prefix, Notification notification,
@@ -811,6 +815,12 @@ public final class NotificationRecord {
Adjustment.KEY_TYPE,
mChannel.getId());
}
+ if ((android.app.Flags.nmSummarizationUi() || android.app.Flags.nmSummarization())
+ && signals.containsKey(KEY_SUMMARIZATION)) {
+ mSummarization = signals.getString(KEY_SUMMARIZATION);
+ EventLogTags.writeNotificationAdjusted(getKey(),
+ KEY_SUMMARIZATION, Boolean.toString(mSummarization != null));
+ }
if (!signals.isEmpty() && adjustment.getIssuer() != null) {
mAdjustmentIssuer = adjustment.getIssuer();
}
@@ -983,6 +993,13 @@ public final class NotificationRecord {
return null;
}
+ public String getSummarization() {
+ if ((android.app.Flags.nmSummarizationUi() || android.app.Flags.nmSummarization())) {
+ return mSummarization;
+ }
+ return null;
+ }
+
public boolean setIntercepted(boolean intercept) {
mIntercept = intercept;
mInterceptSet = true;
diff --git a/services/core/java/com/android/server/notification/NotificationRecordExtractorData.java b/services/core/java/com/android/server/notification/NotificationRecordExtractorData.java
index 3f4f7d3bbc38..9315ddc0d5b0 100644
--- a/services/core/java/com/android/server/notification/NotificationRecordExtractorData.java
+++ b/services/core/java/com/android/server/notification/NotificationRecordExtractorData.java
@@ -47,6 +47,7 @@ public final class NotificationRecordExtractorData {
private final boolean mIsConversation;
private final int mProposedImportance;
private final boolean mSensitiveContent;
+ private final String mSummarization;
NotificationRecordExtractorData(int position, int visibility, boolean showBadge,
boolean allowBubble, boolean isBubble, NotificationChannel channel, String groupKey,
@@ -54,7 +55,8 @@ public final class NotificationRecordExtractorData {
Integer userSentiment, Integer suppressVisually,
ArrayList<Notification.Action> systemSmartActions,
ArrayList<CharSequence> smartReplies, int importance, float rankingScore,
- boolean isConversation, int proposedImportance, boolean sensitiveContent) {
+ boolean isConversation, int proposedImportance, boolean sensitiveContent,
+ String summarization) {
mPosition = position;
mVisibility = visibility;
mShowBadge = showBadge;
@@ -73,6 +75,7 @@ public final class NotificationRecordExtractorData {
mIsConversation = isConversation;
mProposedImportance = proposedImportance;
mSensitiveContent = sensitiveContent;
+ mSummarization = summarization;
}
// Returns whether the provided NotificationRecord differs from the cached data in any way.
@@ -93,7 +96,8 @@ public final class NotificationRecordExtractorData {
|| !Objects.equals(mSmartReplies, r.getSmartReplies())
|| mImportance != r.getImportance()
|| mProposedImportance != r.getProposedImportance()
- || mSensitiveContent != r.hasSensitiveContent();
+ || mSensitiveContent != r.hasSensitiveContent()
+ || !Objects.equals(mSummarization, r.getSummarization());
}
// Returns whether the NotificationRecord has a change from this data for which we should
@@ -117,6 +121,7 @@ public final class NotificationRecordExtractorData {
|| !r.rankingScoreMatches(mRankingScore)
|| mIsConversation != r.isConversation()
|| mProposedImportance != r.getProposedImportance()
- || mSensitiveContent != r.hasSensitiveContent();
+ || mSensitiveContent != r.hasSensitiveContent()
+ || !Objects.equals(mSummarization, r.getSummarization());
}
}
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 3b34dcd17705..14cc91b6305f 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -16,8 +16,8 @@
package com.android.server.notification;
-import static android.app.Flags.notificationClassificationUi;
import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW;
+import static android.app.Flags.notificationClassificationUi;
import static android.app.NotificationChannel.DEFAULT_CHANNEL_ID;
import static android.app.NotificationChannel.NEWS_ID;
import static android.app.NotificationChannel.PLACEHOLDER_CONVERSATION_ID;
@@ -89,9 +89,10 @@ import android.util.SparseBooleanArray;
import android.util.StatsEvent;
import android.util.proto.ProtoOutputStream;
+import androidx.annotation.VisibleForTesting;
+
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
-import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags;
import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags;
import com.android.internal.logging.MetricsLogger;
@@ -1962,21 +1963,11 @@ public class PreferencesHelper implements RankingConfig {
@Override
public ParceledListSlice<NotificationChannel> getNotificationChannels(String pkg, int uid,
boolean includeDeleted, boolean includeBundles) {
- return getNotificationChannels(pkg, uid, includeDeleted, includeBundles, false);
- }
-
- protected ParceledListSlice<NotificationChannel> getNotificationChannels(String pkg, int uid,
- boolean includeDeleted, boolean includeBundles, boolean createPrefsIfNeeded) {
- if (createPrefsIfNeeded && !android.app.Flags.nmBinderPerfCacheChannels()) {
- Slog.wtf(TAG,
- "getNotificationChannels called with createPrefsIfNeeded=true and flag off");
- createPrefsIfNeeded = false;
- }
Objects.requireNonNull(pkg);
List<NotificationChannel> channels = new ArrayList<>();
synchronized (mLock) {
PackagePreferences r;
- if (createPrefsIfNeeded) {
+ if (android.app.Flags.nmBinderPerfCacheChannels()) {
r = getOrCreatePackagePreferencesLocked(pkg, uid);
} else {
r = getPackagePreferencesLocked(pkg, uid);
@@ -1997,6 +1988,18 @@ public class PreferencesHelper implements RankingConfig {
}
}
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ // Gets the entire list of notification channels for this package, with no filtering and
+ // without creating package preferences. For testing only, specifically to confirm the
+ // notification channels of a removed/deleted package.
+ protected List<NotificationChannel> getRemovedPkgNotificationChannels(String pkg, int uid) {
+ PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
+ if (r == null || r.channels == null) {
+ return new ArrayList<>();
+ }
+ return new ArrayList<>(r.channels.values());
+ }
+
/**
* Gets all notification channels associated with the given pkg and uid that can bypass dnd
*/
diff --git a/services/core/java/com/android/server/notification/ZenLog.java b/services/core/java/com/android/server/notification/ZenLog.java
index 7e853d9d2d0b..49f93b8b7c16 100644
--- a/services/core/java/com/android/server/notification/ZenLog.java
+++ b/services/core/java/com/android/server/notification/ZenLog.java
@@ -140,8 +140,9 @@ public class ZenLog {
}
public static void traceEffectsSuppressorChanged(List<ComponentName> oldSuppressors,
- List<ComponentName> newSuppressors, long suppressedEffects) {
- append(TYPE_SUPPRESSOR_CHANGED, "suppressed effects:" + suppressedEffects + ","
+ List<ComponentName> newSuppressors, long oldSuppressedEffects, long suppressedEffects) {
+ append(TYPE_SUPPRESSOR_CHANGED, "suppressed effects:"
+ + oldSuppressedEffects + "->" + suppressedEffects + ","
+ componentListToString(oldSuppressors) + "->"
+ componentListToString(newSuppressors));
}
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index 822ff48c831c..048f2b6b0cbc 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -182,6 +182,16 @@ flag {
}
flag {
+ name: "nm_binder_perf_throttle_effects_suppressor_broadcast"
+ namespace: "systemui"
+ description: "Delay sending the ACTION_EFFECTS_SUPPRESSOR_CHANGED broadcast if it changes too often"
+ bug: "371776935"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "fix_calling_uid_from_cps"
namespace: "systemui"
description: "Correctly checks zen rule ownership when a CPS notifies with a Condition"
diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java
index 0b58c759b284..f011d283c8bb 100644
--- a/services/core/java/com/android/server/pm/DexOptHelper.java
+++ b/services/core/java/com/android/server/pm/DexOptHelper.java
@@ -96,6 +96,9 @@ import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
@@ -105,6 +108,11 @@ import java.util.function.Predicate;
public final class DexOptHelper {
private static final long SEVEN_DAYS_IN_MILLISECONDS = 7 * 24 * 60 * 60 * 1000;
+ @NonNull
+ private static final ThreadPoolExecutor sDexoptExecutor =
+ new ThreadPoolExecutor(1 /* corePoolSize */, 1 /* maximumPoolSize */,
+ 60 /* keepAliveTime */, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
+
private static boolean sArtManagerLocalIsInitialized = false;
private final PackageManagerService mPm;
@@ -113,6 +121,11 @@ public final class DexOptHelper {
// used, to make it available to the onDexoptDone callback.
private volatile long mBootDexoptStartTime;
+ static {
+ // Recycle the thread if it's not used for `keepAliveTime`.
+ sDexoptExecutor.allowsCoreThreadTimeOut();
+ }
+
DexOptHelper(PackageManagerService pm) {
mPm = pm;
}
@@ -746,43 +759,11 @@ public final class DexOptHelper {
*/
static void performDexoptIfNeeded(InstallRequest installRequest, DexManager dexManager,
Context context, PackageManagerTracedLock.RawLock installLock) {
-
// Construct the DexoptOptions early to see if we should skip running dexopt.
- //
- // Do not run PackageDexOptimizer through the local performDexOpt
- // method because `pkg` may not be in `mPackages` yet.
- //
- // Also, don't fail application installs if the dexopt step fails.
DexoptOptions dexoptOptions = getDexoptOptionsByInstallRequest(installRequest, dexManager);
- // Check whether we need to dexopt the app.
- //
- // NOTE: it is IMPORTANT to call dexopt:
- // - after doRename which will sync the package data from AndroidPackage and
- // its corresponding ApplicationInfo.
- // - after installNewPackageLIF or replacePackageLIF which will update result with the
- // uid of the application (pkg.applicationInfo.uid).
- // This update happens in place!
- //
- // We only need to dexopt if the package meets ALL of the following conditions:
- // 1) it is not an instant app or if it is then dexopt is enabled via gservices.
- // 2) it is not debuggable.
- // 3) it is not on Incremental File System.
- //
- // Note that we do not dexopt instant apps by default. dexopt can take some time to
- // complete, so we skip this step during installation. Instead, we'll take extra time
- // the first time the instant app starts. It's preferred to do it this way to provide
- // continuous progress to the useur instead of mysteriously blocking somewhere in the
- // middle of running an instant app. The default behaviour can be overridden
- // via gservices.
- //
- // Furthermore, dexopt may be skipped, depending on the install scenario and current
- // state of the device.
- //
- // TODO(b/174695087): instantApp and onIncremental should be removed and their install
- // path moved to SCENARIO_FAST.
+ boolean performDexopt =
+ DexOptHelper.shouldPerformDexopt(installRequest, dexoptOptions, context);
- final boolean performDexopt = DexOptHelper.shouldPerformDexopt(installRequest,
- dexoptOptions, context);
if (performDexopt) {
// dexopt can take long, and ArtService doesn't require installd, so we release
// the lock here and re-acquire the lock after dexopt is finished.
@@ -791,6 +772,7 @@ public final class DexOptHelper {
}
try {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "dexopt");
+ // Don't fail application installs if the dexopt step fails.
DexoptResult dexOptResult = DexOptHelper.dexoptPackageUsingArtService(
installRequest, dexoptOptions);
installRequest.onDexoptFinished(dexOptResult);
@@ -804,6 +786,41 @@ public final class DexOptHelper {
}
/**
+ * Same as above, but runs asynchronously.
+ */
+ static CompletableFuture<Void> performDexoptIfNeededAsync(InstallRequest installRequest,
+ DexManager dexManager, Context context) {
+ // Construct the DexoptOptions early to see if we should skip running dexopt.
+ DexoptOptions dexoptOptions = getDexoptOptionsByInstallRequest(installRequest, dexManager);
+ boolean performDexopt =
+ DexOptHelper.shouldPerformDexopt(installRequest, dexoptOptions, context);
+
+ if (performDexopt) {
+ return CompletableFuture
+ .runAsync(() -> {
+ try {
+ Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "dexopt");
+ // Don't fail application installs if the dexopt step fails.
+ // TODO(jiakaiz): Make this async in ART Service.
+ DexoptResult dexOptResult = DexOptHelper.dexoptPackageUsingArtService(
+ installRequest, dexoptOptions);
+ installRequest.onDexoptFinished(dexOptResult);
+ } finally {
+ Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
+ }
+ }, sDexoptExecutor)
+ .exceptionally((t) -> {
+ // This should never happen. A normal dexopt failure should result in a
+ // DexoptResult.DEXOPT_FAILED, not an exception.
+ Slog.wtf(TAG, "Dexopt encountered a fatal error", t);
+ return null;
+ });
+ } else {
+ return CompletableFuture.completedFuture(null);
+ }
+ }
+
+ /**
* Use ArtService to perform dexopt by the given InstallRequest.
*/
static DexoptResult dexoptPackageUsingArtService(InstallRequest installRequest,
@@ -840,6 +857,20 @@ public final class DexOptHelper {
*/
static boolean shouldPerformDexopt(InstallRequest installRequest, DexoptOptions dexoptOptions,
Context context) {
+ // We only need to dexopt if the package meets ALL of the following conditions:
+ // 1) it is not an instant app or if it is then dexopt is enabled via gservices.
+ // 2) it is not debuggable.
+ // 3) it is not on Incremental File System.
+ //
+ // Note that we do not dexopt instant apps by default. dexopt can take some time to
+ // complete, so we skip this step during installation. Instead, we'll take extra time
+ // the first time the instant app starts. It's preferred to do it this way to provide
+ // continuous progress to the user instead of mysteriously blocking somewhere in the
+ // middle of running an instant app. The default behaviour can be overridden
+ // via gservices.
+ //
+ // Furthermore, dexopt may be skipped, depending on the install scenario and current
+ // state of the device.
final boolean isApex = ((installRequest.getScanFlags() & SCAN_AS_APEX) != 0);
final boolean instantApp = ((installRequest.getScanFlags() & SCAN_AS_INSTANT_APP) != 0);
final PackageSetting ps = installRequest.getScannedPackageSetting();
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 8eb5b6f11cb2..0c2782393879 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -1158,6 +1158,7 @@ final class InstallPackageHelper {
return;
}
request.setKeepArtProfile(true);
+ // TODO(b/388159696): Use performDexoptIfNeededAsync.
DexOptHelper.performDexoptIfNeeded(request, mDexManager, mContext, null);
}
}
diff --git a/services/core/java/com/android/server/pm/UserManagerInternal.java b/services/core/java/com/android/server/pm/UserManagerInternal.java
index 14b0fc81fdd2..c62aaebf673b 100644
--- a/services/core/java/com/android/server/pm/UserManagerInternal.java
+++ b/services/core/java/com/android/server/pm/UserManagerInternal.java
@@ -584,6 +584,12 @@ public abstract class UserManagerInternal {
* Returns the user id of the main user, or {@link android.os.UserHandle#USER_NULL} if there is
* no main user.
*
+ * <p>NB: Features should ideally not limit functionality to the main user. Ideally, they
+ * should either work for all users or for all admin users. If a feature should only work for
+ * select users, its determination of which user should be done intelligently or be
+ * customizable. Not all devices support a main user, and the idea of singling out one user as
+ * special is contrary to overall multiuser goals.
+ *
* @see UserManager#isMainUser()
*/
public abstract @UserIdInt int getMainUserId();
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 57ccab027c29..8cbccf5feead 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -2922,7 +2922,16 @@ public class UserManagerService extends IUserManager.Stub {
* switchable.
*/
public @UserManager.UserSwitchabilityResult int getUserSwitchability(int userId) {
- checkManageOrInteractPermissionIfCallerInOtherProfileGroup(userId, "getUserSwitchability");
+ if (Flags.getUserSwitchabilityPermission()) {
+ if (!hasManageUsersOrPermission(android.Manifest.permission.INTERACT_ACROSS_USERS)) {
+ throw new SecurityException(
+ "You need MANAGE_USERS or INTERACT_ACROSS_USERS permission to "
+ + "getUserSwitchability");
+ }
+ } else {
+ checkManageOrInteractPermissionIfCallerInOtherProfileGroup(userId,
+ "getUserSwitchability");
+ }
final TimingsTraceAndSlog t = new TimingsTraceAndSlog();
t.traceBegin("getUserSwitchability-" + userId);
@@ -3581,8 +3590,6 @@ public class UserManagerService extends IUserManager.Stub {
}
/**
- * @hide
- *
* Returns who set a user restriction on a user.
* Requires {@link android.Manifest.permission#MANAGE_USERS} permission.
* @param restrictionKey the string key representing the restriction
@@ -6266,9 +6273,6 @@ public class UserManagerService extends IUserManager.Stub {
}
}
- /**
- * @hide
- */
@Override
public @NonNull UserInfo createRestrictedProfileWithThrow(
@Nullable String name, @UserIdInt int parentUserId)
@@ -8495,7 +8499,6 @@ public class UserManagerService extends IUserManager.Stub {
}
/**
- * @hide
* Checks whether to show a notification for sounds (e.g., alarms, timers, etc.) from
* background users.
*/
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 7f511e1e2aa1..283979483e73 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -3801,10 +3801,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
true /* leftOrTop */);
notifyKeyGestureCompleted(event,
KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT);
- } else if (event.isAltPressed()) {
- setSplitscreenFocus(true /* leftOrTop */);
- notifyKeyGestureCompleted(event,
- KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT);
} else {
notifyKeyGestureCompleted(event,
KeyGestureEvent.KEY_GESTURE_TYPE_BACK);
@@ -3821,11 +3817,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
notifyKeyGestureCompleted(event,
KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT);
return true;
- } else if (event.isAltPressed()) {
- setSplitscreenFocus(false /* leftOrTop */);
- notifyKeyGestureCompleted(event,
- KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT);
- return true;
}
}
break;
@@ -4241,9 +4232,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
case KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION:
case KeyGestureEvent.KEY_GESTURE_TYPE_DESKTOP_MODE:
case KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT:
- case KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT:
case KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT:
- case KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT:
case KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER:
case KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_UP:
case KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN:
@@ -4379,22 +4368,12 @@ public class PhoneWindowManager implements WindowManagerPolicy {
true /* leftOrTop */);
}
return true;
- case KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT:
- if (complete) {
- setSplitscreenFocus(true /* leftOrTop */);
- }
- return true;
case KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT:
if (complete) {
moveFocusedTaskToStageSplit(getTargetDisplayIdForKeyGestureEvent(event),
false /* leftOrTop */);
}
return true;
- case KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT:
- if (complete) {
- setSplitscreenFocus(false /* leftOrTop */);
- }
- return true;
case KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER:
if (complete) {
toggleKeyboardShortcutsMenu(deviceId);
@@ -5084,13 +5063,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
}
- private void setSplitscreenFocus(boolean leftOrTop) {
- StatusBarManagerInternal statusbar = getStatusBarManagerInternal();
- if (statusbar != null) {
- statusbar.setSplitscreenFocus(leftOrTop);
- }
- }
-
void launchHomeFromHotKey(int displayId) {
launchHomeFromHotKey(displayId, true /* awakenFromDreams */, true /*respectKeyguard*/);
}
diff --git a/services/core/java/com/android/server/policy/SingleKeyGestureDetector.java b/services/core/java/com/android/server/policy/SingleKeyGestureDetector.java
index 441d3eaf2348..142d919da455 100644
--- a/services/core/java/com/android/server/policy/SingleKeyGestureDetector.java
+++ b/services/core/java/com/android/server/policy/SingleKeyGestureDetector.java
@@ -24,6 +24,8 @@ import android.util.Log;
import android.view.KeyEvent;
import android.view.ViewConfiguration;
+import com.android.hardware.input.Flags;
+
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -355,6 +357,19 @@ public final class SingleKeyGestureDetector {
}
if (event.getKeyCode() == mActiveRule.mKeyCode) {
+ if (Flags.abortSlowMultiPress()
+ && (event.getEventTime() - mLastDownTime
+ >= mActiveRule.getLongPressTimeoutMs())) {
+ // In this case, we are either on a first long press (but long press behavior is not
+ // supported for this rule), or, on a non-first press that is at least as long as
+ // the long-press duration. Thus, we will cancel the multipress gesture.
+ if (DEBUG) {
+ Log.d(TAG, "The duration of the press is too slow. Resetting.");
+ }
+ reset();
+ return false;
+ }
+
// key-up action should always be triggered if not processed by long press.
MessageObject object = new MessageObject(mActiveRule, mActiveRule.mKeyCode,
mKeyPressCounter, event);
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
index 4827c9f414ad..0ed522805bef 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
@@ -56,12 +56,12 @@ public interface StatusBarManagerInternal {
void toggleKeyboardShortcutsMenu(int deviceId);
/**
- * Used by InputMethodManagerService to notify the IME status.
+ * Sets the new IME window status.
*
- * @param displayId The display to which the IME is bound to.
- * @param vis The IME visibility.
- * @param backDisposition The IME back disposition.
- * @param showImeSwitcher {@code true} when the IME switcher button should be shown.
+ * @param displayId The id of the display to which the IME is bound.
+ * @param vis The IME window visibility.
+ * @param backDisposition The IME back disposition mode.
+ * @param showImeSwitcher Whether the IME Switcher button should be shown.
*/
void setImeWindowStatus(int displayId, @ImeWindowVisibility int vis,
@BackDispositionMode int backDisposition, boolean showImeSwitcher);
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerInternal.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerInternal.java
index 25c07500b891..872ab595994b 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerInternal.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerInternal.java
@@ -28,6 +28,9 @@ public abstract class WallpaperManagerInternal {
*/
public abstract void onDisplayReady(int displayId);
+ /** Notifies when display stop showing system decorations and wallpaper. */
+ public abstract void onDisplayRemoveSystemDecorations(int displayId);
+
/** Notifies when the screen finished turning on and is visible to the user. */
public abstract void onScreenTurnedOn(int displayId);
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index a8deeeac311d..db530e728a1a 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -118,6 +118,7 @@ import android.service.wallpaper.WallpaperService;
import android.system.ErrnoException;
import android.system.Os;
import android.text.TextUtils;
+import android.util.ArraySet;
import android.util.EventLog;
import android.util.IntArray;
import android.util.Slog;
@@ -160,6 +161,7 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
@@ -665,71 +667,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
@Override
public void onDisplayRemoved(int displayId) {
- synchronized (mLock) {
- if (enableConnectedDisplaysWallpaper()) {
- // There could be at most 2 wallpaper connections per display:
- // 1. system & lock are the same: mLastWallpaper
- // 2. system, lock are different: mLastWallpaper, mLastLockWallpaper
- // 3. fallback used as both system & lock wallpaper: mFallbackWallpaper
- // 4. fallback used as lock only wallpaper: mFallbackWallpaper,
- // mLastWallpaper
- // 5. fallback used as system only wallpaper: mFallbackWallpaper,
- // mLastLockWallpaper
- List<WallpaperData> pendingDisconnectWallpapers = new ArrayList<>();
- if (mLastWallpaper != null && mLastWallpaper.connection != null
- && mLastWallpaper.connection.containsDisplay(displayId)) {
- pendingDisconnectWallpapers.add(mLastWallpaper);
- }
- if (mLastLockWallpaper != null && mLastLockWallpaper.connection != null
- && mLastLockWallpaper.connection.containsDisplay(displayId)) {
- pendingDisconnectWallpapers.add(mLastLockWallpaper);
- }
- if (mFallbackWallpaper != null && mFallbackWallpaper.connection != null
- && mFallbackWallpaper.connection.containsDisplay(displayId)) {
- pendingDisconnectWallpapers.add(mFallbackWallpaper);
- }
- for (int i = 0; i < pendingDisconnectWallpapers.size(); i++) {
- WallpaperData wallpaper = pendingDisconnectWallpapers.get(i);
- DisplayConnector displayConnector =
- wallpaper.connection.getDisplayConnectorOrCreate(displayId);
- if (displayConnector == null) {
- Slog.w(TAG,
- "Fail to disconnect wallpaper upon display removal");
- return;
- }
- displayConnector.disconnectLocked(wallpaper.connection);
- wallpaper.connection.removeDisplayConnector(displayId);
- }
- } else {
- if (mLastWallpaper != null) {
- WallpaperData targetWallpaper = null;
- if (mLastWallpaper.connection != null
- && mLastWallpaper.connection.containsDisplay(displayId)) {
- targetWallpaper = mLastWallpaper;
- } else if (mFallbackWallpaper != null
- && mFallbackWallpaper.connection != null
- && mFallbackWallpaper.connection.containsDisplay(
- displayId)) {
- targetWallpaper = mFallbackWallpaper;
- }
- if (targetWallpaper == null) return;
- DisplayConnector connector =
- targetWallpaper.connection.getDisplayConnectorOrCreate(
- displayId);
- if (connector == null) return;
- connector.disconnectLocked(targetWallpaper.connection);
- targetWallpaper.connection.removeDisplayConnector(displayId);
- }
- }
-
- mWallpaperDisplayHelper.removeDisplayData(displayId);
-
- for (int i = mColorsChangedListeners.size() - 1; i >= 0; i--) {
- final SparseArray<RemoteCallbackList<IWallpaperManagerCallback>> callbacks =
- mColorsChangedListeners.valueAt(i);
- callbacks.delete(displayId);
- }
- }
+ onDisplayRemovedInternal(displayId);
}
@Override
@@ -772,6 +710,12 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
private final ComponentName mImageWallpaper;
/**
+ * Name of the component that is used when the user-selected wallpaper is incompatible with the
+ * display's resolution or aspect ratio.
+ */
+ @Nullable private final ComponentName mFallbackWallpaperComponent;
+
+ /**
* Default image wallpaper shall never changed after system service started, caching it when we
* first read the image file.
*/
@@ -799,12 +743,38 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
final WallpaperDisplayHelper mWallpaperDisplayHelper;
final WallpaperCropper mWallpaperCropper;
- private boolean supportsMultiDisplay(WallpaperConnection connection) {
- if (connection != null) {
- return connection.mInfo == null // This is image wallpaper
- || connection.mInfo.supportsMultipleDisplays();
+ // TODO(b/384519749): Remove this set after we introduce the aspect ratio check.
+ private final Set<Integer> mWallpaperCompatibleDisplaysForTest = new ArraySet<>();
+
+ private boolean isWallpaperCompatibleForDisplay(int displayId, WallpaperConnection connection) {
+ if (connection == null) {
+ return false;
}
- return false;
+ // Non image wallpaper.
+ if (connection.mInfo != null) {
+ return connection.mInfo.supportsMultipleDisplays();
+ }
+
+ // Image wallpaper
+ if (enableConnectedDisplaysWallpaper()) {
+ // TODO(b/384519749): check display's resolution and image wallpaper cropped image
+ // aspect ratio.
+ return displayId == DEFAULT_DISPLAY
+ || mWallpaperCompatibleDisplaysForTest.contains(displayId);
+ }
+ // When enableConnectedDisplaysWallpaper is off, we assume the image wallpaper supports all
+ // usable displays.
+ return true;
+ }
+
+ @VisibleForTesting
+ void addWallpaperCompatibleDisplayForTest(int displayId) {
+ mWallpaperCompatibleDisplaysForTest.add(displayId);
+ }
+
+ @VisibleForTesting
+ void removeWallpaperCompatibleDisplayForTest(int displayId) {
+ mWallpaperCompatibleDisplaysForTest.remove(displayId);
}
private void updateFallbackConnection() {
@@ -815,7 +785,9 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
Slog.w(TAG, "Fallback wallpaper connection has not been created yet!!");
return;
}
- if (supportsMultiDisplay(systemConnection)) {
+ // TODO(b/384520326) Passing DEFAULT_DISPLAY temporarily before we revamp the
+ // multi-display supports.
+ if (isWallpaperCompatibleForDisplay(DEFAULT_DISPLAY, systemConnection)) {
if (fallbackConnection.mDisplayConnector.size() != 0) {
fallbackConnection.forEachDisplayConnector(connector -> {
if (connector.mEngine != null) {
@@ -990,16 +962,14 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
private void initDisplayState() {
// Do not initialize fallback wallpaper
if (!mWallpaper.equals(mFallbackWallpaper)) {
- if (supportsMultiDisplay(this)) {
- // The system wallpaper is image wallpaper or it can supports multiple displays.
- appendConnectorWithCondition(display ->
- mWallpaperDisplayHelper.isUsableDisplay(display, mClientUid));
- } else {
- // The system wallpaper does not support multiple displays, so just attach it on
- // default display.
- mDisplayConnector.append(DEFAULT_DISPLAY,
- new DisplayConnector(DEFAULT_DISPLAY));
- }
+ appendConnectorWithCondition(display -> {
+ final int displayId = display.getDisplayId();
+ if (display.getDisplayId() == DEFAULT_DISPLAY) {
+ return true;
+ }
+ return mWallpaperDisplayHelper.isUsableDisplay(display, mClientUid)
+ && isWallpaperCompatibleForDisplay(displayId, /* connection= */ this);
+ });
}
}
@@ -1601,6 +1571,12 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
mShuttingDown = false;
mImageWallpaper = ComponentName.unflattenFromString(
context.getResources().getString(R.string.image_wallpaper_component));
+ if (enableConnectedDisplaysWallpaper()) {
+ mFallbackWallpaperComponent = ComponentName.unflattenFromString(
+ context.getResources().getString(R.string.fallback_wallpaper_component));
+ } else {
+ mFallbackWallpaperComponent = null;
+ }
ComponentName defaultComponent = WallpaperManager.getCmfDefaultWallpaperComponent(context);
mDefaultWallpaperComponent = defaultComponent == null ? mImageWallpaper : defaultComponent;
mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
@@ -1665,6 +1641,13 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
}
@Override
+ public void onDisplayRemoveSystemDecorations(int displayId) {
+ // The display mirroring starts. The handling logic is the same as when removing a
+ // display.
+ onDisplayRemovedInternal(displayId);
+ }
+
+ @Override
public void onScreenTurnedOn(int displayId) {
notifyScreenTurnedOn(displayId);
}
@@ -3613,6 +3596,13 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
if (componentName != null && !componentName.equals(mImageWallpaper)) {
// The requested component is not the static wallpaper service, so make sure it's
// actually a wallpaper service.
+ if (mFallbackWallpaperComponent != null
+ && componentName.equals(mFallbackWallpaperComponent)) {
+ // The fallback wallpaper does not declare WallpaperService.SERVICE_INTERFACE
+ // action in its intent filter to prevent it from being listed in the wallpaper
+ // picker. And thus, use explicit intent to query the metadata.
+ intent = new Intent().setComponent(mFallbackWallpaperComponent);
+ }
List<ResolveInfo> ris =
mIPackageManager.queryIntentServices(intent,
intent.resolveTypeIfNeeded(mContext.getContentResolver()),
@@ -3971,7 +3961,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
for (int i = 0; i < wallpapers.size(); i++) {
WallpaperData wallpaper = wallpapers.get(i);
- if (supportsMultiDisplay(wallpaper.connection)) {
+ if (isWallpaperCompatibleForDisplay(displayId, wallpaper.connection)) {
final DisplayConnector connector =
wallpaper.connection.getDisplayConnectorOrCreate(displayId);
if (connector != null) {
@@ -3993,7 +3983,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
}
mFallbackWallpaper.mWhich = useFallbackWallpaperWhich;
} else {
- if (supportsMultiDisplay(mLastWallpaper.connection)) {
+ if (isWallpaperCompatibleForDisplay(displayId, mLastWallpaper.connection)) {
final DisplayConnector connector =
mLastWallpaper.connection.getDisplayConnectorOrCreate(displayId);
if (connector == null) return;
@@ -4016,6 +4006,78 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
}
}
+ // This method may be called even if the display is not being removed from the system.
+ // This can be called when the display is removed, or when the display system decorations are
+ // removed to start mirroring.
+ private void onDisplayRemovedInternal(int displayId) {
+ synchronized (mLock) {
+ if (enableConnectedDisplaysWallpaper()) {
+ // There could be at most 2 wallpaper connections per display:
+ // 1. system & lock are the same: mLastWallpaper
+ // 2. system, lock are different: mLastWallpaper, mLastLockWallpaper
+ // 3. fallback used as both system & lock wallpaper: mFallbackWallpaper
+ // 4. fallback used as lock only wallpaper: mFallbackWallpaper,
+ // mLastWallpaper
+ // 5. fallback used as system only wallpaper: mFallbackWallpaper,
+ // mLastLockWallpaper
+ List<WallpaperData> pendingDisconnectWallpapers = new ArrayList<>();
+ if (mLastWallpaper != null && mLastWallpaper.connection != null
+ && mLastWallpaper.connection.containsDisplay(displayId)) {
+ pendingDisconnectWallpapers.add(mLastWallpaper);
+ }
+ if (mLastLockWallpaper != null && mLastLockWallpaper.connection != null
+ && mLastLockWallpaper.connection.containsDisplay(displayId)) {
+ pendingDisconnectWallpapers.add(mLastLockWallpaper);
+ }
+ if (mFallbackWallpaper != null && mFallbackWallpaper.connection != null
+ && mFallbackWallpaper.connection.containsDisplay(displayId)) {
+ pendingDisconnectWallpapers.add(mFallbackWallpaper);
+ }
+ for (int i = 0; i < pendingDisconnectWallpapers.size(); i++) {
+ WallpaperData wallpaper = pendingDisconnectWallpapers.get(i);
+ DisplayConnector displayConnector =
+ wallpaper.connection.getDisplayConnectorOrCreate(displayId);
+ if (displayConnector == null) {
+ Slog.w(TAG,
+ "Fail to disconnect wallpaper upon display removes system "
+ + "decorations");
+ return;
+ }
+ displayConnector.disconnectLocked(wallpaper.connection);
+ wallpaper.connection.removeDisplayConnector(displayId);
+ }
+ } else {
+ if (mLastWallpaper != null) {
+ WallpaperData targetWallpaper = null;
+ if (mLastWallpaper.connection != null
+ && mLastWallpaper.connection.containsDisplay(displayId)) {
+ targetWallpaper = mLastWallpaper;
+ } else if (mFallbackWallpaper != null
+ && mFallbackWallpaper.connection != null
+ && mFallbackWallpaper.connection.containsDisplay(
+ displayId)) {
+ targetWallpaper = mFallbackWallpaper;
+ }
+ if (targetWallpaper == null) return;
+ DisplayConnector connector =
+ targetWallpaper.connection.getDisplayConnectorOrCreate(
+ displayId);
+ if (connector == null) return;
+ connector.disconnectLocked(targetWallpaper.connection);
+ targetWallpaper.connection.removeDisplayConnector(displayId);
+ }
+ }
+
+ mWallpaperDisplayHelper.removeDisplayData(displayId);
+
+ for (int i = mColorsChangedListeners.size() - 1; i >= 0; i--) {
+ final SparseArray<RemoteCallbackList<IWallpaperManagerCallback>> callbacks =
+ mColorsChangedListeners.valueAt(i);
+ callbacks.delete(displayId);
+ }
+ }
+ }
+
void saveSettingsLocked(int userId) {
TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG);
t.traceBegin("WPMS.saveSettingsLocked-" + userId);
@@ -4100,8 +4162,14 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
mFallbackWallpaper.allowBackup = false;
mFallbackWallpaper.wallpaperId = makeWallpaperIdLocked();
mFallbackWallpaper.mBindSource = BindSource.INITIALIZE_FALLBACK;
- bindWallpaperComponentLocked(mDefaultWallpaperComponent, true, false,
- mFallbackWallpaper, null);
+ if (mFallbackWallpaperComponent == null) {
+ bindWallpaperComponentLocked(mDefaultWallpaperComponent, true, false,
+ mFallbackWallpaper, null);
+ } else {
+ mFallbackWallpaper.mWhich = FLAG_SYSTEM | FLAG_LOCK;
+ bindWallpaperComponentLocked(mFallbackWallpaperComponent, true, false,
+ mFallbackWallpaper, null);
+ }
}
}
diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java
index dd769173fb34..12f553426c80 100644
--- a/services/core/java/com/android/server/wm/AccessibilityController.java
+++ b/services/core/java/com/android/server/wm/AccessibilityController.java
@@ -58,7 +58,6 @@ import android.graphics.Matrix;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.Rect;
-import android.graphics.RectF;
import android.graphics.Region;
import android.os.Binder;
import android.os.Build;
@@ -69,7 +68,6 @@ import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.os.SystemClock;
-import android.util.ArraySet;
import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
@@ -79,7 +77,6 @@ import android.view.Display;
import android.view.MagnificationSpec;
import android.view.Surface;
import android.view.ViewConfiguration;
-import android.view.WindowInfo;
import android.view.WindowManager;
import android.view.WindowManager.TransitionFlags;
import android.view.WindowManager.TransitionType;
@@ -557,9 +554,6 @@ final class AccessibilityController {
private static final boolean DEBUG_WINDOW_TRANSITIONS = false;
private static final boolean DEBUG_DISPLAY_SIZE = false;
- private static final boolean DEBUG_LAYERS = false;
- private static final boolean DEBUG_RECTANGLE_REQUESTED = false;
- private static final boolean DEBUG_VIEWPORT_WINDOW = false;
private final Rect mTempRect1 = new Rect();
private final Rect mTempRect2 = new Rect();
@@ -579,8 +573,6 @@ final class AccessibilityController {
private final MagnificationCallbacks mCallbacks;
private final UserContextChangedNotifier mUserContextChangedNotifier;
- private final long mLongAnimationDuration;
-
private boolean mIsFullscreenMagnificationActivated = false;
private final Region mMagnificationRegion = new Region();
private final Region mOldMagnificationRegion = new Region();
@@ -593,7 +585,6 @@ final class AccessibilityController {
private final Point mScreenSize = new Point();
private final SparseArray<WindowState> mTempWindowStates =
new SparseArray<WindowState>();
- private final RectF mTempRectF = new RectF();
private final Matrix mTempMatrix = new Matrix();
DisplayMagnifier(WindowManagerService windowManagerService,
@@ -609,8 +600,6 @@ final class AccessibilityController {
mUserContextChangedNotifier = new UserContextChangedNotifier(mHandler);
mAccessibilityTracing =
AccessibilityController.getAccessibilityControllerInternal(mService);
- mLongAnimationDuration = mDisplayContext.getResources().getInteger(
- com.android.internal.R.integer.config_longAnimTime);
if (mDisplayContext.getResources().getConfiguration().isScreenRound()) {
mCircularPath = new Path();
@@ -840,10 +829,6 @@ final class AccessibilityController {
outMagnificationRegion.set(mMagnificationRegion);
}
- boolean isMagnifying() {
- return mMagnificationSpec.scale > 1.0f;
- }
-
void destroy() {
if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
mAccessibilityTracing.logTrace(LOG_TAG + ".destroy", FLAGS_MAGNIFICATION_CALLBACK);
@@ -1172,12 +1157,6 @@ final class AccessibilityController {
private static final boolean DEBUG = false;
- private final Set<IBinder> mTempBinderSet = new ArraySet<>();
-
- private final Region mTempRegion = new Region();
-
- private final Region mTempRegion2 = new Region();
-
private final WindowManagerService mService;
private final Handler mHandler;
@@ -1243,11 +1222,10 @@ final class AccessibilityController {
Slog.i(LOG_TAG, "computeChangedWindows()");
}
- List<WindowInfo> windows = null;
final List<AccessibilityWindow> visibleWindows = new ArrayList<>();
final Point screenSize = new Point();
final int topFocusedDisplayId;
- IBinder topFocusedWindowToken = null;
+ final IBinder topFocusedWindowToken;
synchronized (mService.mGlobalLock) {
final WindowState topFocusedWindowState = getTopFocusWindow();
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 1d12c561f118..89b46bc4eba4 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -46,6 +46,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.app.WindowConfiguration.activityTypeToString;
+import static android.app.WindowConfiguration.isFloating;
import static android.app.admin.DevicePolicyResources.Drawables.Source.PROFILE_SWITCH_ANIMATION;
import static android.app.admin.DevicePolicyResources.Drawables.Style.OUTLINE;
import static android.app.admin.DevicePolicyResources.Drawables.WORK_PROFILE_ICON;
@@ -3231,8 +3232,8 @@ final class ActivityRecord extends WindowToken {
* Returns {@code true} if the fixed orientation, aspect ratio, resizability of the application
* can be ignored.
*/
- static boolean canBeUniversalResizeable(ApplicationInfo appInfo, WindowManagerService wms,
- boolean isLargeScreen, boolean forActivity) {
+ static boolean canBeUniversalResizeable(@NonNull ApplicationInfo appInfo,
+ WindowManagerService wms, boolean isLargeScreen, boolean forActivity) {
if (appInfo.category == ApplicationInfo.CATEGORY_GAME) {
return false;
}
@@ -8376,6 +8377,7 @@ final class ActivityRecord extends WindowToken {
mConfigurationSeq = Math.max(++mConfigurationSeq, 1);
getResolvedOverrideConfiguration().seq = mConfigurationSeq;
+ // TODO(b/392069771): Move to AppCompatSandboxingPolicy.
// Sandbox max bounds by setting it to the activity bounds, if activity is letterboxed, or
// has or will have mAppCompatDisplayInsets for size compat. Also forces an activity to be
// sandboxed or not depending upon the configuration settings.
@@ -8404,6 +8406,20 @@ final class ActivityRecord extends WindowToken {
resolvedConfig.windowConfiguration.setMaxBounds(mTmpBounds);
}
+ // Sandbox activity bounds in freeform to app bounds to force app to display within the
+ // container. This prevents UI cropping when activities can draw below insets which are
+ // normally excluded from appBounds before targetSDK < 35
+ // (see ConfigurationContainer#applySizeOverrideIfNeeded).
+ if (isFloating(parentWindowingMode)) {
+ Rect appBounds = resolvedConfig.windowConfiguration.getAppBounds();
+ if (appBounds == null || appBounds.isEmpty()) {
+ // When there is no override bounds, the activity will inherit the bounds from
+ // parent.
+ appBounds = mResolveConfigHint.mParentAppBoundsOverride;
+ }
+ resolvedConfig.windowConfiguration.setBounds(appBounds);
+ }
+
applySizeOverrideIfNeeded(
mDisplayContent,
info.applicationInfo,
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index d4f9c0901162..cf111cdbcc6a 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -2154,6 +2154,16 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
}
}
+ /**
+ * @return ehether the application could be universal resizeable on a large screen,
+ * ignoring any overrides
+ */
+ @Override
+ public boolean canBeUniversalResizeable(@NonNull ApplicationInfo appInfo) {
+ return ActivityRecord.canBeUniversalResizeable(appInfo, mWindowManager,
+ /* isLargeScreen */ true, /* forActivity */ false);
+ }
+
@Override
public void removeAllVisibleRecentTasks() {
mAmInternal.enforceCallingPermission(REMOVE_TASKS, "removeAllVisibleRecentTasks()");
diff --git a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java
index 35fa39dab900..d994a1904a14 100644
--- a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java
@@ -180,10 +180,7 @@ class AppCompatOrientationPolicy {
return true;
}
- final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy
- .getAppCompatCameraPolicy(mActivityRecord);
- if (cameraPolicy != null
- && cameraPolicy.isTreatmentEnabledForActivity(mActivityRecord)) {
+ if (AppCompatCameraPolicy.isTreatmentEnabledForActivity(mActivityRecord)) {
Slog.w(TAG, "Ignoring orientation update to "
+ screenOrientationToString(requestedOrientation)
+ " due to camera compat treatment for " + mActivityRecord);
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index e1a50a93edcc..5b1619995529 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -1910,6 +1910,11 @@ public class DisplayPolicy {
if (statusBar != null) {
statusBar.onDisplayRemoveSystemDecorations(displayId);
}
+ final WallpaperManagerInternal wpMgr =
+ LocalServices.getService(WallpaperManagerInternal.class);
+ if (wpMgr != null) {
+ wpMgr.onDisplayRemoveSystemDecorations(displayId);
+ }
});
}
diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
index bcfaa3947e74..59ca79c7ffc3 100644
--- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
@@ -361,7 +361,7 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider {
controlTarget = mDisplayContent.getImeHostOrFallback(
((InsetsControlTarget) imeInsetsTarget).getWindow());
- if (controlTarget != imeInsetsTarget) {
+ if (controlTarget != null && controlTarget != imeInsetsTarget) {
ImeTracker.forLogging().onProgress(statsToken,
ImeTracker.PHASE_WM_SET_REMOTE_TARGET_IME_VISIBILITY);
controlTarget.setImeInputTargetRequestedVisibility(imeVisible, statsToken);
diff --git a/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java
index efc68aac0323..00e1c01bbadb 100644
--- a/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java
+++ b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java
@@ -22,6 +22,7 @@ import static com.android.internal.protolog.WmProtoLogGroups.WM_ERROR;
import android.media.projection.IMediaProjectionManager;
import android.media.projection.IMediaProjectionWatcherCallback;
+import android.media.projection.MediaProjectionEvent;
import android.media.projection.MediaProjectionInfo;
import android.os.Binder;
import android.os.IBinder;
@@ -84,6 +85,12 @@ public class ScreenRecordingCallbackController {
public void onRecordingSessionSet(MediaProjectionInfo mediaProjectionInfo,
ContentRecordingSession contentRecordingSession) {
}
+
+ @Override
+ public void onMediaProjectionEvent(
+ MediaProjectionEvent event,
+ MediaProjectionInfo mediaProjectionInfo,
+ ContentRecordingSession session) {}
}
ScreenRecordingCallbackController(WindowManagerService wms) {
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 37cc0d22c063..27683b2fcff2 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1504,16 +1504,15 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
}
// Update the input-sink (touch-blocking) state now that the animation is finished.
- SurfaceControl.Transaction inputSinkTransaction = null;
+ boolean scheduleAnimation = false;
for (int i = 0; i < mParticipants.size(); ++i) {
final ActivityRecord ar = mParticipants.valueAt(i).asActivityRecord();
if (ar == null || !ar.isVisible() || ar.getParent() == null) continue;
- if (inputSinkTransaction == null) {
- inputSinkTransaction = ar.mWmService.mTransactionFactory.get();
- }
- ar.mActivityRecordInputSink.applyChangesToSurfaceIfChanged(inputSinkTransaction);
+ scheduleAnimation = true;
+ ar.mActivityRecordInputSink.applyChangesToSurfaceIfChanged(ar.getPendingTransaction());
}
- if (inputSinkTransaction != null) inputSinkTransaction.apply();
+ // To apply pending transactions.
+ if (scheduleAnimation) mController.mAtm.mWindowManager.scheduleAnimationLocked();
// Always schedule stop processing when transition finishes because activities don't
// stop while they are in a transition thus their stop could still be pending.
diff --git a/services/core/java/com/android/server/wm/WallpaperWindowToken.java b/services/core/java/com/android/server/wm/WallpaperWindowToken.java
index 0ecd0251ca94..3b79c54f1c73 100644
--- a/services/core/java/com/android/server/wm/WallpaperWindowToken.java
+++ b/services/core/java/com/android/server/wm/WallpaperWindowToken.java
@@ -89,8 +89,9 @@ class WallpaperWindowToken extends WindowToken {
// Similar to Task.prepareSurfaces, outside of transitions we need to apply visibility
// changes directly. In transitions the transition player will take care of applying the
// visibility change.
- if (!mTransitionController.inTransition(this)) {
- getSyncTransaction().setVisibility(mSurfaceControl, isVisible());
+ if (!mTransitionController.isCollecting(this)
+ && !mTransitionController.isPlayingTarget(this)) {
+ getPendingTransaction().setVisibility(mSurfaceControl, isVisible());
}
}
}
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index e761e024b3ca..883d8f95b612 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -1680,26 +1680,27 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
* Sets the specified orientation of this container. It percolates this change upward along the
* hierarchy to let each level of the hierarchy a chance to respond to it.
*
- * @param orientation the specified orientation. Needs to be one of {@link ScreenOrientation}.
+ * @param requestedOrientation the specified orientation. Needs to be one of
+ * {@link ScreenOrientation}.
* @param requestingContainer the container which orientation request has changed. Mostly used
* to ensure it gets correct configuration.
* @return the resolved override orientation of this window container.
*/
@ScreenOrientation
- int setOrientation(@ScreenOrientation int orientation,
+ int setOrientation(@ScreenOrientation int requestedOrientation,
@Nullable WindowContainer requestingContainer) {
- if (getOverrideOrientation() == orientation) {
- return orientation;
+ if (getOverrideOrientation() == requestedOrientation) {
+ return requestedOrientation;
}
- setOverrideOrientation(orientation);
+ setOverrideOrientation(requestedOrientation);
final WindowContainer parent = getParent();
if (parent == null) {
- return orientation;
+ return requestedOrientation;
}
// The derived class can return a result that is different from the given orientation.
- final int resolvedOrientation = getOverrideOrientation();
+ final int actualOverrideOrientation = getOverrideOrientation();
if (getConfiguration().orientation != getRequestedConfigurationOrientation(
- false /* forDisplay */, resolvedOrientation)
+ false /* forDisplay */, actualOverrideOrientation)
// Update configuration directly only if the change won't be dispatched from
// ancestor. This prevents from computing intermediate configuration when the
// parent also needs to be updated from the ancestor. E.g. the app requests
@@ -1707,12 +1708,12 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
// the task can be updated to portrait first so the configuration can be
// computed in a consistent environment.
&& (inMultiWindowMode()
- || !handlesOrientationChangeFromDescendant(orientation))) {
+ || !handlesOrientationChangeFromDescendant(requestedOrientation))) {
// Resolve the requested orientation.
onConfigurationChanged(parent.getConfiguration());
}
onDescendantOrientationChanged(requestingContainer);
- return resolvedOrientation;
+ return actualOverrideOrientation;
}
@ScreenOrientation
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 5de0e9b6ed93..3a1d652f82d4 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -309,6 +309,7 @@ import android.window.ActivityWindowInfo;
import android.window.AddToSurfaceSyncGroupResult;
import android.window.ClientWindowFrames;
import android.window.ConfigurationChangeSetting;
+import android.window.DesktopModeFlags;
import android.window.IGlobalDragListener;
import android.window.IScreenRecordingCallback;
import android.window.ISurfaceSyncGroupCompletedListener;
@@ -820,6 +821,8 @@ public class WindowManagerService extends IWindowManager.Stub
DEVELOPMENT_WM_DISPLAY_SETTINGS_PATH);
private final Uri mMaximumObscuringOpacityForTouchUri = Settings.Global.getUriFor(
Settings.Global.MAXIMUM_OBSCURING_OPACITY_FOR_TOUCH);
+ private final Uri mDevelopmentOverrideDesktopExperienceUri = Settings.Global.getUriFor(
+ Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_EXPERIENCE_FEATURES);
public SettingsObserver() {
super(new Handler());
@@ -847,6 +850,8 @@ public class WindowManagerService extends IWindowManager.Stub
UserHandle.USER_ALL);
resolver.registerContentObserver(mMaximumObscuringOpacityForTouchUri, false, this,
UserHandle.USER_ALL);
+ resolver.registerContentObserver(mDevelopmentOverrideDesktopExperienceUri, false, this,
+ UserHandle.USER_ALL);
}
@Override
@@ -890,6 +895,11 @@ public class WindowManagerService extends IWindowManager.Stub
return;
}
+ if (mDevelopmentOverrideDesktopExperienceUri.equals(uri)) {
+ updateDevelopmentOverrideDesktopExperience();
+ return;
+ }
+
@UpdateAnimationScaleMode
final int mode;
if (mWindowAnimationScaleUri.equals(uri)) {
@@ -956,6 +966,16 @@ public class WindowManagerService extends IWindowManager.Stub
mAtmService.mForceResizableActivities = forceResizable;
}
+ void updateDevelopmentOverrideDesktopExperience() {
+ ContentResolver resolver = mContext.getContentResolver();
+ final int overrideDesktopMode = Settings.Global.getInt(resolver,
+ Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_EXPERIENCE_FEATURES,
+ DesktopModeFlags.ToggleOverride.OVERRIDE_UNSET.getSetting());
+
+ SystemProperties.set(DesktopModeFlags.SYSTEM_PROPERTY_NAME,
+ Integer.toString(overrideDesktopMode));
+ }
+
void updateDevEnableNonResizableMultiWindow() {
ContentResolver resolver = mContext.getContentResolver();
final boolean devEnableNonResizableMultiWindow = Settings.Global.getInt(resolver,
diff --git a/services/core/jni/com_android_server_utils_AnrTimer.cpp b/services/core/jni/com_android_server_utils_AnrTimer.cpp
index 2add5b09f15b..24909ac6150b 100644
--- a/services/core/jni/com_android_server_utils_AnrTimer.cpp
+++ b/services/core/jni/com_android_server_utils_AnrTimer.cpp
@@ -101,6 +101,9 @@ nsecs_t now() {
return systemTime(SYSTEM_TIME_MONOTONIC);
}
+// The current process. This is cached here on startup.
+const pid_t sThisProcess = getpid();
+
// Return true if the process exists and false if we cannot know.
bool processExists(pid_t pid) {
char path[PATH_MAX];
@@ -726,7 +729,7 @@ class AnrTimerService::Timer {
uid(uid),
timeout(timeout),
extend(extend),
- freeze(pid != 0 && freeze),
+ freeze(freeze),
split(trace.earlyTimeout),
action(trace.action),
status(Running),
@@ -1188,8 +1191,11 @@ const char* AnrTimerService::statusString(Status s) {
}
AnrTimerService::timer_id_t AnrTimerService::start(int pid, int uid, nsecs_t timeout) {
+ // Use the freezer only if the pid is not 0 (a nonsense value) and the pid is not self.
+ // Freezing the current process is a fatal error.
+ bool useFreezer = freeze_ && (pid != 0) && (pid != sThisProcess);
AutoMutex _l(lock_);
- Timer t(pid, uid, timeout, extend_, freeze_, tracer_.getConfig(pid));
+ Timer t(pid, uid, timeout, extend_, useFreezer, tracer_.getConfig(pid));
insertLocked(t);
t.start();
counters_.started++;
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 25e9f8a38f89..c974d9e1dc87 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -1799,7 +1799,7 @@ public final class SystemServer implements Dumpable {
mSystemServiceManager.startService(LogcatManagerService.class);
t.traceEnd();
- if (!isWatch && !isTv && !isAutomotive
+ if (!isWatch && !isTv && !isAutomotive && !isDesktop
&& android.security.Flags.aflApi()) {
t.traceBegin("StartIntrusionDetectionService");
mSystemServiceManager.startService(IntrusionDetectionService.class);
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionService.java b/services/supervision/java/com/android/server/supervision/SupervisionService.java
index 073ee31ddd60..4f9859d33d74 100644
--- a/services/supervision/java/com/android/server/supervision/SupervisionService.java
+++ b/services/supervision/java/com/android/server/supervision/SupervisionService.java
@@ -51,7 +51,6 @@ import com.android.internal.util.DumpUtils;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.LocalServices;
import com.android.server.SystemService;
-import com.android.server.SystemService.TargetUser;
import com.android.server.pm.UserManagerInternal;
import java.io.FileDescriptor;
@@ -62,17 +61,17 @@ import java.util.List;
public class SupervisionService extends ISupervisionManager.Stub {
private static final String LOG_TAG = "SupervisionService";
- private final Context mContext;
-
// TODO(b/362756788): Does this need to be a LockGuard lock?
private final Object mLockDoNoUseDirectly = new Object();
@GuardedBy("getLockObject()")
private final SparseArray<SupervisionUserData> mUserData = new SparseArray<>();
+ private final Context mContext;
private final DevicePolicyManagerInternal mDpmInternal;
private final PackageManager mPackageManager;
private final UserManagerInternal mUserManagerInternal;
+ final SupervisionManagerInternal mInternal = new SupervisionManagerInternalImpl();
public SupervisionService(Context context) {
mContext = context.createAttributionContext(LOG_TAG);
@@ -82,6 +81,12 @@ public class SupervisionService extends ISupervisionManager.Stub {
mUserManagerInternal.addUserLifecycleListener(new UserLifecycleListener());
}
+ /**
+ * Returns whether supervision is enabled for the given user.
+ *
+ * <p>Supervision is automatically enabled when the supervision app becomes the profile owner or
+ * explicitly enabled via an internal call to {@link #setSupervisionEnabledForUser}.
+ */
@Override
public boolean isSupervisionEnabledForUser(@UserIdInt int userId) {
if (UserHandle.getUserId(Binder.getCallingUid()) != userId) {
@@ -92,6 +97,20 @@ public class SupervisionService extends ISupervisionManager.Stub {
}
}
+ /**
+ * Returns the package name of the active supervision app or null if supervision is disabled.
+ */
+ @Override
+ @Nullable
+ public String getActiveSupervisionAppPackage(@UserIdInt int userId) {
+ if (UserHandle.getUserId(Binder.getCallingUid()) != userId) {
+ enforcePermission(INTERACT_ACROSS_USERS);
+ }
+ synchronized (getLockObject()) {
+ return getUserDataLocked(userId).supervisionAppPackage;
+ }
+ }
+
@Override
public void onShellCommand(
@Nullable FileDescriptor in,
@@ -140,35 +159,53 @@ public class SupervisionService extends ISupervisionManager.Stub {
return data;
}
- void setSupervisionEnabledForUser(@UserIdInt int userId, boolean enabled) {
+ /**
+ * Sets supervision as enabled or disabled for the given user and, in case supervision is being
+ * enabled, the package of the active supervision app.
+ */
+ private void setSupervisionEnabledForUser(
+ @UserIdInt int userId, boolean enabled, @Nullable String supervisionAppPackage) {
synchronized (getLockObject()) {
- getUserDataLocked(userId).supervisionEnabled = enabled;
+ SupervisionUserData data = getUserDataLocked(userId);
+ data.supervisionEnabled = enabled;
+ data.supervisionAppPackage = enabled ? supervisionAppPackage : null;
}
}
- /** Ensures that supervision is enabled when supervision app is the profile owner. */
+ /** Ensures that supervision is enabled when the supervision app is the profile owner. */
private void syncStateWithDevicePolicyManager(@UserIdInt int userId) {
- if (isProfileOwner(userId)) {
- setSupervisionEnabledForUser(userId, true);
+ final ComponentName po =
+ mDpmInternal != null ? mDpmInternal.getProfileOwnerAsUser(userId) : null;
+
+ if (po != null && po.getPackageName().equals(getSystemSupervisionPackage())) {
+ setSupervisionEnabledForUser(userId, true, po.getPackageName());
+ } else if (po != null && po.equals(getSupervisionProfileOwnerComponent())) {
+ // TODO(b/392071637): Consider not enabling supervision in case profile owner is given
+ // to the legacy supervision profile owner component.
+ setSupervisionEnabledForUser(userId, true, po.getPackageName());
} else {
// TODO(b/381428475): Avoid disabling supervision when the app is not the profile owner.
// This might only be possible after introducing specific and public APIs to enable
- // supervision.
- setSupervisionEnabledForUser(userId, false);
+ // and disable supervision.
+ setSupervisionEnabledForUser(userId, false, /* supervisionAppPackage= */ null);
}
}
- /** Returns whether the supervision app has profile owner status. */
- private boolean isProfileOwner(@UserIdInt int userId) {
- ComponentName profileOwner =
- mDpmInternal != null ? mDpmInternal.getProfileOwnerAsUser(userId) : null;
- return profileOwner != null && isSupervisionAppPackage(profileOwner.getPackageName());
+ /**
+ * Returns the {@link ComponentName} of the supervision profile owner component.
+ *
+ * <p>This component is used to give GMS Kids Module permission to supervise the device and may
+ * still be active during the transition to the {@code SYSTEM_SUPERVISION} role.
+ */
+ private ComponentName getSupervisionProfileOwnerComponent() {
+ return ComponentName.unflattenFromString(
+ mContext.getResources()
+ .getString(R.string.config_defaultSupervisionProfileOwnerComponent));
}
- /** Returns whether the given package name belongs to the supervision role holder. */
- private boolean isSupervisionAppPackage(String packageName) {
- return packageName.equals(
- mContext.getResources().getString(R.string.config_systemSupervision));
+ /** Returns the package assigned to the {@code SYSTEM_SUPERVISION} role. */
+ private String getSystemSupervisionPackage() {
+ return mContext.getResources().getString(R.string.config_systemSupervision);
}
/** Enforces that the caller has the given permission. */
@@ -228,19 +265,21 @@ public class SupervisionService extends ISupervisionManager.Stub {
}
}
- final SupervisionManagerInternal mInternal = new SupervisionManagerInternalImpl();
-
private final class SupervisionManagerInternalImpl extends SupervisionManagerInternal {
@Override
public boolean isActiveSupervisionApp(int uid) {
- String[] packages = mPackageManager.getPackagesForUid(uid);
- if (packages == null) {
+ int userId = UserHandle.getUserId(uid);
+ String supervisionAppPackage = getActiveSupervisionAppPackage(userId);
+ if (supervisionAppPackage == null) {
return false;
}
- for (var packageName : packages) {
- if (SupervisionService.this.isSupervisionAppPackage(packageName)) {
- int userId = UserHandle.getUserId(uid);
- return SupervisionService.this.isSupervisionEnabledForUser(userId);
+
+ String[] packages = mPackageManager.getPackagesForUid(uid);
+ if (packages != null) {
+ for (var packageName : packages) {
+ if (supervisionAppPackage.equals(packageName)) {
+ return true;
+ }
}
}
return false;
@@ -253,7 +292,8 @@ public class SupervisionService extends ISupervisionManager.Stub {
@Override
public void setSupervisionEnabledForUser(@UserIdInt int userId, boolean enabled) {
- SupervisionService.this.setSupervisionEnabledForUser(userId, enabled);
+ SupervisionService.this.setSupervisionEnabledForUser(
+ userId, enabled, getSystemSupervisionPackage());
}
@Override
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionServiceShellCommand.java b/services/supervision/java/com/android/server/supervision/SupervisionServiceShellCommand.java
index 2adaae3943f1..976642bd563d 100644
--- a/services/supervision/java/com/android/server/supervision/SupervisionServiceShellCommand.java
+++ b/services/supervision/java/com/android/server/supervision/SupervisionServiceShellCommand.java
@@ -32,16 +32,18 @@ public class SupervisionServiceShellCommand extends ShellCommand {
return handleDefaultCommands(null);
}
switch (cmd) {
- case "enable": return setEnabled(true);
- case "disable": return setEnabled(false);
- default: return handleDefaultCommands(cmd);
+ case "enable":
+ return setEnabled(true);
+ case "disable":
+ return setEnabled(false);
+ default:
+ return handleDefaultCommands(cmd);
}
}
private int setEnabled(boolean enabled) {
- final var pw = getOutPrintWriter();
final var userId = UserHandle.parseUserArg(getNextArgRequired());
- mService.setSupervisionEnabledForUser(userId, enabled);
+ mService.mInternal.setSupervisionEnabledForUser(userId, enabled);
return 0;
}
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionUserData.java b/services/supervision/java/com/android/server/supervision/SupervisionUserData.java
index 1dd48f581bf4..06acb91509a1 100644
--- a/services/supervision/java/com/android/server/supervision/SupervisionUserData.java
+++ b/services/supervision/java/com/android/server/supervision/SupervisionUserData.java
@@ -26,6 +26,7 @@ import android.util.IndentingPrintWriter;
public class SupervisionUserData {
public final @UserIdInt int userId;
public boolean supervisionEnabled;
+ @Nullable public String supervisionAppPackage;
public boolean supervisionLockScreenEnabled;
@Nullable public PersistableBundle supervisionLockScreenOptions;
@@ -38,6 +39,7 @@ public class SupervisionUserData {
pw.println("User " + userId + ":");
pw.increaseIndent();
pw.println("supervisionEnabled: " + supervisionEnabled);
+ pw.println("supervisionAppPackage: " + supervisionAppPackage);
pw.println("supervisionLockScreenEnabled: " + supervisionLockScreenEnabled);
pw.println("supervisionLockScreenOptions: " + supervisionLockScreenOptions);
pw.decreaseIndent();
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayBackupHelperTest.kt b/services/tests/displayservicetests/src/com/android/server/display/DisplayBackupHelperTest.kt
new file mode 100644
index 000000000000..26060a406aa0
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayBackupHelperTest.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display
+
+import androidx.test.filters.SmallTest
+import android.hardware.display.DisplayManagerInternal
+import android.util.AtomicFileOutputStream
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.never
+
+@SmallTest
+class DisplayBackupHelperTest {
+ private val mockInjector = mock<DisplayBackupHelper.Injector>()
+ private val mockDmsInternal = mock<DisplayManagerInternal>()
+ private val mockWriteTopologyFile = mock<AtomicFileOutputStream>()
+ private val byteArray = byteArrayOf(0b00000001, 0b00000010, 0b00000011)
+ private val helper = createBackupHelper(0, byteArray)
+
+ @Test
+ fun testBackupDisplayReturnsBytes() {
+ assertThat(helper.getBackupPayload("display")).isEqualTo(byteArray)
+ }
+
+ @Test
+ fun testBackupSomethingReturnsNull() {
+ assertThat(helper.getBackupPayload("something")).isNull()
+ }
+
+ @Test
+ fun testBackupDisplayReturnsNullWhenFlagDisabled() {
+ whenever(mockInjector.isDisplayTopologyFlagEnabled()).thenReturn(false)
+ assertThat(helper.getBackupPayload("display")).isNull()
+ }
+
+ @Test
+ fun testRestoreDisplay() {
+ helper.applyRestoredPayload("display", byteArray)
+ verify(mockWriteTopologyFile).write(byteArray)
+ verify(mockWriteTopologyFile).markSuccess()
+ verify(mockDmsInternal).reloadTopologies(0)
+ }
+
+ @Test
+ fun testRestoreSomethingDoesNothing() {
+ helper.applyRestoredPayload("something", byteArray)
+ verify(mockWriteTopologyFile, never()).write(byteArray)
+ verify(mockWriteTopologyFile, never()).markSuccess()
+ verify(mockDmsInternal, never()).reloadTopologies(0)
+ }
+
+ @Test
+ fun testRestoreDisplayDoesNothingWhenFlagDisabled() {
+ whenever(mockInjector.isDisplayTopologyFlagEnabled()).thenReturn(false)
+ helper.applyRestoredPayload("display", byteArray)
+ verify(mockWriteTopologyFile, never()).write(byteArray)
+ verify(mockWriteTopologyFile, never()).markSuccess()
+ verify(mockDmsInternal, never()).reloadTopologies(0)
+ }
+
+ fun createBackupHelper(userId: Int, topologyToBackup: ByteArray): DisplayBackupHelper {
+ whenever(mockInjector.getDisplayManagerInternal()).thenReturn(mockDmsInternal)
+ whenever(mockInjector.readTopologyFile(userId)).thenReturn(topologyToBackup)
+ whenever(mockInjector.writeTopologyFile(userId)).thenReturn(mockWriteTopologyFile)
+ whenever(mockInjector.isDisplayTopologyFlagEnabled()).thenReturn(true)
+
+ return DisplayBackupHelper(userId, mockInjector)
+ }
+} \ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/plugin/PluginManagerTest.kt b/services/tests/displayservicetests/src/com/android/server/display/plugin/PluginManagerTest.kt
index 01061f16c279..d9224eaf66ca 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/plugin/PluginManagerTest.kt
+++ b/services/tests/displayservicetests/src/com/android/server/display/plugin/PluginManagerTest.kt
@@ -29,6 +29,7 @@ import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
private val TEST_PLUGIN_TYPE = PluginType(Int::class.java, "test_type")
+private val DISPLAY_ID = "display_id"
@SmallTest
class PluginManagerTest {
@@ -62,18 +63,18 @@ class PluginManagerTest {
fun testSubscribe() {
val pluginManager = createPluginManager()
- pluginManager.subscribe(TEST_PLUGIN_TYPE, mockListener)
+ pluginManager.subscribe(TEST_PLUGIN_TYPE, DISPLAY_ID, mockListener)
- verify(testInjector.mockStorage).addListener(TEST_PLUGIN_TYPE, mockListener)
+ verify(testInjector.mockStorage).addListener(TEST_PLUGIN_TYPE, DISPLAY_ID, mockListener)
}
@Test
fun testUnsubscribe() {
val pluginManager = createPluginManager()
- pluginManager.unsubscribe(TEST_PLUGIN_TYPE, mockListener)
+ pluginManager.unsubscribe(TEST_PLUGIN_TYPE, DISPLAY_ID, mockListener)
- verify(testInjector.mockStorage).removeListener(TEST_PLUGIN_TYPE, mockListener)
+ verify(testInjector.mockStorage).removeListener(TEST_PLUGIN_TYPE, DISPLAY_ID, mockListener)
}
private fun createPluginManager(enabled: Boolean = true): PluginManager {
diff --git a/services/tests/displayservicetests/src/com/android/server/display/plugin/PluginStorageTest.kt b/services/tests/displayservicetests/src/com/android/server/display/plugin/PluginStorageTest.kt
index 218e34134e93..8eb3e9fbf9b0 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/plugin/PluginStorageTest.kt
+++ b/services/tests/displayservicetests/src/com/android/server/display/plugin/PluginStorageTest.kt
@@ -23,6 +23,8 @@ import org.junit.Test
private val TEST_PLUGIN_TYPE1 = PluginType(String::class.java, "test_type1")
private val TEST_PLUGIN_TYPE2 = PluginType(String::class.java, "test_type2")
+private val DISPLAY_ID_1 = "display_1"
+private val DISPLAY_ID_2 = "display_2"
@SmallTest
class PluginStorageTest {
@@ -33,9 +35,9 @@ class PluginStorageTest {
fun testUpdateValue() {
val type1Value = "value1"
val testChangeListener = TestPluginChangeListener<String>()
- storage.addListener(TEST_PLUGIN_TYPE1, testChangeListener)
+ storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, testChangeListener)
- storage.updateValue(TEST_PLUGIN_TYPE1, type1Value)
+ storage.updateValue(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, type1Value)
assertThat(testChangeListener.receivedValue).isEqualTo(type1Value)
}
@@ -44,9 +46,9 @@ class PluginStorageTest {
fun testAddListener() {
val type1Value = "value1"
val testChangeListener = TestPluginChangeListener<String>()
- storage.updateValue(TEST_PLUGIN_TYPE1, type1Value)
+ storage.updateValue(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, type1Value)
- storage.addListener(TEST_PLUGIN_TYPE1, testChangeListener)
+ storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, testChangeListener)
assertThat(testChangeListener.receivedValue).isEqualTo(type1Value)
}
@@ -55,10 +57,10 @@ class PluginStorageTest {
fun testRemoveListener() {
val type1Value = "value1"
val testChangeListener = TestPluginChangeListener<String>()
- storage.addListener(TEST_PLUGIN_TYPE1, testChangeListener)
- storage.removeListener(TEST_PLUGIN_TYPE1, testChangeListener)
+ storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, testChangeListener)
+ storage.removeListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, testChangeListener)
- storage.updateValue(TEST_PLUGIN_TYPE1, type1Value)
+ storage.updateValue(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, type1Value)
assertThat(testChangeListener.receivedValue).isNull()
}
@@ -68,10 +70,10 @@ class PluginStorageTest {
val type1Value = "value1"
val type2Value = "value2"
val testChangeListener = TestPluginChangeListener<String>()
- storage.updateValue(TEST_PLUGIN_TYPE1, type1Value)
- storage.updateValue(TEST_PLUGIN_TYPE2, type2Value)
+ storage.updateValue(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, type1Value)
+ storage.updateValue(TEST_PLUGIN_TYPE2, DISPLAY_ID_1, type2Value)
- storage.addListener(TEST_PLUGIN_TYPE1, testChangeListener)
+ storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, testChangeListener)
assertThat(testChangeListener.receivedValue).isEqualTo(type1Value)
}
@@ -81,15 +83,62 @@ class PluginStorageTest {
val type1Value = "value1"
val testChangeListener1 = TestPluginChangeListener<String>()
val testChangeListener2 = TestPluginChangeListener<String>()
- storage.addListener(TEST_PLUGIN_TYPE1, testChangeListener1)
- storage.addListener(TEST_PLUGIN_TYPE2, testChangeListener2)
+ storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, testChangeListener1)
+ storage.addListener(TEST_PLUGIN_TYPE2, DISPLAY_ID_1, testChangeListener2)
- storage.updateValue(TEST_PLUGIN_TYPE1, type1Value)
+ storage.updateValue(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, type1Value)
assertThat(testChangeListener1.receivedValue).isEqualTo(type1Value)
assertThat(testChangeListener2.receivedValue).isNull()
}
+ @Test
+ fun testUpdateGlobal_noDisplaySpecificValue() {
+ val type1Value = "value1"
+ val testChangeListener1 = TestPluginChangeListener<String>()
+ val testChangeListener2 = TestPluginChangeListener<String>()
+ storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, testChangeListener1)
+ storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_2, testChangeListener2)
+
+ storage.updateGlobalValue(TEST_PLUGIN_TYPE1, type1Value)
+
+ assertThat(testChangeListener1.receivedValue).isEqualTo(type1Value)
+ assertThat(testChangeListener2.receivedValue).isEqualTo(type1Value)
+ }
+
+ @Test
+ fun testUpdateGlobal_withDisplaySpecificValue() {
+ val type1Value = "value1"
+ val type1GlobalValue = "value1Global"
+ val testChangeListener1 = TestPluginChangeListener<String>()
+ val testChangeListener2 = TestPluginChangeListener<String>()
+ storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, testChangeListener1)
+ storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_2, testChangeListener2)
+
+ storage.updateValue(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, type1Value)
+ storage.updateGlobalValue(TEST_PLUGIN_TYPE1, type1GlobalValue)
+
+ assertThat(testChangeListener1.receivedValue).isEqualTo(type1Value)
+ assertThat(testChangeListener2.receivedValue).isEqualTo(type1GlobalValue)
+ }
+
+ @Test
+ fun testUpdateGlobal_withDisplaySpecificValueRemoved() {
+ val type1Value = "value1"
+ val type1GlobalValue = "value1Global"
+ val testChangeListener1 = TestPluginChangeListener<String>()
+ val testChangeListener2 = TestPluginChangeListener<String>()
+ storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, testChangeListener1)
+ storage.addListener(TEST_PLUGIN_TYPE1, DISPLAY_ID_2, testChangeListener2)
+
+ storage.updateValue(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, type1Value)
+ storage.updateGlobalValue(TEST_PLUGIN_TYPE1, type1GlobalValue)
+ storage.updateValue(TEST_PLUGIN_TYPE1, DISPLAY_ID_1, null)
+
+ assertThat(testChangeListener1.receivedValue).isEqualTo(type1GlobalValue)
+ assertThat(testChangeListener2.receivedValue).isEqualTo(type1GlobalValue)
+ }
+
private class TestPluginChangeListener<T> : PluginChangeListener<T> {
var receivedValue: T? = null
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
index 8dc8c14f8948..cb52f1849b5b 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
@@ -151,6 +151,7 @@ import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.platform.test.flag.junit.SetFlagsRule;
import android.platform.test.flag.util.FlagSetException;
@@ -192,7 +193,9 @@ import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
+import org.mockito.InOrder;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.mockito.quality.Strictness;
import org.mockito.stubbing.Answer;
@@ -435,6 +438,7 @@ public final class AlarmManagerServiceTest {
private void disableFlagsNotSetByAnnotation() {
try {
mSetFlagsRule.disableFlags(Flags.FLAG_START_USER_BEFORE_SCHEDULED_ALARMS);
+ mSetFlagsRule.disableFlags(Flags.FLAG_ACQUIRE_WAKELOCK_BEFORE_SEND);
} catch (FlagSetException fse) {
// Expected if the test about to be run requires this enabled.
}
@@ -948,6 +952,28 @@ public final class AlarmManagerServiceTest {
}
@Test
+ @EnableFlags(Flags.FLAG_ACQUIRE_WAKELOCK_BEFORE_SEND)
+ public void testWakelockOrdering() throws Exception {
+ final long triggerTime = mNowElapsedTest + 5000;
+ final PendingIntent alarmPi = getNewMockPendingIntent();
+ setTestAlarm(ELAPSED_REALTIME_WAKEUP, triggerTime, alarmPi);
+
+ mNowElapsedTest = mTestTimer.getElapsed();
+ mTestTimer.expire();
+
+ final InOrder inOrder = Mockito.inOrder(alarmPi, mWakeLock);
+ inOrder.verify(mWakeLock).acquire();
+
+ final ArgumentCaptor<PendingIntent.OnFinished> onFinishedCaptor =
+ ArgumentCaptor.forClass(PendingIntent.OnFinished.class);
+ inOrder.verify(alarmPi).send(eq(mMockContext), eq(0), any(Intent.class),
+ onFinishedCaptor.capture(), any(Handler.class), isNull(), any());
+ onFinishedCaptor.getValue().onSendFinished(alarmPi, null, 0, null, null);
+
+ inOrder.verify(mWakeLock).release();
+ }
+
+ @Test
public void testMinFuturityCoreUid() {
setDeviceConfigLong(KEY_MIN_FUTURITY, 10L);
assertEquals(10, mService.mConstants.MIN_FUTURITY);
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java
index 27eada013642..89b48bad2358 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java
@@ -16,8 +16,6 @@
package com.android.server.am;
-import static android.os.PowerWhitelistManager.REASON_NOTIFICATION_SERVICE;
-import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED;
import static android.os.Process.INVALID_UID;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
@@ -29,11 +27,9 @@ import static com.android.server.am.PendingIntentRecord.CANCEL_REASON_OWNER_CANC
import static com.android.server.am.PendingIntentRecord.CANCEL_REASON_OWNER_FORCE_STOPPED;
import static com.android.server.am.PendingIntentRecord.CANCEL_REASON_SUPERSEDED;
import static com.android.server.am.PendingIntentRecord.CANCEL_REASON_USER_STOPPED;
-import static com.android.server.am.PendingIntentRecord.FLAG_ACTIVITY_SENDER;
import static com.android.server.am.PendingIntentRecord.cancelReasonToString;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
@@ -43,11 +39,9 @@ import static org.mockito.Mockito.when;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.AppGlobals;
-import android.app.BackgroundStartPrivileges;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.pm.IPackageManager;
-import android.os.Binder;
import android.os.Looper;
import android.os.UserHandle;
@@ -185,34 +179,6 @@ public class PendingIntentControllerTest {
}
}
- @Test
- public void testClearAllowBgActivityStartsClearsToken() {
- final PendingIntentRecord pir = createPendingIntentRecord(0);
- Binder token = new Binder();
- pir.setAllowBgActivityStarts(token, FLAG_ACTIVITY_SENDER);
- assertEquals(BackgroundStartPrivileges.allowBackgroundActivityStarts(token),
- pir.getBackgroundStartPrivilegesForActivitySender(token));
- pir.clearAllowBgActivityStarts(token);
- assertEquals(BackgroundStartPrivileges.NONE,
- pir.getBackgroundStartPrivilegesForActivitySender(token));
- }
-
- @Test
- public void testClearAllowBgActivityStartsClearsDuration() {
- final PendingIntentRecord pir = createPendingIntentRecord(0);
- Binder token = new Binder();
- pir.setAllowlistDurationLocked(token, 1000,
- TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED, REASON_NOTIFICATION_SERVICE,
- "NotificationManagerService");
- PendingIntentRecord.TempAllowListDuration allowlistDurationLocked =
- pir.getAllowlistDurationLocked(token);
- assertEquals(1000, allowlistDurationLocked.duration);
- pir.clearAllowBgActivityStarts(token);
- PendingIntentRecord.TempAllowListDuration allowlistDurationLockedAfterClear =
- pir.getAllowlistDurationLocked(token);
- assertNull(allowlistDurationLockedAfterClear);
- }
-
private void assertCancelReason(int expectedReason, int actualReason) {
final String errMsg = "Expected: " + cancelReasonToString(expectedReason)
+ "; Actual: " + cancelReasonToString(actualReason);
diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java
index 7e179095d99b..86bf203771ba 100644
--- a/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java
@@ -93,7 +93,8 @@ public class SystemBackupAgentTest {
"app_locales",
"app_gender",
"companion",
- "system_gender");
+ "system_gender",
+ "display");
}
@Test
@@ -118,7 +119,8 @@ public class SystemBackupAgentTest {
"app_locales",
"app_gender",
"companion",
- "system_gender");
+ "system_gender",
+ "display");
}
@Test
@@ -136,7 +138,8 @@ public class SystemBackupAgentTest {
"app_locales",
"companion",
"app_gender",
- "system_gender");
+ "system_gender",
+ "display");
}
@Test
@@ -158,7 +161,8 @@ public class SystemBackupAgentTest {
"shortcut_manager",
"companion",
"app_gender",
- "system_gender");
+ "system_gender",
+ "display");
}
@Test
diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
index 1cf655675a30..b92afc5c0ca7 100644
--- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
@@ -144,6 +144,8 @@ public class WallpaperManagerServiceTests {
private static ComponentName sImageWallpaperComponentName;
private static ComponentName sDefaultWallpaperComponent;
+ private static ComponentName sFallbackWallpaperComponentName;
+
private IPackageManager mIpm = AppGlobals.getPackageManager();
@Mock
@@ -195,6 +197,8 @@ public class WallpaperManagerServiceTests {
sContext.getResources().getString(R.string.image_wallpaper_component));
// Mock default wallpaper as image wallpaper if there is no pre-defined default wallpaper.
sDefaultWallpaperComponent = WallpaperManager.getCmfDefaultWallpaperComponent(sContext);
+ sFallbackWallpaperComponentName = ComponentName.unflattenFromString(
+ sContext.getResources().getString(R.string.fallback_wallpaper_component));
if (sDefaultWallpaperComponent == null) {
sDefaultWallpaperComponent = sImageWallpaperComponentName;
@@ -205,6 +209,9 @@ public class WallpaperManagerServiceTests {
}
sContext.addMockService(sImageWallpaperComponentName, sWallpaperService);
+ if (sFallbackWallpaperComponentName != null) {
+ sContext.addMockService(sFallbackWallpaperComponentName, sWallpaperService);
+ }
}
@AfterClass
@@ -216,6 +223,7 @@ public class WallpaperManagerServiceTests {
LocalServices.removeServiceForTest(WindowManagerInternal.class);
sImageWallpaperComponentName = null;
sDefaultWallpaperComponent = null;
+ sFallbackWallpaperComponentName = null;
reset(sContext);
}
@@ -306,6 +314,7 @@ public class WallpaperManagerServiceTests {
* Tests that internal basic data should be correct after boot up.
*/
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER)
public void testDataCorrectAfterBoot() {
mService.switchUser(USER_SYSTEM, null);
@@ -719,7 +728,7 @@ public class WallpaperManagerServiceTests {
final int testDisplayId = 2;
setUpDisplays(List.of(DEFAULT_DISPLAY, testDisplayId));
- // WHEN displayId, 2, is ready.
+ // WHEN display ID, 2, is ready.
WallpaperManagerInternal wallpaperManagerInternal = LocalServices.getService(
WallpaperManagerInternal.class);
wallpaperManagerInternal.onDisplayReady(testDisplayId);
@@ -759,7 +768,7 @@ public class WallpaperManagerServiceTests {
final int testDisplayId = 2;
setUpDisplays(List.of(DEFAULT_DISPLAY, testDisplayId));
- // WHEN displayId, 2, is ready.
+ // WHEN display ID, 2, is ready.
WallpaperManagerInternal wallpaperManagerInternal = LocalServices.getService(
WallpaperManagerInternal.class);
wallpaperManagerInternal.onDisplayReady(testDisplayId);
@@ -791,6 +800,40 @@ public class WallpaperManagerServiceTests {
/* info= */ any(),
/* description= */ any());
}
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER)
+ public void displayAdded_wallpaperIncompatibleForDisplay_shouldAttachFallbackWallpaperService()
+ throws Exception {
+ final int testUserId = USER_SYSTEM;
+ mService.switchUser(testUserId, null);
+ IWallpaperService mockIWallpaperService = mock(IWallpaperService.class);
+ mService.mFallbackWallpaper.connection.mService = mockIWallpaperService;
+ // GIVEN there are two displays: DEFAULT_DISPLAY, 2
+ final int testDisplayId = 2;
+ setUpDisplays(List.of(DEFAULT_DISPLAY, testDisplayId));
+ // GIVEN the wallpaper isn't compatible with display ID, 2
+ mService.removeWallpaperCompatibleDisplayForTest(testDisplayId);
+
+ // WHEN display ID, 2, is ready.
+ WallpaperManagerInternal wallpaperManagerInternal = LocalServices.getService(
+ WallpaperManagerInternal.class);
+ wallpaperManagerInternal.onDisplayReady(testDisplayId);
+
+ // Then there is a connection established for the fallback wallpaper for display ID, 2.
+ verify(mockIWallpaperService).attach(
+ /* connection= */ eq(mService.mFallbackWallpaper.connection),
+ /* windowToken= */ any(),
+ /* windowType= */ anyInt(),
+ /* isPreview= */ anyBoolean(),
+ /* reqWidth= */ anyInt(),
+ /* reqHeight= */ anyInt(),
+ /* padding= */ any(),
+ /* displayId= */ eq(testDisplayId),
+ /* which= */ eq(FLAG_SYSTEM | FLAG_LOCK),
+ /* info= */ any(),
+ /* description= */ any());
+ }
// Verify a secondary display added end
// Verify a secondary display removed started
@@ -810,7 +853,7 @@ public class WallpaperManagerServiceTests {
// GIVEN there are two displays: DEFAULT_DISPLAY, 2
final int testDisplayId = 2;
setUpDisplays(List.of(DEFAULT_DISPLAY, testDisplayId));
- // GIVEN wallpaper connections have been established for displayID, 2.
+ // GIVEN wallpaper connections have been established for display ID, 2.
WallpaperManagerInternal wallpaperManagerInternal = LocalServices.getService(
WallpaperManagerInternal.class);
wallpaperManagerInternal.onDisplayReady(testDisplayId);
@@ -818,11 +861,11 @@ public class WallpaperManagerServiceTests {
WallpaperManagerService.DisplayConnector displayConnector =
wallpaper.connection.getDisplayConnectorOrCreate(testDisplayId);
- // WHEN displayId, 2, is removed.
+ // WHEN display ID, 2, is removed.
DisplayListener displayListener = displayListenerCaptor.getValue();
displayListener.onDisplayRemoved(testDisplayId);
- // Then the wallpaper connection for displayId, 2, is detached.
+ // Then the wallpaper connection for display ID, 2, is detached.
verify(mockIWallpaperService).detach(eq(displayConnector.mToken));
}
@@ -848,26 +891,142 @@ public class WallpaperManagerServiceTests {
// GIVEN there are two displays: DEFAULT_DISPLAY, 2
final int testDisplayId = 2;
setUpDisplays(List.of(DEFAULT_DISPLAY, testDisplayId));
- // GIVEN wallpaper connections have been established for displayID, 2.
+ // GIVEN wallpaper connections have been established for display ID, 2.
WallpaperManagerInternal wallpaperManagerInternal = LocalServices.getService(
WallpaperManagerInternal.class);
wallpaperManagerInternal.onDisplayReady(testDisplayId);
- // Save displayConnectors for displayId 2 before display removal.
+ // Save displayConnectors for display ID, 2, before display removal.
WallpaperManagerService.DisplayConnector systemWallpaperDisplayConnector =
systemWallpaper.connection.getDisplayConnectorOrCreate(testDisplayId);
WallpaperManagerService.DisplayConnector lockWallpaperDisplayConnector =
lockWallpaper.connection.getDisplayConnectorOrCreate(testDisplayId);
+ // WHEN display ID, 2, is removed.
+ DisplayListener displayListener = displayListenerCaptor.getValue();
+ displayListener.onDisplayRemoved(testDisplayId);
+
+ // Then the system wallpaper connection for display ID, 2, is detached.
+ verify(mockIWallpaperService).detach(eq(systemWallpaperDisplayConnector.mToken));
+ // Then the lock wallpaper connection for display ID, 2, is detached.
+ verify(mockIWallpaperService).detach(eq(lockWallpaperDisplayConnector.mToken));
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER)
+ public void displayRemoved_fallbackWallpaper_shouldDetachFallbackWallpaperService()
+ throws Exception {
+ ArgumentCaptor<DisplayListener> displayListenerCaptor = ArgumentCaptor.forClass(
+ DisplayListener.class);
+ verify(mDisplayManager).registerDisplayListener(displayListenerCaptor.capture(), eq(null));
+ final int testUserId = USER_SYSTEM;
+ mService.switchUser(testUserId, null);
+ IWallpaperService mockIWallpaperService = mock(IWallpaperService.class);
+ mService.mFallbackWallpaper.connection.mService = mockIWallpaperService;
+ // GIVEN there are two displays: DEFAULT_DISPLAY, 2
+ final int testDisplayId = 2;
+ setUpDisplays(List.of(DEFAULT_DISPLAY, testDisplayId));
+ // GIVEN display ID, 2, is incompatible with the wallpaper.
+ mService.removeWallpaperCompatibleDisplayForTest(testDisplayId);
+ // GIVEN wallpaper connections have been established for display ID, 2.
+ WallpaperManagerInternal wallpaperManagerInternal = LocalServices.getService(
+ WallpaperManagerInternal.class);
+ wallpaperManagerInternal.onDisplayReady(testDisplayId);
+ // Save fallback wallpaper displayConnector for display ID, 2, before display removal.
+ WallpaperManagerService.DisplayConnector fallbackWallpaperConnector =
+ mService.mFallbackWallpaper.connection.getDisplayConnectorOrCreate(testDisplayId);
+
// WHEN displayId, 2, is removed.
DisplayListener displayListener = displayListenerCaptor.getValue();
displayListener.onDisplayRemoved(testDisplayId);
+ // Then the fallback wallpaper connection for display ID, 2, is detached.
+ verify(mockIWallpaperService).detach(eq(fallbackWallpaperConnector.mToken));
+ }
+ // Verify a secondary display removed ended
+
+ // Test fallback wallpaper after enabling connected display supports.
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER)
+ public void testFallbackWallpaperForConnectedDisplays() {
+ final WallpaperData fallbackData = mService.mFallbackWallpaper;
+
+ assertWithMessage(
+ "The connected wallpaper component should be fallback wallpaper component from "
+ + "config file.")
+ .that(fallbackData.connection.mWallpaper.getComponent())
+ .isEqualTo(sFallbackWallpaperComponentName);
+ assertWithMessage("Fallback wallpaper should support both lock & system.")
+ .that(fallbackData.mWhich)
+ .isEqualTo(FLAG_LOCK | FLAG_SYSTEM);
+ }
+
+ // Verify a secondary display removes system decorations started
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER)
+ public void displayRemoveSystemDecorations_sameSystemAndLockWallpaper_shouldDetachWallpaperServiceOnce()
+ throws Exception {
+ // GIVEN the same wallpaper used for the lock and system.
+ final int testUserId = USER_SYSTEM;
+ mService.switchUser(testUserId, null);
+ final WallpaperData wallpaper = mService.getCurrentWallpaperData(FLAG_SYSTEM, testUserId);
+ IWallpaperService mockIWallpaperService = mock(IWallpaperService.class);
+ wallpaper.connection.mService = mockIWallpaperService;
+ // GIVEN there are two displays: DEFAULT_DISPLAY, 2
+ final int testDisplayId = 2;
+ setUpDisplays(List.of(DEFAULT_DISPLAY, testDisplayId));
+ // GIVEN wallpaper connections have been established for displayID, 2.
+ WallpaperManagerInternal wallpaperManagerInternal = LocalServices.getService(
+ WallpaperManagerInternal.class);
+ wallpaperManagerInternal.onDisplayReady(testDisplayId);
+ // Save displayConnector for displayId 2 before display removal.
+ WallpaperManagerService.DisplayConnector displayConnector =
+ wallpaper.connection.getDisplayConnectorOrCreate(testDisplayId);
+
+ // WHEN displayId, 2, is removed.
+ wallpaperManagerInternal.onDisplayRemoveSystemDecorations(testDisplayId);
+
+ // Then the wallpaper connection for displayId, 2, is detached.
+ verify(mockIWallpaperService).detach(eq(displayConnector.mToken));
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER)
+ public void displayRemoveSystemDecorations_differentSystemAndLockWallpapers_shouldDetachWallpaperServiceTwice()
+ throws Exception {
+ // GIVEN different wallpapers used for the lock and system.
+ final int testUserId = USER_SYSTEM;
+ mService.switchUser(testUserId, null);
+ mService.setWallpaperComponent(sImageWallpaperComponentName, sContext.getOpPackageName(),
+ FLAG_LOCK, testUserId);
+ final WallpaperData systemWallpaper = mService.getCurrentWallpaperData(FLAG_SYSTEM,
+ testUserId);
+ final WallpaperData lockWallpaper = mService.getCurrentWallpaperData(FLAG_LOCK,
+ testUserId);
+ IWallpaperService mockIWallpaperService = mock(IWallpaperService.class);
+ systemWallpaper.connection.mService = mockIWallpaperService;
+ lockWallpaper.connection.mService = mockIWallpaperService;
+ // GIVEN there are two displays: DEFAULT_DISPLAY, 2
+ final int testDisplayId = 2;
+ setUpDisplays(List.of(DEFAULT_DISPLAY, testDisplayId));
+ // GIVEN wallpaper connections have been established for displayID, 2.
+ WallpaperManagerInternal wallpaperManagerInternal = LocalServices.getService(
+ WallpaperManagerInternal.class);
+ wallpaperManagerInternal.onDisplayReady(testDisplayId);
+ // Save displayConnectors for displayId 2 before display removal.
+ WallpaperManagerService.DisplayConnector systemWallpaperDisplayConnector =
+ systemWallpaper.connection.getDisplayConnectorOrCreate(testDisplayId);
+ WallpaperManagerService.DisplayConnector lockWallpaperDisplayConnector =
+ lockWallpaper.connection.getDisplayConnectorOrCreate(testDisplayId);
+
+ // WHEN displayId, 2, is removed.
+ wallpaperManagerInternal.onDisplayRemoveSystemDecorations(testDisplayId);
+
// Then the system wallpaper connection for displayId, 2, is detached.
verify(mockIWallpaperService).detach(eq(systemWallpaperDisplayConnector.mToken));
// Then the lock wallpaper connection for displayId, 2, is detached.
verify(mockIWallpaperService).detach(eq(lockWallpaperDisplayConnector.mToken));
}
- // Verify a secondary display removed ended
+ // Verify a secondary display removes system decorations ended
// Verify that after continue switch user from userId 0 to lastUserId, the wallpaper data for
// non-current user must not bind to wallpaper service.
@@ -893,11 +1052,11 @@ public class WallpaperManagerServiceTests {
final WallpaperData lastLockData = mService.mLastLockWallpaper;
assertWithMessage("Last wallpaper must not be null").that(lastLockData).isNotNull();
assertWithMessage("Last wallpaper component must be equals.")
- .that(expectedComponent)
- .isEqualTo(lastLockData.getComponent());
+ .that(lastLockData.getComponent())
+ .isEqualTo(expectedComponent);
assertWithMessage("The user id in last wallpaper should be the last switched user")
- .that(lastUserId)
- .isEqualTo(lastLockData.userId);
+ .that(lastLockData.userId)
+ .isEqualTo(lastUserId);
assertWithMessage("Must exist user data connection on last wallpaper data")
.that(lastLockData.connection)
.isNotNull();
@@ -946,6 +1105,7 @@ public class WallpaperManagerServiceTests {
doReturn(mockDisplay).when(mDisplayManager).getDisplay(eq(displayId));
doReturn(displayId).when(mockDisplay).getDisplayId();
doReturn(true).when(mockDisplay).hasAccess(anyInt());
+ mService.addWallpaperCompatibleDisplayForTest(displayId);
}
doReturn(mockDisplays).when(mDisplayManager).getDisplays();
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterTest.java
index ecc48bfc40e4..f55ca0c0b12b 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterTest.java
@@ -57,6 +57,7 @@ import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import com.android.server.LocalServices;
+import com.android.server.accessibility.autoclick.AutoclickController;
import com.android.server.accessibility.gestures.TouchExplorer;
import com.android.server.accessibility.magnification.FullScreenMagnificationGestureHandler;
import com.android.server.accessibility.magnification.MagnificationGestureHandler;
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
index 5602fb76e6f5..28e5be505556 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
@@ -68,7 +68,6 @@ import android.annotation.UserIdInt;
import android.app.PendingIntent;
import android.app.RemoteAction;
import android.app.admin.DevicePolicyManager;
-import android.app.ecm.EnhancedConfirmationManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
@@ -2120,23 +2119,6 @@ public class AccessibilityManagerServiceTest {
}
@Test
- @EnableFlags({android.permission.flags.Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED,
- android.security.Flags.FLAG_EXTEND_ECM_TO_ALL_SETTINGS})
- public void isAccessibilityTargetAllowed_nonSystemUserId_useEcmWithNonSystemUserId() {
- String fakePackageName = "FAKE_PACKAGE_NAME";
- int uid = 0; // uid is not used in the actual implementation when flags are on
- int userId = mTestableContext.getUserId() + 1234;
- when(mDevicePolicyManager.getPermittedAccessibilityServices(userId)).thenReturn(
- List.of(fakePackageName));
- Context mockUserContext = mock(Context.class);
- mTestableContext.addMockUserContext(userId, mockUserContext);
-
- mA11yms.isAccessibilityTargetAllowed(fakePackageName, uid, userId);
-
- verify(mockUserContext).getSystemService(EnhancedConfirmationManager.class);
- }
-
- @Test
@EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES)
public void handleKeyGestureEvent_toggleMagnifier() {
mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY);
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AutoclickControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
index acce813ff659..f02bdae1d9e6 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AutoclickControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.server.accessibility;
+package com.android.server.accessibility.autoclick;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
@@ -39,6 +39,8 @@ import android.view.MotionEvent;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityManager;
+import com.android.server.accessibility.AccessibilityTraceManager;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
diff --git a/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java
index ef9580c54de6..cbe2bfb26cd6 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java
@@ -28,8 +28,10 @@ import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.Manifest;
import android.app.AppOpsManager;
import android.content.Context;
+import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceInfo;
@@ -38,6 +40,7 @@ import android.media.AudioManager;
import android.media.AudioSystem;
import android.media.IAudioDeviceVolumeDispatcher;
import android.media.VolumeInfo;
+import android.os.IpcDataCache;
import android.os.PermissionEnforcer;
import android.os.RemoteException;
import android.os.test.TestLooper;
@@ -83,6 +86,7 @@ public class AbsoluteVolumeBehaviorTest {
@Before
public void setUp() throws Exception {
+ IpcDataCache.disableForTestMode();
mTestLooper = new TestLooper();
mContext = spy(ApplicationProvider.getApplicationContext());
@@ -93,6 +97,10 @@ public class AbsoluteVolumeBehaviorTest {
when(mResources.getBoolean(com.android.internal.R.bool.config_useFixedVolume))
.thenReturn(false);
+ when(mContext.checkCallingOrSelfPermission(
+ Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED))
+ .thenReturn(PackageManager.PERMISSION_GRANTED);
+
mSpyAudioSystem = spy(new NoOpAudioSystemAdapter());
mSystemServer = new NoOpSystemServerAdapter();
mSettingsAdapter = new NoOpSettingsAdapter();
diff --git a/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java
index 746645a8c585..541dbba67957 100644
--- a/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java
@@ -28,6 +28,7 @@ import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.IDeviceVolumeBehaviorDispatcher;
+import android.os.IpcDataCache;
import android.os.PermissionEnforcer;
import android.os.test.TestLooper;
import android.platform.test.annotations.Presubmit;
@@ -69,6 +70,7 @@ public class DeviceVolumeBehaviorTest {
@Before
public void setUp() throws Exception {
+ IpcDataCache.disableForTestMode();
mTestLooper = new TestLooper();
mContext = InstrumentationRegistry.getTargetContext();
mAudioSystem = new NoOpAudioSystemAdapter();
diff --git a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
index 6b41c434b80f..0bbae247d8bb 100644
--- a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
@@ -80,6 +80,7 @@ import android.media.AudioSystem;
import android.media.IDeviceVolumeBehaviorDispatcher;
import android.media.VolumeInfo;
import android.media.audiopolicy.AudioVolumeGroup;
+import android.os.IpcDataCache;
import android.os.Looper;
import android.os.PermissionEnforcer;
import android.os.test.TestLooper;
@@ -210,6 +211,8 @@ public class VolumeHelperTest {
@Before
public void setUp() throws Exception {
+ IpcDataCache.disableForTestMode();
+
mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
mTestLooper = new TestLooper();
diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
index 3ced56a04138..a58a9cd2a28f 100644
--- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
@@ -34,7 +34,6 @@ import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_DISPLAY;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -53,15 +52,11 @@ import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertThrows;
-import android.Manifest;
import android.annotation.SuppressLint;
import android.app.ActivityManagerInternal;
import android.app.ActivityOptions.LaunchCookie;
import android.app.AppOpsManager;
-import android.app.Instrumentation;
import android.app.KeyguardManager;
-import android.app.role.RoleManager;
-import android.companion.AssociationRequest;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
@@ -76,11 +71,9 @@ import android.media.projection.StopReason;
import android.os.Binder;
import android.os.IBinder;
import android.os.Looper;
-import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.test.TestLooper;
-import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.Settings;
@@ -99,7 +92,6 @@ import com.android.server.testutils.OffsettableClock;
import com.android.server.wm.WindowManagerInternal;
import org.junit.After;
-import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -110,7 +102,6 @@ import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -292,8 +283,6 @@ public class MediaProjectionManagerServiceTest {
assertThat(stoppedCallback2).isFalse();
}
- @EnableFlags(android.companion.virtualdevice.flags
- .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
@Test
public void testCreateProjection_keyguardLocked() throws Exception {
MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
@@ -308,8 +297,6 @@ public class MediaProjectionManagerServiceTest {
assertThat(mIMediaProjectionCallback.mLatch.await(5, TimeUnit.SECONDS)).isTrue();
}
- @EnableFlags(android.companion.virtualdevice.flags
- .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
@Test
public void testCreateProjection_keyguardLocked_packageAllowlisted()
throws NameNotFoundException {
@@ -325,8 +312,6 @@ public class MediaProjectionManagerServiceTest {
assertThat(mService.getActiveProjectionInfo()).isNotNull();
}
- @EnableFlags(android.companion.virtualdevice.flags
- .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
@Test
public void testCreateProjection_keyguardLocked_AppOpMediaProjection()
throws NameNotFoundException {
@@ -347,50 +332,6 @@ public class MediaProjectionManagerServiceTest {
assertThat(mService.getActiveProjectionInfo()).isNotNull();
}
- @EnableFlags(android.companion.virtualdevice.flags
- .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
- @Test
- public void testCreateProjection_keyguardLocked_RoleHeld() {
- runWithRole(
- AssociationRequest.DEVICE_PROFILE_APP_STREAMING,
- () -> {
- try {
- mAppInfo.privateFlags |= PRIVATE_FLAG_PRIVILEGED;
- doReturn(mAppInfo)
- .when(mPackageManager)
- .getApplicationInfoAsUser(
- anyString(),
- any(ApplicationInfoFlags.class),
- any(UserHandle.class));
- MediaProjectionManagerService.MediaProjection projection =
- mService.createProjectionInternal(
- Process.myUid(),
- mContext.getPackageName(),
- TYPE_MIRRORING,
- /* isPermanentGrant= */ false,
- UserHandle.CURRENT,
- DEFAULT_DISPLAY);
- doReturn(true).when(mKeyguardManager).isKeyguardLocked();
- doReturn(PackageManager.PERMISSION_DENIED)
- .when(mPackageManager)
- .checkPermission(RECORD_SENSITIVE_CONTENT, projection.packageName);
-
- projection.start(mIMediaProjectionCallback);
- projection.notifyVirtualDisplayCreated(10);
-
- // The projection was started because it was allowed to capture the
- // keyguard.
- assertWithMessage("Failed to run projection")
- .that(mService.getActiveProjectionInfo())
- .isNotNull();
- } catch (NameNotFoundException e) {
- throw new RuntimeException(e);
- }
- });
- }
-
- @EnableFlags(android.companion.virtualdevice.flags
- .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
@Test
public void testCreateProjection_keyguardLocked_screenshareProtectionsDisabled()
throws NameNotFoundException {
@@ -416,8 +357,6 @@ public class MediaProjectionManagerServiceTest {
}
}
- @EnableFlags(android.companion.virtualdevice.flags
- .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
@Test
public void testCreateProjection_keyguardLocked_noDisplayCreated()
throws NameNotFoundException {
@@ -509,8 +448,6 @@ public class MediaProjectionManagerServiceTest {
assertThat(secondProjection).isNotEqualTo(projection);
}
- @EnableFlags(android.companion.virtualdevice.flags
- .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
@Test
public void testReuseProjection_keyguardNotLocked_startConsentDialog()
throws NameNotFoundException {
@@ -527,8 +464,6 @@ public class MediaProjectionManagerServiceTest {
verify(mContext).startActivityAsUser(any(), any());
}
- @EnableFlags(android.companion.virtualdevice.flags
- .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
@Test
public void testReuseProjection_keyguardLocked_noConsentDialog() throws NameNotFoundException {
MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
@@ -1302,48 +1237,6 @@ public class MediaProjectionManagerServiceTest {
return mService.getProjectionInternal(UID, PACKAGE_NAME);
}
- /**
- * Run the provided block giving the current context's package the provided role.
- */
- @SuppressWarnings("SameParameterValue")
- private void runWithRole(String role, Runnable block) {
- Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
- String packageName = mContext.getPackageName();
- UserHandle user = instrumentation.getTargetContext().getUser();
- RoleManager roleManager = Objects.requireNonNull(
- mContext.getSystemService(RoleManager.class));
- try {
- CountDownLatch latch = new CountDownLatch(1);
- instrumentation.getUiAutomation().adoptShellPermissionIdentity(
- Manifest.permission.MANAGE_ROLE_HOLDERS,
- Manifest.permission.BYPASS_ROLE_QUALIFICATION);
-
- roleManager.setBypassingRoleQualification(true);
- roleManager.addRoleHolderAsUser(role, packageName,
- /* flags= */ RoleManager.MANAGE_HOLDERS_FLAG_DONT_KILL_APP, user,
- mContext.getMainExecutor(), success -> {
- if (success) {
- latch.countDown();
- } else {
- Assert.fail("Couldn't set role for test (failure) " + role);
- }
- });
- assertWithMessage("Couldn't set role for test (timeout) : " + role)
- .that(latch.await(1, TimeUnit.SECONDS)).isTrue();
- block.run();
-
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- } finally {
- roleManager.removeRoleHolderAsUser(role, packageName,
- /* flags= */ RoleManager.MANAGE_HOLDERS_FLAG_DONT_KILL_APP, user,
- mContext.getMainExecutor(), (aBool) -> {});
- roleManager.setBypassingRoleQualification(false);
- instrumentation.getUiAutomation()
- .dropShellPermissionIdentity();
- }
- }
-
private static class FakeIMediaProjectionCallback extends IMediaProjectionCallback.Stub {
CountDownLatch mLatch = new CountDownLatch(1);
@Override
diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionStopControllerTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionStopControllerTest.java
index affcfc14034e..10ac0495d69a 100644
--- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionStopControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionStopControllerTest.java
@@ -22,7 +22,6 @@ import static android.provider.Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_
import static android.view.Display.INVALID_DISPLAY;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -37,13 +36,10 @@ import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import android.Manifest;
import android.annotation.SuppressLint;
import android.app.ActivityManagerInternal;
import android.app.AppOpsManager;
-import android.app.Instrumentation;
import android.app.KeyguardManager;
-import android.app.role.RoleManager;
import android.companion.AssociationRequest;
import android.content.Context;
import android.content.pm.ApplicationInfo;
@@ -69,7 +65,6 @@ import com.android.server.SystemConfig;
import com.android.server.wm.WindowManagerInternal;
import org.junit.After;
-import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -79,9 +74,7 @@ import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
-import java.util.Objects;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
+import java.util.List;
import java.util.function.Consumer;
/**
@@ -123,6 +116,8 @@ public class MediaProjectionStopControllerTest {
private KeyguardManager mKeyguardManager;
@Mock
private TelecomManager mTelecomManager;
+ @Mock
+ private MediaProjectionStopController.RoleHolderProvider mRoleManager;
private AppOpsManager mAppOpsManager;
@Mock
@@ -145,7 +140,7 @@ public class MediaProjectionStopControllerTest {
mContext.addMockSystemService(TelecomManager.class, mTelecomManager);
mContext.setMockPackageManager(mPackageManager);
- mStopController = new MediaProjectionStopController(mContext, mStopConsumer);
+ mStopController = new MediaProjectionStopController(mContext, mStopConsumer, mRoleManager);
mService = new MediaProjectionManagerService(mContext,
mMediaProjectionMetricsLoggerInjector);
@@ -170,8 +165,6 @@ public class MediaProjectionStopControllerTest {
}
@Test
- @EnableFlags(
- android.companion.virtualdevice.flags.Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
public void testMediaProjectionNotRestricted() throws Exception {
when(mKeyguardManager.isKeyguardLocked()).thenReturn(false);
@@ -180,8 +173,6 @@ public class MediaProjectionStopControllerTest {
}
@Test
- @EnableFlags(
- android.companion.virtualdevice.flags.Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
public void testMediaProjectionRestricted() throws Exception {
MediaProjectionManagerService.MediaProjection mediaProjection = createMediaProjection();
mediaProjection.notifyVirtualDisplayCreated(1);
@@ -239,21 +230,13 @@ public class MediaProjectionStopControllerTest {
@Test
public void testExemptFromStoppingHasAppStreamingRole() throws Exception {
- runWithRole(
- AssociationRequest.DEVICE_PROFILE_APP_STREAMING,
- () -> {
- try {
- MediaProjectionManagerService.MediaProjection mediaProjection =
- createMediaProjection();
- doReturn(PackageManager.PERMISSION_DENIED).when(
- mPackageManager).checkPermission(
- RECORD_SENSITIVE_CONTENT, mediaProjection.packageName);
- assertThat(mStopController.isExemptFromStopping(mediaProjection,
- MediaProjectionStopController.STOP_REASON_UNKNOWN)).isTrue();
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- });
+ MediaProjectionManagerService.MediaProjection mediaProjection = createMediaProjection();
+ doReturn(PackageManager.PERMISSION_DENIED).when(mPackageManager).checkPermission(
+ RECORD_SENSITIVE_CONTENT, mediaProjection.packageName);
+ doReturn(List.of(mediaProjection.packageName)).when(mRoleManager).getRoleHoldersAsUser(
+ eq(AssociationRequest.DEVICE_PROFILE_APP_STREAMING), any(UserHandle.class));
+ assertThat(mStopController.isExemptFromStopping(mediaProjection,
+ MediaProjectionStopController.STOP_REASON_UNKNOWN)).isTrue();
}
@Test
@@ -300,8 +283,22 @@ public class MediaProjectionStopControllerTest {
}
@Test
- @EnableFlags(
- android.companion.virtualdevice.flags.Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
+ public void isStopReasonCallEnd_stopReasonCallEnd_returnsTrue() {
+ boolean result =
+ mStopController.isStopReasonCallEnd(
+ MediaProjectionStopController.STOP_REASON_CALL_END);
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ public void isStopReasonCallEnd_stopReasonKeyguard_returnsFalse() {
+ boolean result =
+ mStopController.isStopReasonCallEnd(
+ MediaProjectionStopController.STOP_REASON_KEYGUARD);
+ assertThat(result).isFalse();
+ }
+
+ @Test
public void testKeyguardLockedStateChanged_unlocked() {
mStopController.onKeyguardLockedStateChanged(false);
@@ -309,8 +306,6 @@ public class MediaProjectionStopControllerTest {
}
@Test
- @EnableFlags(
- android.companion.virtualdevice.flags.Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
public void testKeyguardLockedStateChanged_locked() {
mStopController.onKeyguardLockedStateChanged(true);
@@ -422,47 +417,4 @@ public class MediaProjectionStopControllerTest {
MediaProjectionManager.TYPE_SCREEN_CAPTURE, false, mContext.getUser(),
INVALID_DISPLAY);
}
-
- /**
- * Run the provided block giving the current context's package the provided role.
- */
- @SuppressWarnings("SameParameterValue")
- private void runWithRole(String role, Runnable block) {
- Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
- String packageName = mContext.getPackageName();
- UserHandle user = instrumentation.getTargetContext().getUser();
- RoleManager roleManager = Objects.requireNonNull(
- mContext.getSystemService(RoleManager.class));
- try {
- CountDownLatch latch = new CountDownLatch(1);
- instrumentation.getUiAutomation().adoptShellPermissionIdentity(
- Manifest.permission.MANAGE_ROLE_HOLDERS,
- Manifest.permission.BYPASS_ROLE_QUALIFICATION);
-
- roleManager.setBypassingRoleQualification(true);
- roleManager.addRoleHolderAsUser(role, packageName,
- /* flags= */ RoleManager.MANAGE_HOLDERS_FLAG_DONT_KILL_APP, user,
- mContext.getMainExecutor(), success -> {
- if (success) {
- latch.countDown();
- } else {
- Assert.fail("Couldn't set role for test (failure) " + role);
- }
- });
- assertWithMessage("Couldn't set role for test (timeout) : " + role)
- .that(latch.await(1, TimeUnit.SECONDS)).isTrue();
- block.run();
-
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- } finally {
- roleManager.removeRoleHolderAsUser(role, packageName,
- /* flags= */ RoleManager.MANAGE_HOLDERS_FLAG_DONT_KILL_APP, user,
- mContext.getMainExecutor(), (aBool) -> {
- });
- roleManager.setBypassingRoleQualification(false);
- instrumentation.getUiAutomation()
- .dropShellPermissionIdentity();
- }
- }
}
diff --git a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java
index 2f3bca031f46..fbb673a194b4 100644
--- a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java
@@ -1762,7 +1762,8 @@ public final class DataManagerTest {
/* rankingAdjustment= */ 0,
/* isBubble= */ false,
/* proposedImportance= */ 0,
- /* sensitiveContent= */ false
+ /* sensitiveContent= */ false,
+ /* summarization = */ null
);
return true;
}).when(mRankingMap).getRanking(eq(key),
@@ -1806,7 +1807,8 @@ public final class DataManagerTest {
/* rankingAdjustment= */ 0,
/* isBubble= */ false,
/* proposedImportance= */ 0,
- /* sensitiveContent= */ false
+ /* sensitiveContent= */ false,
+ /* summarization = */ null
);
return true;
}).when(mRankingMap).getRanking(eq(CUSTOM_KEY),
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java
index 5c2132f0e0e9..1c0ee09ccc6f 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java
@@ -336,7 +336,7 @@ public class ShortcutManagerTest8 extends BaseShortcutManagerTest {
checkRequestPinShortcut(makeResultIntent());
}
- public void testRequestPinShortcut_explicitTargetActivity() {
+ public void disabled_testRequestPinShortcut_explicitTargetActivity() {
setDefaultLauncher(USER_10, LAUNCHER_1);
setDefaultLauncher(USER_11, LAUNCHER_2);
@@ -475,7 +475,7 @@ public class ShortcutManagerTest8 extends BaseShortcutManagerTest {
}
- public void testRequestPinShortcut_dynamicExists() {
+ public void disabled_testRequestPinShortcut_dynamicExists() {
setDefaultLauncher(USER_10, LAUNCHER_1);
final Icon res32x32 = Icon.createWithResource(getTestContext(), R.drawable.black_32x32);
@@ -590,7 +590,7 @@ public class ShortcutManagerTest8 extends BaseShortcutManagerTest {
});
}
- public void testRequestPinShortcut_dynamicExists_alreadyPinned() {
+ public void disabled_testRequestPinShortcut_dynamicExists_alreadyPinned() {
setDefaultLauncher(USER_10, LAUNCHER_1);
final Icon res32x32 = Icon.createWithResource(getTestContext(), R.drawable.black_32x32);
@@ -848,7 +848,7 @@ public class ShortcutManagerTest8 extends BaseShortcutManagerTest {
});
}
- public void testRequestPinShortcut_dynamicExists_alreadyPinnedByAnother() {
+ public void disabled_testRequestPinShortcut_dynamicExists_alreadyPinnedByAnother() {
// Initially all launchers have the shortcut permission, until we call setDefaultLauncher().
runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
@@ -1041,7 +1041,7 @@ public class ShortcutManagerTest8 extends BaseShortcutManagerTest {
/**
* When trying to pin an existing shortcut, the new fields shouldn't override existing fields.
*/
- public void testRequestPinShortcut_dynamicExists_titleWontChange() {
+ public void disabled_testRequestPinShortcut_dynamicExists_titleWontChange() {
setDefaultLauncher(USER_10, LAUNCHER_1);
final Icon res32x32 = Icon.createWithResource(getTestContext(), R.drawable.black_32x32);
@@ -1173,7 +1173,7 @@ public class ShortcutManagerTest8 extends BaseShortcutManagerTest {
* The dynamic shortcut existed, but before accepting(), it's removed. Because the request
* has a partial shortcut, accept() should fail.
*/
- public void testRequestPinShortcut_dynamicExists_thenRemoved_error() {
+ public void disabled_testRequestPinShortcut_dynamicExists_thenRemoved_error() {
setDefaultLauncher(USER_10, LAUNCHER_1);
runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
@@ -1231,7 +1231,7 @@ public class ShortcutManagerTest8 extends BaseShortcutManagerTest {
* The dynamic shortcut existed, but before accepting(), it's removed. Because the request
* has all the mandatory fields, we can go ahead and still publish it.
*/
- public void testRequestPinShortcut_dynamicExists_thenRemoved_okay() {
+ public void disabled_testRequestPinShortcut_dynamicExists_thenRemoved_okay() {
setDefaultLauncher(USER_10, LAUNCHER_1);
runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
@@ -1404,7 +1404,7 @@ public class ShortcutManagerTest8 extends BaseShortcutManagerTest {
* The dynamic shortcut existed, but before accepting(), it's removed. Because the request
* has a partial shortcut, accept() should fail.
*/
- public void testRequestPinShortcut_dynamicExists_thenDisabled_error() {
+ public void disabled_testRequestPinShortcut_dynamicExists_thenDisabled_error() {
setDefaultLauncher(USER_10, LAUNCHER_1);
runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
diff --git a/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt
index 5862ac65eba9..af50effb7c8e 100644
--- a/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt
+++ b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt
@@ -92,6 +92,21 @@ class SupervisionServiceTest {
simulateUserStarting(USER_ID)
assertThat(service.isSupervisionEnabledForUser(USER_ID)).isTrue()
+ assertThat(service.getActiveSupervisionAppPackage(USER_ID))
+ .isEqualTo(systemSupervisionPackage)
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SYNC_WITH_DPM)
+ fun onUserStarting_legacyProfileOwnerComponent_enablesSupervision() {
+ whenever(mockDpmInternal.getProfileOwnerAsUser(USER_ID))
+ .thenReturn(supervisionProfileOwnerComponent)
+
+ simulateUserStarting(USER_ID)
+
+ assertThat(service.isSupervisionEnabledForUser(USER_ID)).isTrue()
+ assertThat(service.getActiveSupervisionAppPackage(USER_ID))
+ .isEqualTo(supervisionProfileOwnerComponent.packageName)
}
@Test
@@ -103,6 +118,7 @@ class SupervisionServiceTest {
simulateUserStarting(USER_ID, preCreated = true)
assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse()
+ assertThat(service.getActiveSupervisionAppPackage(USER_ID)).isNull()
}
@Test
@@ -114,6 +130,7 @@ class SupervisionServiceTest {
simulateUserStarting(USER_ID)
assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse()
+ assertThat(service.getActiveSupervisionAppPackage(USER_ID)).isNull()
}
@Test
@@ -125,6 +142,21 @@ class SupervisionServiceTest {
broadcastProfileOwnerChanged(USER_ID)
assertThat(service.isSupervisionEnabledForUser(USER_ID)).isTrue()
+ assertThat(service.getActiveSupervisionAppPackage(USER_ID))
+ .isEqualTo(systemSupervisionPackage)
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_SYNC_WITH_DPM)
+ fun profileOwnerChanged_legacyProfileOwnerComponent_enablesSupervision() {
+ whenever(mockDpmInternal.getProfileOwnerAsUser(USER_ID))
+ .thenReturn(supervisionProfileOwnerComponent)
+
+ broadcastProfileOwnerChanged(USER_ID)
+
+ assertThat(service.isSupervisionEnabledForUser(USER_ID)).isTrue()
+ assertThat(service.getActiveSupervisionAppPackage(USER_ID))
+ .isEqualTo(supervisionProfileOwnerComponent.packageName)
}
@Test
@@ -136,13 +168,14 @@ class SupervisionServiceTest {
broadcastProfileOwnerChanged(USER_ID)
assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse()
+ assertThat(service.getActiveSupervisionAppPackage(USER_ID)).isNull()
}
@Test
fun isActiveSupervisionApp_supervisionUid_supervisionEnabled_returnsTrue() {
whenever(mockPackageManager.getPackagesForUid(APP_UID))
.thenReturn(arrayOf(systemSupervisionPackage))
- service.setSupervisionEnabledForUser(USER_ID, true)
+ service.mInternal.setSupervisionEnabledForUser(USER_ID, true)
assertThat(service.mInternal.isActiveSupervisionApp(APP_UID)).isTrue()
}
@@ -151,7 +184,7 @@ class SupervisionServiceTest {
fun isActiveSupervisionApp_supervisionUid_supervisionNotEnabled_returnsFalse() {
whenever(mockPackageManager.getPackagesForUid(APP_UID))
.thenReturn(arrayOf(systemSupervisionPackage))
- service.setSupervisionEnabledForUser(USER_ID, false)
+ service.mInternal.setSupervisionEnabledForUser(USER_ID, false)
assertThat(service.mInternal.isActiveSupervisionApp(APP_UID)).isFalse()
}
@@ -167,15 +200,15 @@ class SupervisionServiceTest {
fun setSupervisionEnabledForUser() {
assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse()
- service.setSupervisionEnabledForUser(USER_ID, true)
+ service.mInternal.setSupervisionEnabledForUser(USER_ID, true)
assertThat(service.isSupervisionEnabledForUser(USER_ID)).isTrue()
- service.setSupervisionEnabledForUser(USER_ID, false)
+ service.mInternal.setSupervisionEnabledForUser(USER_ID, false)
assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse()
}
@Test
- fun supervisionEnabledForUser_internal() {
+ fun setSupervisionEnabledForUser_internal() {
assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse()
service.mInternal.setSupervisionEnabledForUser(USER_ID, true)
@@ -205,6 +238,13 @@ class SupervisionServiceTest {
private val systemSupervisionPackage: String
get() = context.getResources().getString(R.string.config_systemSupervision)
+ private val supervisionProfileOwnerComponent: ComponentName
+ get() =
+ context
+ .getResources()
+ .getString(R.string.config_defaultSupervisionProfileOwnerComponent)
+ .let(ComponentName::unflattenFromString)!!
+
private fun simulateUserStarting(userId: Int, preCreated: Boolean = false) {
val userInfo = UserInfo(userId, /* name= */ "tempUser", /* flags= */ 0)
userInfo.preCreated = preCreated
diff --git a/services/tests/uiservicestests/Android.bp b/services/tests/uiservicestests/Android.bp
index a63a38da3740..0eb20eb22380 100644
--- a/services/tests/uiservicestests/Android.bp
+++ b/services/tests/uiservicestests/Android.bp
@@ -20,6 +20,7 @@ android_test {
],
static_libs: [
+ "compatibility-device-util-axt-minus-dexmaker",
"frameworks-base-testutils",
"services.accessibility",
"services.core",
diff --git a/services/tests/uiservicestests/AndroidManifest.xml b/services/tests/uiservicestests/AndroidManifest.xml
index 4315254f68a9..69f17757b367 100644
--- a/services/tests/uiservicestests/AndroidManifest.xml
+++ b/services/tests/uiservicestests/AndroidManifest.xml
@@ -45,6 +45,8 @@
<provider android:name=".DummyProvider"
android:authorities="com.android.services.uitests" />
+ <activity android:name="android.app.ExampleActivity" />
+
</application>
<instrumentation
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeUserActionsViewModelKosmos.kt b/services/tests/uiservicestests/src/android/app/ExampleActivity.java
index 6345c4076412..58395e4d75e1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeUserActionsViewModelKosmos.kt
+++ b/services/tests/uiservicestests/src/android/app/ExampleActivity.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2024 The Android Open Source Project
+ * Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,11 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.shade.ui.viewmodel
+package android.app;
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeUserActionsViewModel
-
-val Kosmos.notificationsShadeUserActionsViewModel:
- NotificationsShadeUserActionsViewModel by Fixture { NotificationsShadeUserActionsViewModel() }
+public class ExampleActivity extends Activity {
+}
diff --git a/services/tests/uiservicestests/src/android/app/NotificationManagerZenTest.java b/services/tests/uiservicestests/src/android/app/NotificationManagerZenTest.java
new file mode 100644
index 000000000000..779fa1aa2f72
--- /dev/null
+++ b/services/tests/uiservicestests/src/android/app/NotificationManagerZenTest.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
+import static android.app.NotificationSystemUtil.runAsSystemUi;
+import static android.app.NotificationSystemUtil.toggleNotificationPolicyAccess;
+import static android.service.notification.Condition.STATE_FALSE;
+import static android.service.notification.Condition.STATE_TRUE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.net.Uri;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.service.notification.Condition;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Map;
+
+@RunWith(AndroidJUnit4.class)
+public class NotificationManagerZenTest {
+
+ private Context mContext;
+ private NotificationManager mNotificationManager;
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ @Before
+ public void setUp() throws Exception {
+ mContext = ApplicationProvider.getApplicationContext();
+ mNotificationManager = mContext.getSystemService(NotificationManager.class);
+
+ toggleNotificationPolicyAccess(mContext, mContext.getPackageName(), true);
+ runAsSystemUi(() -> mNotificationManager.setInterruptionFilter(INTERRUPTION_FILTER_ALL));
+ removeAutomaticZenRules();
+ }
+
+ @After
+ public void tearDown() {
+ runAsSystemUi(() -> mNotificationManager.setInterruptionFilter(INTERRUPTION_FILTER_ALL));
+ removeAutomaticZenRules();
+ }
+
+ private void removeAutomaticZenRules() {
+ // Delete AZRs created by this test (query "as app", then delete "as system" so they are
+ // not preserved to be restored later).
+ Map<String, AutomaticZenRule> rules = mNotificationManager.getAutomaticZenRules();
+ runAsSystemUi(() -> {
+ for (String ruleId : rules.keySet()) {
+ mNotificationManager.removeAutomaticZenRule(ruleId);
+ }
+ });
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+ public void setAutomaticZenRuleState_manualActivation() {
+ AutomaticZenRule ruleToCreate = createZenRule("rule");
+ String ruleId = mNotificationManager.addAutomaticZenRule(ruleToCreate);
+ Condition manualActivate = new Condition(ruleToCreate.getConditionId(), "manual-on",
+ STATE_TRUE, Condition.SOURCE_USER_ACTION);
+ Condition manualDeactivate = new Condition(ruleToCreate.getConditionId(), "manual-off",
+ STATE_FALSE, Condition.SOURCE_USER_ACTION);
+ Condition autoActivate = new Condition(ruleToCreate.getConditionId(), "auto-on",
+ STATE_TRUE);
+ Condition autoDeactivate = new Condition(ruleToCreate.getConditionId(), "auto-off",
+ STATE_FALSE);
+
+ // User manually activates -> it's active.
+ runAsSystemUi(
+ () -> mNotificationManager.setAutomaticZenRuleState(ruleId, manualActivate));
+ assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_TRUE);
+
+ // User manually deactivates -> it's inactive.
+ runAsSystemUi(
+ () -> mNotificationManager.setAutomaticZenRuleState(ruleId, manualDeactivate));
+ assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_FALSE);
+
+ // And app can activate and deactivate.
+ mNotificationManager.setAutomaticZenRuleState(ruleId, autoActivate);
+ assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_TRUE);
+ mNotificationManager.setAutomaticZenRuleState(ruleId, autoDeactivate);
+ assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_FALSE);
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+ public void setAutomaticZenRuleState_manualDeactivation() {
+ AutomaticZenRule ruleToCreate = createZenRule("rule");
+ String ruleId = mNotificationManager.addAutomaticZenRule(ruleToCreate);
+ Condition manualActivate = new Condition(ruleToCreate.getConditionId(), "manual-on",
+ STATE_TRUE, Condition.SOURCE_USER_ACTION);
+ Condition manualDeactivate = new Condition(ruleToCreate.getConditionId(), "manual-off",
+ STATE_FALSE, Condition.SOURCE_USER_ACTION);
+ Condition autoActivate = new Condition(ruleToCreate.getConditionId(), "auto-on",
+ STATE_TRUE);
+ Condition autoDeactivate = new Condition(ruleToCreate.getConditionId(), "auto-off",
+ STATE_FALSE);
+
+ // App activates rule.
+ mNotificationManager.setAutomaticZenRuleState(ruleId, autoActivate);
+ assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_TRUE);
+
+ // User manually deactivates -> it's inactive.
+ runAsSystemUi(
+ () -> mNotificationManager.setAutomaticZenRuleState(ruleId, manualDeactivate));
+ assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_FALSE);
+
+ // User manually reactivates -> it's active.
+ runAsSystemUi(
+ () -> mNotificationManager.setAutomaticZenRuleState(ruleId, manualActivate));
+ assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_TRUE);
+
+ // That manual activation removed the override-deactivate, but didn't put an
+ // override-activate, so app can deactivate when its natural schedule ends.
+ mNotificationManager.setAutomaticZenRuleState(ruleId, autoDeactivate);
+ assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_FALSE);
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+ public void setAutomaticZenRuleState_respectsManuallyActivated() {
+ AutomaticZenRule ruleToCreate = createZenRule("rule");
+ String ruleId = mNotificationManager.addAutomaticZenRule(ruleToCreate);
+ Condition manualActivate = new Condition(ruleToCreate.getConditionId(), "manual-on",
+ STATE_TRUE, Condition.SOURCE_USER_ACTION);
+ Condition autoActivate = new Condition(ruleToCreate.getConditionId(), "auto-on",
+ STATE_TRUE);
+ Condition autoDeactivate = new Condition(ruleToCreate.getConditionId(), "auto-off",
+ STATE_FALSE);
+
+ // App thinks rule should be inactive.
+ mNotificationManager.setAutomaticZenRuleState(ruleId, autoDeactivate);
+ assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_FALSE);
+
+ // Manually activate -> it's active.
+ runAsSystemUi(() -> mNotificationManager.setAutomaticZenRuleState(ruleId, manualActivate));
+ assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_TRUE);
+
+ // App says it should be inactive, but it's ignored.
+ mNotificationManager.setAutomaticZenRuleState(ruleId, autoDeactivate);
+ assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_TRUE);
+
+ // App says it should be active. No change now...
+ mNotificationManager.setAutomaticZenRuleState(ruleId, autoActivate);
+ assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_TRUE);
+
+ // ... but when the app wants to deactivate next time, it works.
+ mNotificationManager.setAutomaticZenRuleState(ruleId, autoDeactivate);
+ assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_FALSE);
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+ public void setAutomaticZenRuleState_respectsManuallyDeactivated() {
+ AutomaticZenRule ruleToCreate = createZenRule("rule");
+ String ruleId = mNotificationManager.addAutomaticZenRule(ruleToCreate);
+ Condition manualDeactivate = new Condition(ruleToCreate.getConditionId(), "manual-off",
+ STATE_FALSE, Condition.SOURCE_USER_ACTION);
+ Condition autoActivate = new Condition(ruleToCreate.getConditionId(), "auto-on",
+ STATE_TRUE);
+ Condition autoDeactivate = new Condition(ruleToCreate.getConditionId(), "auto-off",
+ STATE_FALSE);
+
+ // App activates rule.
+ mNotificationManager.setAutomaticZenRuleState(ruleId, autoActivate);
+ assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_TRUE);
+
+ // User manually deactivates -> it's inactive.
+ runAsSystemUi(
+ () -> mNotificationManager.setAutomaticZenRuleState(ruleId, manualDeactivate));
+ assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_FALSE);
+
+ // App says it should be active, but it's ignored.
+ mNotificationManager.setAutomaticZenRuleState(ruleId, autoActivate);
+ assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_FALSE);
+
+ // App says it should be inactive. No change now...
+ mNotificationManager.setAutomaticZenRuleState(ruleId, autoDeactivate);
+ assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_FALSE);
+
+ // ... but when the app wants to activate next time, it works.
+ mNotificationManager.setAutomaticZenRuleState(ruleId, autoActivate);
+ assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_TRUE);
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+ public void setAutomaticZenRuleState_manualActivationFromApp() {
+ AutomaticZenRule ruleToCreate = createZenRule("rule");
+ String ruleId = mNotificationManager.addAutomaticZenRule(ruleToCreate);
+ Condition manualActivate = new Condition(ruleToCreate.getConditionId(), "manual-off",
+ STATE_TRUE, Condition.SOURCE_USER_ACTION);
+ Condition manualDeactivate = new Condition(ruleToCreate.getConditionId(), "manual-off",
+ STATE_FALSE, Condition.SOURCE_USER_ACTION);
+ Condition autoActivate = new Condition(ruleToCreate.getConditionId(), "auto-on",
+ STATE_TRUE);
+ Condition autoDeactivate = new Condition(ruleToCreate.getConditionId(), "auto-off",
+ STATE_FALSE);
+
+ // App activates rule.
+ mNotificationManager.setAutomaticZenRuleState(ruleId, autoActivate);
+ assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_TRUE);
+
+ // User manually deactivates from SysUI -> it's inactive.
+ runAsSystemUi(
+ () -> mNotificationManager.setAutomaticZenRuleState(ruleId, manualDeactivate));
+ assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_FALSE);
+
+ // User manually activates from App -> it's active.
+ mNotificationManager.setAutomaticZenRuleState(ruleId, manualActivate);
+ assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_TRUE);
+
+ // And app can automatically deactivate it later.
+ mNotificationManager.setAutomaticZenRuleState(ruleId, autoDeactivate);
+ assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_FALSE);
+ }
+
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+ public void setAutomaticZenRuleState_manualDeactivationFromApp() {
+ AutomaticZenRule ruleToCreate = createZenRule("rule");
+ String ruleId = mNotificationManager.addAutomaticZenRule(ruleToCreate);
+ Condition manualActivate = new Condition(ruleToCreate.getConditionId(), "manual-off",
+ STATE_TRUE, Condition.SOURCE_USER_ACTION);
+ Condition manualDeactivate = new Condition(ruleToCreate.getConditionId(), "manual-off",
+ STATE_FALSE, Condition.SOURCE_USER_ACTION);
+ Condition autoActivate = new Condition(ruleToCreate.getConditionId(), "auto-on",
+ STATE_TRUE);
+
+ // User manually activates from SysUI -> it's active.
+ runAsSystemUi(
+ () -> mNotificationManager.setAutomaticZenRuleState(ruleId, manualActivate));
+ assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_TRUE);
+
+ // User manually deactivates from App -> it's inactive.
+ mNotificationManager.setAutomaticZenRuleState(ruleId, manualDeactivate);
+ assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_FALSE);
+
+ // And app can automatically activate it later.
+ mNotificationManager.setAutomaticZenRuleState(ruleId, autoActivate);
+ assertThat(mNotificationManager.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_TRUE);
+ }
+
+ private AutomaticZenRule createZenRule(String name) {
+ return createZenRule(name, NotificationManager.INTERRUPTION_FILTER_PRIORITY);
+ }
+
+ private AutomaticZenRule createZenRule(String name, int filter) {
+ return new AutomaticZenRule(name, null,
+ new ComponentName(mContext, ExampleActivity.class),
+ new Uri.Builder().scheme("scheme")
+ .appendPath("path")
+ .appendQueryParameter("fake_rule", "fake_value")
+ .build(), null, filter, true);
+ }
+}
diff --git a/services/tests/uiservicestests/src/android/app/NotificationSystemUtil.java b/services/tests/uiservicestests/src/android/app/NotificationSystemUtil.java
new file mode 100644
index 000000000000..cf6e39b962ab
--- /dev/null
+++ b/services/tests/uiservicestests/src/android/app/NotificationSystemUtil.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import static org.junit.Assert.assertEquals;
+
+import android.Manifest;
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+
+import androidx.annotation.NonNull;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.AmUtils;
+import com.android.compatibility.common.util.FileUtils;
+import com.android.compatibility.common.util.SystemUtil;
+import com.android.compatibility.common.util.ThrowingRunnable;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+
+public class NotificationSystemUtil {
+
+ /**
+ * Runs a {@link ThrowingRunnable} as the Shell, while adopting SystemUI's permission (as
+ * checked by {@code NotificationManagerService#isCallerSystemOrSystemUi}).
+ */
+ protected static void runAsSystemUi(@NonNull ThrowingRunnable runnable) {
+ SystemUtil.runWithShellPermissionIdentity(
+ InstrumentationRegistry.getInstrumentation().getUiAutomation(),
+ runnable, Manifest.permission.STATUS_BAR_SERVICE);
+ }
+
+ static void toggleNotificationPolicyAccess(Context context, String packageName,
+ boolean on) throws IOException {
+
+ String command = " cmd notification " + (on ? "allow_dnd " : "disallow_dnd ") + packageName
+ + " " + context.getUserId();
+
+ runCommand(command, InstrumentationRegistry.getInstrumentation());
+ AmUtils.waitForBroadcastBarrier();
+
+ NotificationManager nm = context.getSystemService(NotificationManager.class);
+ assertEquals("Notification Policy Access Grant is "
+ + nm.isNotificationPolicyAccessGranted() + " not " + on + " for "
+ + packageName, on, nm.isNotificationPolicyAccessGranted());
+ }
+
+ private static void runCommand(String command, Instrumentation instrumentation)
+ throws IOException {
+ UiAutomation uiAutomation = instrumentation.getUiAutomation();
+ try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(
+ uiAutomation.executeShellCommand(command))) {
+ FileUtils.readInputStreamFully(fis);
+ }
+ }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java
index 8fad01a7541d..2616ccbe474a 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java
@@ -247,7 +247,8 @@ public class NotificationListenerServiceTest extends UiServiceTestCase {
getRankingAdjustment(i),
isBubble(i),
getProposedImportance(i),
- hasSensitiveContent(i)
+ hasSensitiveContent(i),
+ getSummarization(i)
);
rankings[i] = ranking;
}
@@ -383,6 +384,13 @@ public class NotificationListenerServiceTest extends UiServiceTestCase {
return index % 3 == 0;
}
+ public static String getSummarization(int index) {
+ if ((android.app.Flags.nmSummarizationUi() || android.app.Flags.nmSummarization())) {
+ return "summary " + index;
+ }
+ return null;
+ }
+
private boolean isBubble(int index) {
return index % 4 == 0;
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index edfdb4ffec3a..a8fcadd9dd74 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -51,6 +51,7 @@ import static android.app.NotificationChannel.RECS_ID;
import static android.app.NotificationChannel.SOCIAL_MEDIA_ID;
import static android.app.NotificationChannel.USER_LOCKED_ALLOW_BUBBLE;
import static android.app.NotificationManager.ACTION_AUTOMATIC_ZEN_RULE_STATUS_CHANGED;
+import static android.app.NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED;
import static android.app.NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED;
import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_ACTIVATED;
import static android.app.NotificationManager.BUBBLE_PREFERENCE_ALL;
@@ -123,7 +124,9 @@ import static android.service.notification.Flags.FLAG_REDACT_SENSITIVE_NOTIFICAT
import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING;
import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS;
import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ONGOING;
+import static android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_CALL_EFFECTS;
import static android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_EFFECTS;
+import static android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_NOTIFICATION_EFFECTS;
import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
import static android.service.notification.NotificationListenerService.REASON_CANCEL;
import static android.service.notification.NotificationListenerService.REASON_LOCKDOWN;
@@ -163,6 +166,7 @@ import static junit.framework.Assert.fail;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyLong;
@@ -360,7 +364,6 @@ import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;
-import org.mockito.ArgumentMatchers;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.Mockito;
@@ -368,6 +371,9 @@ import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
@@ -383,9 +389,6 @@ import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.function.Consumer;
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
-import platform.test.runner.parameterized.Parameters;
-
@SmallTest
@RunWith(ParameterizedAndroidJunit4.class)
@RunWithLooper
@@ -600,7 +603,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
@Parameters(name = "{0}")
public static List<FlagsParameterization> getParams() {
- return FlagsParameterization.allCombinationsOf();
+ return FlagsParameterization.allCombinationsOf(
+ FLAG_NOTIFICATION_CLASSIFICATION);
}
public NotificationManagerServiceTest(FlagsParameterization flags) {
@@ -10889,6 +10893,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
Bundle signals = new Bundle();
signals.putInt(KEY_IMPORTANCE, IMPORTANCE_LOW);
signals.putInt(KEY_USER_SENTIMENT, USER_SENTIMENT_NEGATIVE);
+ signals.putInt(KEY_TYPE, TYPE_PROMOTION);
Adjustment adjustment = new Adjustment(r.getSbn().getPackageName(), r.getKey(), signals,
"", r.getUser().getIdentifier());
@@ -11413,8 +11418,14 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
verify(mContext).sendBroadcastAsUser(eqIntent(expected), eq(UserHandle.of(mUserId)));
}
+ private static Intent isIntentWithAction(String wantedAction) {
+ return argThat(
+ intent -> intent != null && wantedAction.equals(intent.getAction())
+ );
+ }
+
private static Intent eqIntent(Intent wanted) {
- return ArgumentMatchers.argThat(
+ return argThat(
new ArgumentMatcher<Intent>() {
@Override
public boolean matches(Intent argument) {
@@ -17491,6 +17502,66 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
+ @EnableFlags({Flags.FLAG_NM_BINDER_PERF_THROTTLE_EFFECTS_SUPPRESSOR_BROADCAST,
+ Flags.FLAG_NM_BINDER_PERF_REDUCE_ZEN_BROADCASTS})
+ public void requestHintsFromListener_changingEffectsButNotSuppressor_noBroadcast()
+ throws Exception {
+ // Note that NM_BINDER_PERF_REDUCE_ZEN_BROADCASTS is not strictly necessary; however each
+ // path will do slightly different calls so we force one of them to simplify the test.
+ when(mUmInternal.getProfileIds(anyInt(), anyBoolean())).thenReturn(new int[]{mUserId});
+ when(mListeners.checkServiceTokenLocked(any())).thenReturn(mListener);
+ INotificationListener token = mock(INotificationListener.class);
+ mService.isSystemUid = true;
+
+ mBinderService.requestHintsFromListener(token, HINT_HOST_DISABLE_CALL_EFFECTS);
+ mTestableLooper.moveTimeForward(500); // more than ZEN_BROADCAST_DELAY
+ waitForIdle();
+
+ verify(mContext, times(1)).sendBroadcastMultiplePermissions(
+ isIntentWithAction(ACTION_EFFECTS_SUPPRESSOR_CHANGED), any(), any(), any());
+
+ // Same suppressor suppresses something else.
+ mBinderService.requestHintsFromListener(token, HINT_HOST_DISABLE_NOTIFICATION_EFFECTS);
+ mTestableLooper.moveTimeForward(500); // more than ZEN_BROADCAST_DELAY
+ waitForIdle();
+
+ // Still 1 total calls (the previous one).
+ verify(mContext, times(1)).sendBroadcastMultiplePermissions(
+ isIntentWithAction(ACTION_EFFECTS_SUPPRESSOR_CHANGED), any(), any(), any());
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_NM_BINDER_PERF_THROTTLE_EFFECTS_SUPPRESSOR_BROADCAST,
+ Flags.FLAG_NM_BINDER_PERF_REDUCE_ZEN_BROADCASTS})
+ public void requestHintsFromListener_changingSuppressor_throttlesBroadcast() throws Exception {
+ // Note that NM_BINDER_PERF_REDUCE_ZEN_BROADCASTS is not strictly necessary; however each
+ // path will do slightly different calls so we force one of them to simplify the test.
+ when(mUmInternal.getProfileIds(anyInt(), anyBoolean())).thenReturn(new int[]{mUserId});
+ when(mListeners.checkServiceTokenLocked(any())).thenReturn(mListener);
+ INotificationListener token = mock(INotificationListener.class);
+ mService.isSystemUid = true;
+
+ // Several updates in quick succession.
+ mBinderService.requestHintsFromListener(token, HINT_HOST_DISABLE_CALL_EFFECTS);
+ mBinderService.clearRequestedListenerHints(token);
+ mBinderService.requestHintsFromListener(token, HINT_HOST_DISABLE_NOTIFICATION_EFFECTS);
+ mBinderService.clearRequestedListenerHints(token);
+ mBinderService.requestHintsFromListener(token, HINT_HOST_DISABLE_CALL_EFFECTS);
+ mBinderService.clearRequestedListenerHints(token);
+ mBinderService.requestHintsFromListener(token, HINT_HOST_DISABLE_NOTIFICATION_EFFECTS);
+
+ // No broadcasts yet!
+ verify(mContext, never()).sendBroadcastMultiplePermissions(any(), any(), any(), any());
+
+ mTestableLooper.moveTimeForward(500); // more than ZEN_BROADCAST_DELAY
+ waitForIdle();
+
+ // Only one broadcast after idle time.
+ verify(mContext, times(1)).sendBroadcastMultiplePermissions(
+ isIntentWithAction(ACTION_EFFECTS_SUPPRESSOR_CHANGED), any(), any(), any());
+ }
+
+ @Test
@EnableFlags(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION)
public void testApplyAdjustment_keyType_validType() throws Exception {
final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordExtractorDataTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordExtractorDataTest.java
index 9fe0e49c4ab8..09ebc23a1d7b 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordExtractorDataTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordExtractorDataTest.java
@@ -29,12 +29,19 @@ import android.os.UserHandle;
import android.service.notification.Adjustment;
import android.service.notification.StatusBarNotification;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+
import com.android.server.UiServiceTestCase;
+import org.junit.Rule;
import org.junit.Test;
public class NotificationRecordExtractorDataTest extends UiServiceTestCase {
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
@Test
public void testHasDiffs_noDiffs() {
NotificationRecord r = generateRecord();
@@ -57,7 +64,8 @@ public class NotificationRecordExtractorDataTest extends UiServiceTestCase {
r.getRankingScore(),
r.isConversation(),
r.getProposedImportance(),
- r.hasSensitiveContent());
+ r.hasSensitiveContent(),
+ r.getSummarization());
assertFalse(extractorData.hasDiffForRankingLocked(r, 1));
assertFalse(extractorData.hasDiffForLoggingLocked(r, 1));
@@ -85,7 +93,8 @@ public class NotificationRecordExtractorDataTest extends UiServiceTestCase {
r.getRankingScore(),
r.isConversation(),
r.getProposedImportance(),
- r.hasSensitiveContent());
+ r.hasSensitiveContent(),
+ r.getSummarization());
Bundle signals = new Bundle();
signals.putInt(Adjustment.KEY_IMPORTANCE_PROPOSAL, IMPORTANCE_HIGH);
@@ -119,7 +128,8 @@ public class NotificationRecordExtractorDataTest extends UiServiceTestCase {
r.getRankingScore(),
r.isConversation(),
r.getProposedImportance(),
- r.hasSensitiveContent());
+ r.hasSensitiveContent(),
+ r.getSummarization());
Bundle signals = new Bundle();
signals.putString(Adjustment.KEY_GROUP_KEY, "ranker_group");
@@ -154,7 +164,8 @@ public class NotificationRecordExtractorDataTest extends UiServiceTestCase {
r.getRankingScore(),
r.isConversation(),
r.getProposedImportance(),
- r.hasSensitiveContent());
+ r.hasSensitiveContent(),
+ r.getSummarization());
Bundle signals = new Bundle();
signals.putBoolean(Adjustment.KEY_SENSITIVE_CONTENT, true);
@@ -166,6 +177,42 @@ public class NotificationRecordExtractorDataTest extends UiServiceTestCase {
assertTrue(extractorData.hasDiffForLoggingLocked(r, 1));
}
+ @Test
+ @EnableFlags(android.app.Flags.FLAG_NM_SUMMARIZATION)
+ public void testHasDiffs_summarization() {
+ NotificationRecord r = generateRecord();
+
+ NotificationRecordExtractorData extractorData = new NotificationRecordExtractorData(
+ 1,
+ r.getPackageVisibilityOverride(),
+ r.canShowBadge(),
+ r.canBubble(),
+ r.getNotification().isBubbleNotification(),
+ r.getChannel(),
+ r.getGroupKey(),
+ r.getPeopleOverride(),
+ r.getSnoozeCriteria(),
+ r.getUserSentiment(),
+ r.getSuppressedVisualEffects(),
+ r.getSystemGeneratedSmartActions(),
+ r.getSmartReplies(),
+ r.getImportance(),
+ r.getRankingScore(),
+ r.isConversation(),
+ r.getProposedImportance(),
+ r.hasSensitiveContent(),
+ r.getSummarization());
+
+ Bundle signals = new Bundle();
+ signals.putString(Adjustment.KEY_SUMMARIZATION, "SUMMARIZED!");
+ Adjustment adjustment = new Adjustment("pkg", r.getKey(), signals, "", 0);
+ r.addAdjustment(adjustment);
+ r.applyAdjustments();
+
+ assertTrue(extractorData.hasDiffForRankingLocked(r, 1));
+ assertTrue(extractorData.hasDiffForLoggingLocked(r, 1));
+ }
+
private NotificationRecord generateRecord() {
NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_LOW);
final Notification.Builder builder = new Notification.Builder(getContext())
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index 704b580a80b0..832ca51ae580 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -260,7 +260,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
return FlagsParameterization.allCombinationsOf(
android.app.Flags.FLAG_API_RICH_ONGOING,
FLAG_NOTIFICATION_CLASSIFICATION, FLAG_NOTIFICATION_CLASSIFICATION_UI,
- FLAG_MODES_UI);
+ FLAG_MODES_UI, android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS);
}
public PreferencesHelperTest(FlagsParameterization flags) {
@@ -3381,13 +3381,12 @@ public class PreferencesHelperTest extends UiServiceTestCase {
// user 0 records remain
for (int i = 0; i < user0Uids.length; i++) {
assertEquals(1,
- mHelper.getNotificationChannels(PKG_N_MR1, user0Uids[i], false, true)
- .getList().size());
+ mHelper.getRemovedPkgNotificationChannels(PKG_N_MR1, user0Uids[i]).size());
}
// user 1 records are gone
for (int i = 0; i < user1Uids.length; i++) {
- assertEquals(0, mHelper.getNotificationChannels(PKG_N_MR1, user1Uids[i], false, true)
- .getList().size());
+ assertEquals(0,
+ mHelper.getRemovedPkgNotificationChannels(PKG_N_MR1, user1Uids[i]).size());
}
}
@@ -3402,8 +3401,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
assertTrue(mHelper.onPackagesChanged(true, USER_SYSTEM, new String[]{PKG_N_MR1},
new int[]{UID_N_MR1}));
- assertEquals(0, mHelper.getNotificationChannels(
- PKG_N_MR1, UID_N_MR1, true, true).getList().size());
+ assertEquals(0, mHelper.getRemovedPkgNotificationChannels(PKG_N_MR1, UID_N_MR1).size());
// Not deleted
mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false,
@@ -3472,7 +3470,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
assertTrue(mHelper.canShowBadge(PKG_O, UID_O));
assertNull(mHelper.getNotificationDelegate(PKG_O, UID_O));
assertEquals(0, mHelper.getAppLockedFields(PKG_O, UID_O));
- assertEquals(0, mHelper.getNotificationChannels(PKG_O, UID_O, true, true).getList().size());
+ assertEquals(0, mHelper.getRemovedPkgNotificationChannels(PKG_O, UID_O).size());
assertEquals(0, mHelper.getNotificationChannelGroups(PKG_O, UID_O).size());
NotificationChannel channel = getChannel();
@@ -6836,38 +6834,11 @@ public class PreferencesHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
- public void testGetNotificationChannels_createIfNeeded() {
- // Test setup hasn't created any channels or read package preferences yet.
- // If we ask for notification channels _without_ creating, we should get no result.
- ParceledListSlice<NotificationChannel> channels = mHelper.getNotificationChannels(PKG_N_MR1,
- UID_N_MR1, false, false, /* createPrefsIfNeeded= */ false);
- assertThat(channels.getList().size()).isEqualTo(0);
-
- // If we ask it to create package preferences, we expect the default channel to be created
- // for N_MR1.
- channels = mHelper.getNotificationChannels(PKG_N_MR1, UID_N_MR1, false,
- false, /* createPrefsIfNeeded= */ true);
- assertThat(channels.getList().size()).isEqualTo(1);
- assertThat(channels.getList().getFirst().getId()).isEqualTo(
- NotificationChannel.DEFAULT_CHANNEL_ID);
- }
-
- @Test
@DisableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
public void testGetNotificationChannels_neverCreatesWhenFlagOff() {
- ParceledListSlice<NotificationChannel> channels;
- try {
- channels = mHelper.getNotificationChannels(PKG_N_MR1, UID_N_MR1, false,
- false, /* createPrefsIfNeeded= */ true);
- } catch (Exception e) {
- // Slog.wtf kicks in, presumably
- } finally {
- channels = mHelper.getNotificationChannels(PKG_N_MR1, UID_N_MR1, false,
- false, /* createPrefsIfNeeded= */ false);
- assertThat(channels.getList().size()).isEqualTo(0);
- }
-
+ ParceledListSlice<NotificationChannel> channels = mHelper.getNotificationChannels(PKG_N_MR1,
+ UID_N_MR1, false, false);
+ assertThat(channels.getList().size()).isEqualTo(0);
}
// Test version of PreferencesHelper whose only functional difference is that it does not
diff --git a/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java b/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java
index 038e1357159b..036e03c60091 100644
--- a/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java
@@ -23,6 +23,7 @@ import static com.android.server.policy.PhoneWindowManager.POWER_VOLUME_UP_BEHAV
import static com.android.server.policy.PhoneWindowManager.POWER_VOLUME_UP_BEHAVIOR_MUTE;
import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.Presubmit;
import android.view.ViewConfiguration;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -38,6 +39,7 @@ import org.junit.runner.RunWith;
* Build/Install/Run:
* atest WmTests:CombinationKeyTests
*/
+@Presubmit
@MediumTest
@RunWith(AndroidJUnit4.class)
@DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER_MULTI_KEY_GESTURES)
diff --git a/services/tests/wmtests/src/com/android/server/policy/DeferredKeyActionExecutorTests.java b/services/tests/wmtests/src/com/android/server/policy/DeferredKeyActionExecutorTests.java
index ca3787ec4546..bccdd67d33ed 100644
--- a/services/tests/wmtests/src/com/android/server/policy/DeferredKeyActionExecutorTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/DeferredKeyActionExecutorTests.java
@@ -19,6 +19,7 @@ package com.android.server.policy;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import android.platform.test.annotations.Presubmit;
import android.view.KeyEvent;
import org.junit.Before;
@@ -29,6 +30,7 @@ import org.junit.Test;
*
* <p>Build/Install/Run: atest WmTests:DeferredKeyActionExecutorTests
*/
+@Presubmit
public final class DeferredKeyActionExecutorTests {
private DeferredKeyActionExecutor mKeyActionExecutor;
diff --git a/services/tests/wmtests/src/com/android/server/policy/KeyCombinationManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyCombinationManagerTests.java
index 8b5f68a1e974..a912c177c863 100644
--- a/services/tests/wmtests/src/com/android/server/policy/KeyCombinationManagerTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/KeyCombinationManagerTests.java
@@ -29,6 +29,7 @@ import static org.testng.Assert.assertTrue;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
+import android.platform.test.annotations.Presubmit;
import android.view.KeyEvent;
import androidx.test.filters.SmallTest;
@@ -45,7 +46,7 @@ import java.util.concurrent.TimeUnit;
* Build/Install/Run:
* atest KeyCombinationManagerTests
*/
-
+@Presubmit
@SmallTest
public class KeyCombinationManagerTests {
private KeyCombinationManager mKeyCombinationManager;
diff --git a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
index 85ef466b2477..16c786b52655 100644
--- a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
@@ -544,17 +544,6 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase {
}
@Test
- public void testKeyGestureSplitscreenFocus() {
- Assert.assertTrue(sendKeyGestureEventComplete(
- KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT));
- mPhoneWindowManager.assertSetSplitscreenFocus(true);
-
- Assert.assertTrue(sendKeyGestureEventComplete(
- KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT));
- mPhoneWindowManager.assertSetSplitscreenFocus(false);
- }
-
- @Test
public void testKeyGestureShortcutHelper() {
Assert.assertTrue(sendKeyGestureEventComplete(
KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER));
diff --git a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java
index 82a5add407f4..d961a6acc9c1 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java
@@ -42,6 +42,7 @@ import android.os.Handler;
import android.os.Looper;
import android.os.UserHandle;
import android.platform.test.annotations.EnableFlags;
+import android.platform.test.annotations.Presubmit;
import android.platform.test.flag.junit.SetFlagsRule;
import android.view.KeyEvent;
import android.view.KeyboardShortcutGroup;
@@ -64,7 +65,7 @@ import java.util.Collections;
* Build/Install/Run:
* atest ModifierShortcutManagerTests
*/
-
+@Presubmit
@SmallTest
@EnableFlags(com.android.hardware.input.Flags.FLAG_MODIFIER_SHORTCUT_MANAGER_REFACTOR)
public class ModifierShortcutManagerTests {
@@ -127,7 +128,7 @@ public class ModifierShortcutManagerTests {
// Total valid shortcuts.
KeyboardShortcutGroup group =
mModifierShortcutManager.getApplicationLaunchKeyboardShortcuts(-1);
- assertEquals(13, group.getItems().size());
+ assertEquals(11, group.getItems().size());
// Total valid shift shortcuts.
assertEquals(3, group.getItems().stream()
diff --git a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
index af3dc1da4dcc..c73ce23fe6b5 100644
--- a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
@@ -48,6 +48,7 @@ import android.app.AppOpsManager;
import android.content.Context;
import android.hardware.input.InputManager;
import android.os.PowerManager;
+import android.platform.test.annotations.Presubmit;
import android.platform.test.flag.junit.SetFlagsRule;
import androidx.test.filters.SmallTest;
@@ -72,6 +73,7 @@ import org.junit.Test;
* Build/Install/Run:
* atest WmTests:PhoneWindowManagerTests
*/
+@Presubmit
@SmallTest
public class PhoneWindowManagerTests {
diff --git a/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java b/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java
index 33ccec3f785a..53e82bad818d 100644
--- a/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java
@@ -28,6 +28,7 @@ import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_POWER_GO_
import static org.junit.Assert.assertEquals;
import android.platform.test.annotations.EnableFlags;
+import android.platform.test.annotations.Presubmit;
import android.provider.Settings;
import android.view.Display;
@@ -40,6 +41,7 @@ import org.junit.Test;
* Build/Install/Run:
* atest WmTests:PowerKeyGestureTests
*/
+@Presubmit
public class PowerKeyGestureTests extends ShortcutKeyTestBase {
@Before
public void setUp() {
diff --git a/services/tests/wmtests/src/com/android/server/policy/SingleKeyGestureTests.java b/services/tests/wmtests/src/com/android/server/policy/SingleKeyGestureTests.java
index ff8b6d3c1962..6c6594220bad 100644
--- a/services/tests/wmtests/src/com/android/server/policy/SingleKeyGestureTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/SingleKeyGestureTests.java
@@ -23,6 +23,8 @@ import static android.view.KeyEvent.KEYCODE_POWER;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+import static com.android.hardware.input.Flags.FLAG_ABORT_SLOW_MULTI_PRESS;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -35,9 +37,14 @@ import android.os.HandlerThread;
import android.os.Looper;
import android.os.Process;
import android.os.SystemClock;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.view.KeyEvent;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import java.util.concurrent.BlockingQueue;
@@ -51,7 +58,10 @@ import java.util.concurrent.TimeUnit;
* Build/Install/Run:
* atest WmTests:SingleKeyGestureTests
*/
+@Presubmit
public class SingleKeyGestureTests {
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
private SingleKeyGestureDetector mDetector;
private int mMaxMultiPressCount = 3;
@@ -260,6 +270,44 @@ public class SingleKeyGestureTests {
}
@Test
+ @EnableFlags(FLAG_ABORT_SLOW_MULTI_PRESS)
+ public void testMultipress_noLongPressBehavior_longPressCancelsMultiPress()
+ throws InterruptedException {
+ mLongPressOnPowerBehavior = false;
+
+ pressKey(KEYCODE_POWER, 0 /* pressTime */);
+ pressKey(KEYCODE_POWER, mLongPressTime /* pressTime */);
+
+ assertFalse(mMultiPressed.await(mWaitTimeout, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ @EnableFlags(FLAG_ABORT_SLOW_MULTI_PRESS)
+ public void testMultipress_noVeryLongPressBehavior_veryLongPressCancelsMultiPress()
+ throws InterruptedException {
+ mLongPressOnPowerBehavior = false;
+ mVeryLongPressOnPowerBehavior = false;
+
+ pressKey(KEYCODE_POWER, 0 /* pressTime */);
+ pressKey(KEYCODE_POWER, mVeryLongPressTime /* pressTime */);
+
+ assertFalse(mMultiPressed.await(mWaitTimeout, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ @DisableFlags(FLAG_ABORT_SLOW_MULTI_PRESS)
+ public void testMultipress_flagDisabled_noLongPressBehavior_longPressDoesNotCancelMultiPress()
+ throws InterruptedException {
+ mLongPressOnPowerBehavior = false;
+ mExpectedMultiPressCount = 2;
+
+ pressKey(KEYCODE_POWER, 0 /* pressTime */);
+ pressKey(KEYCODE_POWER, mLongPressTime /* pressTime */);
+
+ assertTrue(mMultiPressed.await(mWaitTimeout, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
public void testMultiPress() throws InterruptedException {
// Double presses.
mExpectedMultiPressCount = 2;
diff --git a/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java b/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java
index 3ea3235df0f4..833dd5d768e4 100644
--- a/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java
@@ -35,6 +35,7 @@ import android.app.ActivityTaskManager.RootTaskInfo;
import android.content.ComponentName;
import android.hardware.input.KeyGestureEvent;
import android.os.RemoteException;
+import android.platform.test.annotations.Presubmit;
import android.provider.Settings;
import android.view.Display;
@@ -47,6 +48,7 @@ import org.junit.Test;
* Build/Install/Run:
* atest WmTests:StemKeyGestureTests
*/
+@Presubmit
public class StemKeyGestureTests extends ShortcutKeyTestBase {
private static final String TEST_TARGET_ACTIVITY = "com.android.server.policy/.TestActivity";
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index 4ff3d433632a..f88492477487 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -925,11 +925,6 @@ class TestPhoneWindowManager {
verify(mStatusBarManagerInternal).moveFocusedTaskToStageSplit(anyInt(), eq(leftOrTop));
}
- void assertSetSplitscreenFocus(boolean leftOrTop) {
- mTestLooper.dispatchAll();
- verify(mStatusBarManagerInternal).setSplitscreenFocus(eq(leftOrTop));
- }
-
void assertStatusBarStartAssist() {
mTestLooper.dispatchAll();
verify(mStatusBarManagerInternal).startAssist(any());
diff --git a/services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java b/services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java
index 3ca352cfa60d..9e59bced01f1 100644
--- a/services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java
@@ -57,6 +57,7 @@ import android.content.res.Resources;
import android.os.PowerManager;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
+import android.platform.test.annotations.Presubmit;
import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.Settings;
import android.view.Display;
@@ -83,6 +84,7 @@ import java.util.function.BooleanSupplier;
*
* <p>Build/Install/Run: atest WmTests:WindowWakeUpPolicyTests
*/
+@Presubmit
public final class WindowWakeUpPolicyTests {
@Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
@Rule public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule();
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 5ac3e483231c..7af4ede05363 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -4461,7 +4461,46 @@ public class SizeCompatTests extends WindowTestsBase {
// are aligned to the top of the parentAppBounds
assertEquals(new Rect(0, notchHeight, 1000, 1200), appBounds);
assertEquals(new Rect(0, 0, 1000, 1200), bounds);
+ }
+
+ @Test
+ @DisableCompatChanges({ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED})
+ public void testInFreeform_boundsSandboxedToAppBounds() {
+ final int dw = 2800;
+ final int dh = 1400;
+ final int notchHeight = 100;
+ final DisplayContent display = new TestDisplayContent.Builder(mAtm, dw, dh)
+ .setNotch(notchHeight)
+ .build();
+ setUpApp(display);
+ prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
+ mTask.mDisplayContent.getDefaultTaskDisplayArea()
+ .setWindowingMode(WindowConfiguration.WINDOWING_MODE_FREEFORM);
+ mTask.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ Rect appBounds = new Rect(0, 0, 1000, 500);
+ Rect bounds = new Rect(0, 0, 1000, 600);
+ mTask.getWindowConfiguration().setAppBounds(appBounds);
+ mTask.getWindowConfiguration().setBounds(bounds);
+ mActivity.onConfigurationChanged(mTask.getConfiguration());
+
+ // Bounds are sandboxed to appBounds in freeform.
+ assertDownScaled();
+ assertEquals(mActivity.getWindowConfiguration().getAppBounds(),
+ mActivity.getWindowConfiguration().getBounds());
+
+ // Exit freeform.
+ mTask.mDisplayContent.getDefaultTaskDisplayArea()
+ .setWindowingMode(WindowConfiguration.WINDOWING_MODE_FULLSCREEN);
+ mTask.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ mTask.getWindowConfiguration().setBounds(new Rect(0, 0, dw, dh));
+ mActivity.onConfigurationChanged(mTask.getConfiguration());
+ assertFitted();
+ appBounds = mActivity.getWindowConfiguration().getAppBounds();
+ bounds = mActivity.getWindowConfiguration().getBounds();
+ // Bounds are not sandboxed to appBounds.
+ assertNotEquals(appBounds, bounds);
+ assertEquals(notchHeight, appBounds.top - bounds.top);
}
@Test
diff --git a/services/usb/OWNERS b/services/usb/OWNERS
index 2dff392d4e34..259261252032 100644
--- a/services/usb/OWNERS
+++ b/services/usb/OWNERS
@@ -1,9 +1,9 @@
-anothermark@google.com
+vmartensson@google.com
+nkapron@google.com
febinthattil@google.com
-aprasath@google.com
+shubhankarm@google.com
badhri@google.com
elaurent@google.com
albertccwang@google.com
jameswei@google.com
howardyen@google.com
-kumarashishg@google.com \ No newline at end of file
diff --git a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
index b32379ae4b1e..4d9df4666016 100644
--- a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
+++ b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
@@ -415,4 +415,5 @@ interface ITelecomService {
*/
boolean hasForegroundServiceDelegation(in PhoneAccountHandle phoneAccountHandle,
String callingPackage);
+ void setMetricsTestMode(boolean enabled);
}
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index d3f98d1a1d70..0b3d720bf52a 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -3219,7 +3219,6 @@ public class CarrierConfigManager {
* The roaming indicator will be shown if this is {@code true} and will not be shown if this is
* {@code false}.
*/
- @FlaggedApi(Flags.FLAG_HIDE_ROAMING_ICON)
public static final String KEY_SHOW_ROAMING_INDICATOR_BOOL = "show_roaming_indicator_bool";
/**
diff --git a/telephony/java/android/telephony/satellite/SatelliteSubscriberInfo.java b/telephony/java/android/telephony/satellite/SatelliteSubscriberInfo.java
index 9d9cac9702bb..d62fd63aeda6 100644
--- a/telephony/java/android/telephony/satellite/SatelliteSubscriberInfo.java
+++ b/telephony/java/android/telephony/satellite/SatelliteSubscriberInfo.java
@@ -22,8 +22,10 @@ import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
+import android.telephony.Rlog;
import com.android.internal.telephony.flags.Flags;
+import com.android.internal.telephony.util.TelephonyUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -220,7 +222,7 @@ public final class SatelliteSubscriberInfo implements Parcelable {
StringBuilder sb = new StringBuilder();
sb.append("SubscriberId:");
- sb.append(mSubscriberId);
+ sb.append(Rlog.pii(TelephonyUtils.IS_DEBUGGABLE, mSubscriberId));
sb.append(",");
sb.append("CarrierId:");
diff --git a/telephony/java/android/telephony/satellite/SatelliteSubscriberProvisionStatus.java b/telephony/java/android/telephony/satellite/SatelliteSubscriberProvisionStatus.java
index fb4f89ded547..75d3ec6d3fe6 100644
--- a/telephony/java/android/telephony/satellite/SatelliteSubscriberProvisionStatus.java
+++ b/telephony/java/android/telephony/satellite/SatelliteSubscriberProvisionStatus.java
@@ -21,8 +21,10 @@ import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
+import android.telephony.Rlog;
import com.android.internal.telephony.flags.Flags;
+import com.android.internal.telephony.util.TelephonyUtils;
import java.util.Objects;
@@ -132,7 +134,7 @@ public final class SatelliteSubscriberProvisionStatus implements Parcelable {
StringBuilder sb = new StringBuilder();
sb.append("SatelliteSubscriberInfo:");
- sb.append(mSubscriberInfo);
+ sb.append(Rlog.pii(TelephonyUtils.IS_DEBUGGABLE, mSubscriberInfo));
sb.append(",");
sb.append("ProvisionStatus:");
diff --git a/telephony/java/com/android/internal/telephony/util/WorkerThread.java b/telephony/java/com/android/internal/telephony/util/WorkerThread.java
new file mode 100644
index 000000000000..f5b653656352
--- /dev/null
+++ b/telephony/java/com/android/internal/telephony/util/WorkerThread.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.telephony.util;
+
+import android.annotation.NonNull;
+import android.os.Handler;
+import android.os.HandlerExecutor;
+import android.os.HandlerThread;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+
+/**
+ * Shared singleton worker thread for each process.
+ *
+ * This thread should be used for work that needs to be executed at standard priority
+ * but not on the main thread. This is suitable for handling asynchronous tasks that
+ * are ephemeral or require enough work that they shouldn't block the main thread, but
+ * should not block each other for more than around 100ms.
+ */
+public final class WorkerThread extends HandlerThread {
+ private static volatile WorkerThread sInstance;
+ private static volatile Handler sHandler;
+ private static volatile HandlerExecutor sHandlerExecutor;
+ private static final Object sLock = new Object();
+
+ private CountDownLatch mInitLock = new CountDownLatch(1);
+
+
+ private WorkerThread() {
+ super("android.telephony.worker");
+ }
+
+ private static void ensureThread() {
+ if (sInstance != null) return;
+ synchronized (sLock) {
+ if (sInstance != null) return;
+
+ final WorkerThread tmpThread = new WorkerThread();
+ tmpThread.start();
+
+ try {
+ tmpThread.mInitLock.await();
+ } catch (InterruptedException ignored) {
+ }
+
+
+ sHandler = new Handler(
+ tmpThread.getLooper(),
+ /* callback= */ null,
+ /* async= */ false,
+ /* shared= */ true);
+ sHandlerExecutor = new HandlerExecutor(sHandler);
+ sInstance = tmpThread; // Note: order matters here. sInstance must be assigned last.
+
+ }
+ }
+
+ @Override
+ protected void onLooperPrepared() {
+ mInitLock.countDown();
+ }
+
+ /**
+ * Get the worker thread directly.
+ *
+ * Users of this thread should take care not to block it for extended periods of
+ * time.
+ *
+ * @return a HandlerThread, never null
+ */
+ @NonNull public static HandlerThread get() {
+ ensureThread();
+ return sInstance;
+ }
+
+ /**
+ * Get a Handler that can process Runnables.
+ *
+ * @return a Handler, never null
+ */
+ @NonNull public static Handler getHandler() {
+ ensureThread();
+ return sHandler;
+ }
+
+ /**
+ * Get an Executor that can process Runnables
+ *
+ * @return an Executor, never null
+ */
+ @NonNull public static Executor getExecutor() {
+ ensureThread();
+ return sHandlerExecutor;
+ }
+
+ /**
+ * A method to reset the WorkerThread from scratch.
+ *
+ * This method should only be used for unit testing. In production it would have
+ * catastrophic consequences. Do not ever use this outside of tests.
+ */
+ @VisibleForTesting
+ public static void reset() {
+ synchronized (sLock) {
+ if (sInstance == null) return;
+ sInstance.quitSafely();
+ sInstance = null;
+ sHandler = null;
+ sHandlerExecutor = null;
+ ensureThread();
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/RecentTasksUtils.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/RecentTasksUtils.kt
index aa262f9dfd6a..1a5fda7c8324 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/RecentTasksUtils.kt
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/RecentTasksUtils.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.wm.shell.flicker.utils
+package com.android.server.wm.flicker.helpers
import android.app.Instrumentation
@@ -24,4 +24,4 @@ object RecentTasksUtils {
"dumpsys activity service SystemUIService WMShell recents clearAll"
)
}
-} \ No newline at end of file
+}
diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
index e0532633d40b..eef4e6f58463 100644
--- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
@@ -467,30 +467,6 @@ class KeyGestureControllerTests {
intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
),
TestData(
- "CTRL + ALT + DPAD_LEFT -> Change Splitscreen Focus Left",
- intArrayOf(
- KeyEvent.KEYCODE_CTRL_LEFT,
- KeyEvent.KEYCODE_ALT_LEFT,
- KeyEvent.KEYCODE_DPAD_LEFT
- ),
- KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT,
- intArrayOf(KeyEvent.KEYCODE_DPAD_LEFT),
- KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON,
- intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
- ),
- TestData(
- "CTRL + ALT + DPAD_RIGHT -> Change Splitscreen Focus Right",
- intArrayOf(
- KeyEvent.KEYCODE_CTRL_LEFT,
- KeyEvent.KEYCODE_ALT_LEFT,
- KeyEvent.KEYCODE_DPAD_RIGHT
- ),
- KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT,
- intArrayOf(KeyEvent.KEYCODE_DPAD_RIGHT),
- KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON,
- intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
- ),
- TestData(
"META + / -> Open Shortcut Helper",
intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_SLASH),
KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER,
diff --git a/tests/Tracing/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java
index 9e029a8d5e57..72b1780ceb06 100644
--- a/tests/Tracing/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java
+++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java
@@ -33,6 +33,7 @@ import org.junit.runners.JUnit4;
import perfetto.protos.ProtologCommon;
import java.io.File;
+import java.io.IOException;
@Presubmit
@RunWith(JUnit4.class)
@@ -159,4 +160,14 @@ public class ProtoLogViewerConfigReaderTest {
loadViewerConfig();
unloadViewerConfig();
}
+
+ @Test
+ public void testMessageHashIsAvailableInFile() throws IOException {
+ Truth.assertThat(mConfig.messageHashIsAvailableInFile(1)).isTrue();
+ Truth.assertThat(mConfig.messageHashIsAvailableInFile(2)).isTrue();
+ Truth.assertThat(mConfig.messageHashIsAvailableInFile(3)).isTrue();
+ Truth.assertThat(mConfig.messageHashIsAvailableInFile(4)).isTrue();
+ Truth.assertThat(mConfig.messageHashIsAvailableInFile(5)).isTrue();
+ Truth.assertThat(mConfig.messageHashIsAvailableInFile(6)).isFalse();
+ }
}
diff --git a/tests/testables/src/android/testing/OWNERS b/tests/testables/src/android/testing/OWNERS
new file mode 100644
index 000000000000..f31666b43654
--- /dev/null
+++ b/tests/testables/src/android/testing/OWNERS
@@ -0,0 +1,2 @@
+# MessageQueue-related classes
+per-file TestableLooper.java = mfasheh@google.com, shayba@google.com
diff --git a/tests/testables/src/android/testing/TestableLooper.java b/tests/testables/src/android/testing/TestableLooper.java
index be5c84c0353c..3ee6dc48bfa3 100644
--- a/tests/testables/src/android/testing/TestableLooper.java
+++ b/tests/testables/src/android/testing/TestableLooper.java
@@ -42,6 +42,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
* and provide an easy annotation for use with tests.
*
* @see TestableLooperTest TestableLooperTest for examples.
+ *
+ * @deprecated Use {@link android.os.TestLooperManager} or {@link
+ * org.robolectric.shadows.ShadowLooper} instead.
+ * This class is not actively maintained.
+ * Both of the recommended alternatives allow fine control of execution.
+ * The Robolectric class also allows advancing time.
*/
public class TestableLooper {
diff --git a/tests/utils/testutils/java/android/os/test/OWNERS b/tests/utils/testutils/java/android/os/test/OWNERS
index 3a9129e1bb69..6448261102fa 100644
--- a/tests/utils/testutils/java/android/os/test/OWNERS
+++ b/tests/utils/testutils/java/android/os/test/OWNERS
@@ -1 +1,4 @@
per-file FakePermissionEnforcer.java = file:/tests/EnforcePermission/OWNERS
+
+# MessageQueue-related classes
+per-file TestLooper.java = mfasheh@google.com, shayba@google.com
diff --git a/tests/utils/testutils/java/android/os/test/TestLooper.java b/tests/utils/testutils/java/android/os/test/TestLooper.java
index 56b0a25ed2dd..83d22d923c78 100644
--- a/tests/utils/testutils/java/android/os/test/TestLooper.java
+++ b/tests/utils/testutils/java/android/os/test/TestLooper.java
@@ -33,9 +33,15 @@ import java.lang.reflect.Method;
import java.util.concurrent.Executor;
/**
- * Creates a looper whose message queue can be manipulated
- * This allows testing code that uses a looper to dispatch messages in a deterministic manner
- * Creating a TestLooper will also install it as the looper for the current thread
+ * Creates a looper whose message queue can be manipulated This allows testing code that uses a
+ * looper to dispatch messages in a deterministic manner Creating a TestLooper will also install it
+ * as the looper for the current thread
+ *
+ * @deprecated Use {@link android.os.TestLooperManager} or {@link
+ * org.robolectric.shadows.ShadowLooper} instead.
+ * This class is not actively maintained.
+ * Both of the recommended alternatives allow fine control of execution.
+ * The Robolectric class also allows advancing time.
*/
public class TestLooper {
protected final Looper mLooper;
diff --git a/tools/aapt2/tools/finalize_res.py b/tools/aapt2/tools/finalize_res.py
index 0e4d865bc890..059f3b2087cf 100755
--- a/tools/aapt2/tools/finalize_res.py
+++ b/tools/aapt2/tools/finalize_res.py
@@ -38,13 +38,22 @@ Usage: $ANDROID_BUILD_TOP/frameworks/base/tools/aapt2/tools/finalize_res.py \
import re
import sys
+import subprocess
+from collections import defaultdict
resTypes = ["attr", "id", "style", "string", "dimen", "color", "array", "drawable", "layout",
"anim", "animator", "interpolator", "mipmap", "integer", "transition", "raw", "bool",
"fraction"]
+NO_FLAG_MAGIC_CONSTANT = "no_flag"
+
+_aconfig_map = {}
+_not_finalized = defaultdict(list)
_type_ids = {}
_type = ""
+_finalized_flags = defaultdict(list)
+_non_finalized_flags = defaultdict(list)
+
_lowest_staging_first_id = 0x01FFFFFF
@@ -53,13 +62,53 @@ _lowest_staging_first_id = 0x01FFFFFF
prefixed with removed_. The IDs are assigned without holes starting from the last ID for that
type currently finalized in public-final.xml.
"""
-def finalize_item(raw):
- name = raw.group(1)
- if re.match(r'_*removed.+', name):
- return ""
+def finalize_item(comment_and_item):
+ print("Processing:\n" + comment_and_item)
+ name = re.search('<public name="(.+?)"',comment_and_item, flags=re.DOTALL).group(1)
+ if re.match('removed_.+', name):
+ # Remove it from <staging-public-group> in public-staging.xml
+ # Include it as is in <staging-public-group-final> in public-final.xml
+ # Don't assign an id in public-final.xml
+ return ("", comment_and_item, "")
+
+ comment = re.search(' *<!--.+?-->\n', comment_and_item, flags=re.DOTALL).group(0)
+
+ match = re.search('<!-- @FlaggedApi\((.+?)\)', comment, flags=re.DOTALL)
+ if match:
+ flag = match.group(1)
+ else:
+ flag = NO_FLAG_MAGIC_CONSTANT
+
+ if flag.startswith("\""):
+ # Flag is a string value, just remove "
+ flag = flag.replace("\"", "")
+ else:
+ # Flag is a java constant, convert to string value
+ flag = flag.replace(".Flags.FLAG_", ".").lower()
+
+ if flag not in _aconfig_map:
+ raise Exception("Unknown flag: " + flag)
+
+ # READ_ONLY-ENABLED is a magic string from printflags output below
+ if _aconfig_map[flag] != "READ_ONLY-ENABLED":
+ _non_finalized_flags[flag].append(name)
+ # Keep it as is in <staging-public-group> in public-staging.xml
+ # Include as magic constant "removed_" in <staging-public-group-final> in public-final.xml
+ # Don't assign an id in public-final.xml
+ return (comment_and_item, " <public name=\"removed_\" />\n", "")
+
+ _finalized_flags[flag].append(name)
+
id = _type_ids[_type]
_type_ids[_type] += 1
- return ' <public type="%s" name="%s" id="%s" />\n' % (_type, name, '0x{0:0{1}x}'.format(id, 8))
+
+ # Removes one indentation step to align the comment with the item outside the
+ comment = re.sub("^ ", "", comment, flags=re.MULTILINE)
+
+ # Remove from <staging-public-group> in public-staging.xml
+ # Include as is in <staging-public-group-final> in public-final.xml
+ # Assign an id in public-final.xml
+ return ("", comment_and_item, comment + ' <public type="%s" name="%s" id="%s" />\n' % (_type, name, '0x{0:0{1}x}'.format(id, 8)))
"""
@@ -72,10 +121,26 @@ def finalize_group(raw):
_type = raw.group(1)
id = int(raw.group(2), 16)
_type_ids[_type] = _type_ids.get(_type, id)
- (res, count) = re.subn(' {0,4}<public name="(.+?)" */>\n', finalize_item, raw.group(3))
- if count > 0:
- res = raw.group(0).replace("staging-public-group",
- "staging-public-group-final") + '\n' + res
+
+
+ all = re.findall(' *<!--.*?<public name=".+?" */>\n', raw.group(3), flags=re.DOTALL)
+ res = ""
+ group_matches = ""
+ for match in all:
+ (staging_group, final_group, final_id_assignment) = finalize_item(match)
+
+ if staging_group:
+ _not_finalized[_type].append(staging_group)
+
+ if final_group:
+ group_matches += final_group
+
+ if final_id_assignment:
+ res += final_id_assignment
+
+ # Only add it to final.xml if new ids were actually assigned
+ if res:
+ res = '<staging-public-group-final type="%s" first-id="%s">\n%s </staging-public-group-final>\n\n%s' % (_type, raw.group(2), group_matches, res)
_lowest_staging_first_id = min(id, _lowest_staging_first_id)
return res
@@ -88,6 +153,15 @@ def collect_ids(raw):
id = int(m.group(2), 16)
_type_ids[type] = max(id + 1, _type_ids.get(type, 0))
+# This is a hack and assumes this script is run from the top directory
+output=subprocess.run("printflags --format='{fully_qualified_name} {permission}-{state}'", shell=True, capture_output=True, encoding="utf-8", check=True)
+for line in output.stdout.splitlines():
+ parts = line.split()
+ key = parts[0]
+ value = parts[1]
+ _aconfig_map[key]=value
+
+_aconfig_map[NO_FLAG_MAGIC_CONSTANT]="READ_ONLY-DISABLED"
with open(sys.argv[1], "r+") as stagingFile:
with open(sys.argv[2], "r+") as finalFile:
@@ -132,10 +206,25 @@ with open(sys.argv[1], "r+") as stagingFile:
nextId = _lowest_staging_first_id - 0x00010000
for resType in resTypes:
stagingFile.write(' <staging-public-group type="%s" first-id="%s">\n'
- ' </staging-public-group>\n\n' %
- (resType, '0x{0:0{1}x}'.format(nextId, 8)))
+ % (resType, '0x{0:0{1}x}'.format(nextId, 8)))
+ for item in _not_finalized[resType]:
+ stagingFile.write(item)
+ stagingFile.write(' </staging-public-group>\n\n')
nextId -= 0x00010000
# Close the resources tag and truncate, since the file will be shorter than the previous
stagingFile.write("</resources>\n")
stagingFile.truncate()
+
+
+print("\nFlags that had resources that were NOT finalized:")
+for flag in sorted(_non_finalized_flags.keys()):
+ print(f" {flag}")
+ for value in _non_finalized_flags[flag]:
+ print(f" {value}")
+
+print("\nFlags that had resources that were finalized:")
+for flag in sorted(_finalized_flags.keys()):
+ print(f" {flag}")
+ for value in _finalized_flags[flag]:
+ print(f" {value}")